2016-07-31 10:18:03 -05:00
|
|
|
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
|
|
|
|
/*
|
|
|
|
* This file is part of the LibreOffice project.
|
|
|
|
*
|
|
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
|
|
|
#include <algorithm>
|
2016-08-01 21:21:06 -05:00
|
|
|
#include <chrono>
|
2016-07-31 10:18:03 -05:00
|
|
|
#include <cstdlib>
|
|
|
|
#include <cstring>
|
|
|
|
#include <fstream>
|
|
|
|
#include <iostream>
|
|
|
|
#include <random>
|
2016-08-01 21:21:06 -05:00
|
|
|
#include <thread>
|
2016-07-31 10:18:03 -05:00
|
|
|
|
|
|
|
#include <Poco/Net/NetException.h>
|
|
|
|
#include <Poco/Net/HTTPRequest.h>
|
|
|
|
#include <Poco/Net/HTTPResponse.h>
|
|
|
|
#include <Poco/Net/KeyConsoleHandler.h>
|
|
|
|
#include <Poco/Net/AcceptCertificateHandler.h>
|
|
|
|
#include <Poco/StreamCopier.h>
|
|
|
|
#include <Poco/URI.h>
|
|
|
|
#include <Poco/Process.h>
|
|
|
|
#include <Poco/StringTokenizer.h>
|
|
|
|
#include <Poco/Thread.h>
|
|
|
|
#include <Poco/Timespan.h>
|
|
|
|
#include <Poco/Timestamp.h>
|
|
|
|
#include <Poco/URI.h>
|
|
|
|
#include <Poco/Util/Application.h>
|
|
|
|
#include <Poco/Util/HelpFormatter.h>
|
|
|
|
#include <Poco/Util/Option.h>
|
|
|
|
#include <Poco/Util/OptionSet.h>
|
|
|
|
|
2016-07-31 13:17:52 -05:00
|
|
|
#include <Poco/Util/Application.h>
|
|
|
|
#include <Poco/Util/OptionSet.h>
|
|
|
|
|
2016-07-31 10:18:03 -05:00
|
|
|
#include "Common.hpp"
|
|
|
|
#include "LOOLProtocol.hpp"
|
2016-07-31 13:17:52 -05:00
|
|
|
#include "TraceFile.hpp"
|
2016-07-31 10:18:03 -05:00
|
|
|
#include "Util.hpp"
|
|
|
|
#include "test/helpers.hpp"
|
|
|
|
|
|
|
|
/// Stress testing and performance/scalability benchmarking tool.
|
|
|
|
|
|
|
|
class Stress: public Poco::Util::Application
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
Stress();
|
|
|
|
~Stress() {}
|
|
|
|
|
|
|
|
unsigned _numClients;
|
|
|
|
std::string _serverURI;
|
|
|
|
|
|
|
|
protected:
|
|
|
|
void defineOptions(Poco::Util::OptionSet& options) override;
|
|
|
|
void handleOption(const std::string& name, const std::string& value) override;
|
|
|
|
int main(const std::vector<std::string>& args) override;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
using namespace LOOLProtocol;
|
|
|
|
|
|
|
|
using Poco::Net::HTTPClientSession;
|
|
|
|
using Poco::Net::HTTPRequest;
|
|
|
|
using Poco::Net::HTTPResponse;
|
|
|
|
using Poco::Runnable;
|
|
|
|
using Poco::Thread;
|
|
|
|
using Poco::URI;
|
|
|
|
using Poco::Util::Application;
|
|
|
|
using Poco::Util::HelpFormatter;
|
|
|
|
using Poco::Util::Option;
|
|
|
|
using Poco::Util::OptionSet;
|
|
|
|
|
2016-08-04 09:31:11 -05:00
|
|
|
class Connection
|
2016-07-31 10:18:03 -05:00
|
|
|
{
|
|
|
|
public:
|
2016-08-04 09:31:11 -05:00
|
|
|
static
|
|
|
|
std::unique_ptr<Connection> create(const std::string& serverURI, const std::string& documentURL, const std::string& sessionId)
|
|
|
|
{
|
|
|
|
Poco::URI uri(serverURI);
|
2016-07-31 10:18:03 -05:00
|
|
|
|
2016-08-04 09:31:11 -05:00
|
|
|
// Load a document and get its status.
|
|
|
|
std::cerr << "NewSession [" << sessionId << "]: " << uri.toString() << "... ";
|
|
|
|
Poco::Net::HTTPRequest request(Poco::Net::HTTPRequest::HTTP_GET, "/lool/ws/" + documentURL);
|
|
|
|
Poco::Net::HTTPResponse response;
|
|
|
|
auto ws = helpers::connectLOKit(uri, request, response, "loolStress ");
|
|
|
|
std::cerr << "Connected.\n";
|
|
|
|
return std::unique_ptr<Connection>(new Connection(documentURL, sessionId, ws));
|
|
|
|
}
|
|
|
|
|
|
|
|
void send(const std::string& data) const
|
2016-07-31 10:18:03 -05:00
|
|
|
{
|
2016-08-04 09:31:11 -05:00
|
|
|
helpers::sendTextFrame(_ws, data, "loolstress ");
|
2016-07-31 10:18:03 -05:00
|
|
|
}
|
|
|
|
|
2016-08-04 09:31:11 -05:00
|
|
|
private:
|
|
|
|
Connection(const std::string& documentURL, const std::string& sessionId, std::shared_ptr<Poco::Net::WebSocket>& ws) :
|
|
|
|
_documentURL(documentURL),
|
|
|
|
_sessionId(sessionId),
|
|
|
|
_ws(ws)
|
2016-07-31 10:18:03 -05:00
|
|
|
{
|
2016-08-04 09:31:11 -05:00
|
|
|
}
|
2016-07-31 10:18:03 -05:00
|
|
|
|
2016-08-04 09:31:11 -05:00
|
|
|
private:
|
|
|
|
const std::string _documentURL;
|
|
|
|
const std::string _sessionId;
|
|
|
|
std::shared_ptr<Poco::Net::WebSocket> _ws;
|
|
|
|
};
|
2016-07-31 10:18:03 -05:00
|
|
|
|
2016-08-04 09:31:11 -05:00
|
|
|
class Worker: public Runnable
|
|
|
|
{
|
|
|
|
public:
|
2016-07-31 10:18:03 -05:00
|
|
|
|
2016-08-04 09:31:11 -05:00
|
|
|
Worker(Stress& app, const std::string& traceFilePath) :
|
|
|
|
_app(app),
|
|
|
|
_traceFile(traceFilePath)
|
|
|
|
{
|
|
|
|
}
|
2016-07-31 10:18:03 -05:00
|
|
|
|
2016-08-04 09:31:11 -05:00
|
|
|
void run() override
|
|
|
|
{
|
2016-08-01 21:21:06 -05:00
|
|
|
const auto epochStart(std::chrono::steady_clock::now());
|
2016-07-31 10:18:03 -05:00
|
|
|
try
|
|
|
|
{
|
2016-08-01 21:21:06 -05:00
|
|
|
for (;;)
|
|
|
|
{
|
2016-08-04 09:31:11 -05:00
|
|
|
const auto rec = _traceFile.getNextRecord();
|
2016-08-01 21:21:06 -05:00
|
|
|
if (rec.Dir == TraceFileRecord::Direction::Invalid)
|
|
|
|
{
|
2016-08-04 09:31:11 -05:00
|
|
|
// End of trace file.
|
2016-08-01 21:21:06 -05:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto delta = (epochStart - std::chrono::steady_clock::now());
|
|
|
|
const auto delay = rec.TimestampNs - std::chrono::duration_cast<std::chrono::microseconds>(delta).count();
|
|
|
|
if (delay > 0)
|
|
|
|
{
|
|
|
|
std::this_thread::sleep_for(std::chrono::microseconds(delay));
|
|
|
|
}
|
|
|
|
|
2016-08-04 09:31:11 -05:00
|
|
|
if (rec.Dir == TraceFileRecord::Direction::Event)
|
|
|
|
{
|
|
|
|
// Meta info about about an event.
|
|
|
|
static const std::string NewSession("NewSession: ");
|
|
|
|
static const std::string EndSession("EndSession: ");
|
|
|
|
|
|
|
|
if (rec.Payload.find(NewSession) == 0)
|
|
|
|
{
|
|
|
|
const auto& uri = rec.Payload.substr(NewSession.size());
|
|
|
|
auto it = Sessions.find(uri);
|
|
|
|
if (it != Sessions.end())
|
|
|
|
{
|
|
|
|
// Add a new session.
|
|
|
|
if (it->second.find(rec.SessionId) != it->second.end())
|
|
|
|
{
|
|
|
|
std::cerr << "ERROR: session [" << rec.SessionId << "] already exists on doc [" << uri << "]\n";
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
it->second.emplace(rec.SessionId, Connection::create(_app._serverURI, uri, rec.SessionId));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
std::cerr << "New Document: " << uri << "\n";
|
|
|
|
ChildToDoc.emplace(rec.Pid, uri);
|
|
|
|
Sessions[uri].emplace(rec.SessionId, Connection::create(_app._serverURI, uri, rec.SessionId));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (rec.Payload.find(EndSession) == 0)
|
|
|
|
{
|
|
|
|
const auto& uri = rec.Payload.substr(EndSession.size());
|
|
|
|
auto it = Sessions.find(uri);
|
|
|
|
if (it != Sessions.end())
|
|
|
|
{
|
|
|
|
std::cerr << "EndSession [" << rec.SessionId << "]: " << uri << "\n";
|
|
|
|
|
|
|
|
it->second.erase(rec.SessionId);
|
|
|
|
if (it->second.empty())
|
|
|
|
{
|
|
|
|
std::cerr << "End Doc [" << uri << "].\n";
|
|
|
|
Sessions.erase(it);
|
|
|
|
ChildToDoc.erase(rec.Pid);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
std::cerr << "ERROR: Doc [" << uri << "] does not exist.\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (rec.Dir == TraceFileRecord::Direction::Incoming)
|
|
|
|
{
|
|
|
|
auto docIt = ChildToDoc.find(rec.Pid);
|
|
|
|
if (docIt != ChildToDoc.end())
|
|
|
|
{
|
|
|
|
const auto& uri = docIt->second;
|
|
|
|
auto it = Sessions.find(uri);
|
|
|
|
if (it != Sessions.end())
|
|
|
|
{
|
|
|
|
const auto sessionIt = it->second.find(rec.SessionId);
|
|
|
|
if (sessionIt != it->second.end())
|
|
|
|
{
|
|
|
|
sessionIt->second->send(rec.Payload);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
std::cerr << "ERROR: Doc [" << uri << "] does not exist.\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
std::cerr << "ERROR: Unknown PID [" << rec.Pid << "] maps to no active document.\n";
|
|
|
|
}
|
|
|
|
}
|
2016-08-01 21:21:06 -05:00
|
|
|
}
|
2016-07-31 10:18:03 -05:00
|
|
|
}
|
|
|
|
catch (const Poco::Exception &e)
|
|
|
|
{
|
|
|
|
std::cerr << "Failed to write data: " << e.name() <<
|
|
|
|
" " << e.message() << "\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
Stress& _app;
|
2016-07-31 13:17:52 -05:00
|
|
|
TraceFileReader _traceFile;
|
2016-08-04 09:31:11 -05:00
|
|
|
|
|
|
|
/// LOK child process PID to Doc URI map.
|
|
|
|
std::map<unsigned, std::string> ChildToDoc;
|
|
|
|
|
|
|
|
/// Doc URI to Sessions map. Sessions are maps of SessionID to Connection.
|
|
|
|
std::map<std::string, std::map<std::string, std::unique_ptr<Connection>>> Sessions;
|
2016-07-31 10:18:03 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
Stress::Stress() :
|
2016-08-01 20:53:09 -05:00
|
|
|
_numClients(1),
|
2016-07-31 10:18:03 -05:00
|
|
|
#if ENABLE_SSL
|
|
|
|
_serverURI("https://127.0.0.1:" + std::to_string(DEFAULT_CLIENT_PORT_NUMBER))
|
|
|
|
#else
|
|
|
|
_serverURI("http://127.0.0.1:" + std::to_string(DEFAULT_CLIENT_PORT_NUMBER))
|
|
|
|
#endif
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
void Stress::defineOptions(OptionSet& optionSet)
|
|
|
|
{
|
|
|
|
Application::defineOptions(optionSet);
|
|
|
|
|
|
|
|
optionSet.addOption(Option("help", "", "Display help information on command line arguments.")
|
|
|
|
.required(false).repeatable(false));
|
|
|
|
optionSet.addOption(Option("clientsperdoc", "", "Number of simultaneous clients on each doc.")
|
|
|
|
.required(false).repeatable(false)
|
|
|
|
.argument("concurrency"));
|
|
|
|
optionSet.addOption(Option("server", "", "URI of LOOL server")
|
|
|
|
.required(false).repeatable(false)
|
|
|
|
.argument("uri"));
|
|
|
|
}
|
|
|
|
|
|
|
|
void Stress::handleOption(const std::string& optionName,
|
|
|
|
const std::string& value)
|
|
|
|
{
|
|
|
|
Application::handleOption(optionName, value);
|
|
|
|
|
|
|
|
if (optionName == "help")
|
|
|
|
{
|
|
|
|
HelpFormatter helpFormatter(options());
|
|
|
|
|
|
|
|
helpFormatter.setCommand(commandName());
|
|
|
|
helpFormatter.setUsage("OPTIONS");
|
|
|
|
helpFormatter.setHeader("LibreOffice On-Line tool.");
|
|
|
|
helpFormatter.format(std::cout);
|
|
|
|
std::exit(Application::EXIT_OK);
|
|
|
|
}
|
|
|
|
else if (optionName == "clientsperdoc")
|
|
|
|
_numClients = std::max(std::stoi(value), 1);
|
|
|
|
else if (optionName == "server")
|
|
|
|
_serverURI = value;
|
|
|
|
else
|
|
|
|
{
|
|
|
|
std::cerr << "Unknown option: " << optionName << std::endl;
|
|
|
|
exit(1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int Stress::main(const std::vector<std::string>& args)
|
|
|
|
{
|
|
|
|
std::vector<std::unique_ptr<Thread>> clients(_numClients * args.size());
|
|
|
|
|
|
|
|
std::cout << "Args: " << args.size() << std::endl;
|
|
|
|
|
|
|
|
unsigned index = 0;
|
|
|
|
for (unsigned i = 0; i < args.size(); ++i)
|
|
|
|
{
|
|
|
|
std::cout << "Arg: " << args[i] << std::endl;
|
|
|
|
for (unsigned j = 0; j < _numClients; ++j, ++index)
|
|
|
|
{
|
|
|
|
clients[index].reset(new Thread());
|
|
|
|
clients[index]->start(*(new Worker(*this, args[i])));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const auto& client : clients)
|
|
|
|
{
|
|
|
|
client->join();
|
|
|
|
}
|
|
|
|
|
|
|
|
return Application::EXIT_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
POCO_APP_MAIN(Stress)
|
|
|
|
|
|
|
|
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|