mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
1141 lines
42 KiB
C++
1141 lines
42 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
This file is part of rippled: https://github.com/ripple/rippled
|
|
Copyright (c) 2018 Ripple Labs Inc.
|
|
|
|
Permission to use, copy, modify, and/or distribute this software for any
|
|
purpose with or without fee is hereby granted, provided that the above
|
|
copyright notice and this permission notice appear in all copies.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
//==============================================================================
|
|
|
|
#include <ripple/basics/PerfLog.h>
|
|
#include <ripple/basics/random.h>
|
|
#include <ripple/beast/unit_test.h>
|
|
#include <ripple/json/json_reader.h>
|
|
#include <ripple/protocol/jss.h>
|
|
#include <ripple/rpc/impl/Handler.h>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <random>
|
|
#include <string>
|
|
#include <test/jtx/Env.h>
|
|
#include <thread>
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
namespace ripple {
|
|
|
|
class PerfLog_test : public beast::unit_test::suite
|
|
{
|
|
enum class WithFile : bool { no = false, yes = true };
|
|
|
|
using path = boost::filesystem::path;
|
|
|
|
// We're only using Env for its Journal. That Journal gives better
|
|
// coverage in unit tests.
|
|
test::jtx::Env env_{*this};
|
|
beast::Journal j_{env_.app().journal("PerfLog_test")};
|
|
|
|
// A PerfLog needs a Parent that is a Stoppable and a function to
|
|
// call if it wants to shutdown the system. This class provides both.
|
|
struct PerfLogParent : public RootStoppable
|
|
{
|
|
bool stopSignaled{false};
|
|
beast::Journal j_;
|
|
|
|
explicit PerfLogParent(beast::Journal const& j)
|
|
: RootStoppable("testRootStoppable"), j_(j)
|
|
{
|
|
}
|
|
|
|
~PerfLogParent() override
|
|
{
|
|
doStop();
|
|
cleanupPerfLogDir();
|
|
}
|
|
|
|
// Benign replacement for Application::signalStop().
|
|
void
|
|
signalStop()
|
|
{
|
|
stopSignaled = true;
|
|
}
|
|
|
|
private:
|
|
// Interfaces for RootStoppable. Made private to encourage the use
|
|
// of doStart() and doStop().
|
|
void
|
|
onPrepare() override
|
|
{
|
|
}
|
|
|
|
void
|
|
onStart() override
|
|
{
|
|
}
|
|
|
|
void
|
|
onStop() override
|
|
{
|
|
if (areChildrenStopped())
|
|
stopped();
|
|
}
|
|
|
|
void
|
|
onChildrenStopped() override
|
|
{
|
|
onStop();
|
|
}
|
|
|
|
public:
|
|
// Test interfaces to start and stop the PerfLog
|
|
void
|
|
doStart()
|
|
{
|
|
start();
|
|
}
|
|
|
|
void
|
|
doStop()
|
|
{
|
|
if (started())
|
|
stop(j_);
|
|
}
|
|
|
|
// Interfaces for PerfLog file management
|
|
static path
|
|
getPerfLogPath()
|
|
{
|
|
using namespace boost::filesystem;
|
|
return temp_directory_path() / "perf_log_test_dir";
|
|
}
|
|
|
|
static path
|
|
getPerfLogFileName()
|
|
{
|
|
return {"perf_log.txt"};
|
|
}
|
|
|
|
static std::chrono::milliseconds
|
|
getLogInterval()
|
|
{
|
|
return std::chrono::milliseconds{10};
|
|
}
|
|
|
|
static perf::PerfLog::Setup
|
|
getSetup(WithFile withFile)
|
|
{
|
|
return perf::PerfLog::Setup{
|
|
withFile == WithFile::no
|
|
? ""
|
|
: getPerfLogPath() / getPerfLogFileName(),
|
|
getLogInterval()};
|
|
}
|
|
|
|
static void
|
|
cleanupPerfLogDir()
|
|
{
|
|
using namespace boost::filesystem;
|
|
|
|
auto const perfLogPath{getPerfLogPath()};
|
|
auto const fullPath = perfLogPath / getPerfLogFileName();
|
|
if (exists(fullPath))
|
|
remove(fullPath);
|
|
|
|
if (!exists(perfLogPath) || !is_directory(perfLogPath) ||
|
|
!is_empty(perfLogPath))
|
|
{
|
|
return;
|
|
}
|
|
remove(perfLogPath);
|
|
}
|
|
};
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
// Convenience function to return a PerfLog
|
|
std::unique_ptr<perf::PerfLog>
|
|
getPerfLog(PerfLogParent& parent, WithFile withFile)
|
|
{
|
|
return perf::make_PerfLog(
|
|
parent.getSetup(withFile), parent, j_, [&parent]() {
|
|
return parent.signalStop();
|
|
});
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
// Convenience function to return a uint64 given a Json::Value containing
|
|
// a string.
|
|
static std::uint64_t
|
|
jsonToUint64(Json::Value const& jsonUintAsString)
|
|
{
|
|
return std::stoull(jsonUintAsString.asString());
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
// The PerfLog's current state is easier to sort by duration if the
|
|
// duration is converted from string to integer. The following struct
|
|
// is a way to think about the converted entry.
|
|
struct Cur
|
|
{
|
|
std::uint64_t dur;
|
|
std::string name;
|
|
|
|
Cur(std::uint64_t d, std::string n) : dur(d), name(std::move(n))
|
|
{
|
|
}
|
|
};
|
|
|
|
// A convenience function to convert JSON to Cur and sort. The sort
|
|
// goes from longest to shortest duration. That way stuff that was started
|
|
// earlier goes to the front.
|
|
static std::vector<Cur>
|
|
getSortedCurrent(Json::Value const& currentJson)
|
|
{
|
|
std::vector<Cur> currents;
|
|
currents.reserve(currentJson.size());
|
|
for (Json::Value const& cur : currentJson)
|
|
{
|
|
currents.emplace_back(
|
|
jsonToUint64(cur[jss::duration_us]),
|
|
cur.isMember(jss::job) ? cur[jss::job].asString()
|
|
: cur[jss::method].asString());
|
|
}
|
|
|
|
// Note that the longest durations should be at the front of the
|
|
// vector since they were started first.
|
|
std::sort(
|
|
currents.begin(),
|
|
currents.end(),
|
|
[](Cur const& lhs, Cur const& rhs) {
|
|
if (lhs.dur != rhs.dur)
|
|
return (rhs.dur < lhs.dur);
|
|
return (lhs.name < rhs.name);
|
|
});
|
|
return currents;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
// Helper function that checks the size of the PerfLog file and then
|
|
// returns when the file gets bigger. This indicates that the PerfLog
|
|
// has written new values to the file and _should_ have the latest
|
|
// update.
|
|
static void
|
|
waitForFileUpdate(PerfLogParent const& parent)
|
|
{
|
|
using namespace boost::filesystem;
|
|
|
|
auto const path = parent.getPerfLogPath() / parent.getPerfLogFileName();
|
|
if (!exists(path))
|
|
return;
|
|
|
|
// We wait for the file to change size twice. The first file size
|
|
// change may have been in process while we arrived.
|
|
std::uintmax_t const firstSize{file_size(path)};
|
|
std::uintmax_t secondSize{firstSize};
|
|
do
|
|
{
|
|
std::this_thread::sleep_for(parent.getLogInterval());
|
|
secondSize = file_size(path);
|
|
} while (firstSize >= secondSize);
|
|
|
|
do
|
|
{
|
|
std::this_thread::sleep_for(parent.getLogInterval());
|
|
} while (secondSize >= file_size(path));
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
public:
|
|
void
|
|
testFileCreation()
|
|
{
|
|
using namespace boost::filesystem;
|
|
|
|
auto const perfLogPath{PerfLogParent::getPerfLogPath()};
|
|
auto const fullPath = perfLogPath / PerfLogParent::getPerfLogFileName();
|
|
{
|
|
// Verify a PerfLog creates its file when constructed.
|
|
PerfLogParent parent{j_};
|
|
BEAST_EXPECT(!exists(perfLogPath));
|
|
|
|
auto perfLog{getPerfLog(parent, WithFile::yes)};
|
|
|
|
BEAST_EXPECT(parent.stopSignaled == false);
|
|
BEAST_EXPECT(exists(perfLogPath));
|
|
}
|
|
{
|
|
// Create a file where PerfLog wants to put its directory.
|
|
// Make sure that PerfLog tries to shutdown the server since it
|
|
// can't open its file.
|
|
PerfLogParent parent{j_};
|
|
if (!BEAST_EXPECT(!exists(perfLogPath)))
|
|
return;
|
|
|
|
{
|
|
// Make a file that prevents PerfLog from creating its file.
|
|
std::ofstream nastyFile;
|
|
nastyFile.open(
|
|
perfLogPath.c_str(), std::ios::out | std::ios::app);
|
|
if (!BEAST_EXPECT(nastyFile))
|
|
return;
|
|
nastyFile.close();
|
|
}
|
|
|
|
// Now construct a PerfLog. The PerfLog should attempt to shut
|
|
// down the server because it can't open its file.
|
|
BEAST_EXPECT(parent.stopSignaled == false);
|
|
auto perfLog{getPerfLog(parent, WithFile::yes)};
|
|
BEAST_EXPECT(parent.stopSignaled == true);
|
|
|
|
// Start PerfLog and wait long enough for PerfLog::report()
|
|
// to not be able to write to its file. That should cause no
|
|
// problems.
|
|
parent.doStart();
|
|
std::this_thread::sleep_for(parent.getLogInterval() * 10);
|
|
parent.doStop();
|
|
|
|
// Remove the file.
|
|
remove(perfLogPath);
|
|
}
|
|
{
|
|
// Put a write protected file where PerfLog wants to write its
|
|
// file. Make sure that PerfLog tries to shutdown the server
|
|
// since it can't open its file.
|
|
PerfLogParent parent{j_};
|
|
if (!BEAST_EXPECT(!exists(perfLogPath)))
|
|
return;
|
|
|
|
// Construct and write protect a file to prevent PerfLog
|
|
// from creating its file.
|
|
boost::system::error_code ec;
|
|
boost::filesystem::create_directories(perfLogPath, ec);
|
|
if (!BEAST_EXPECT(!ec))
|
|
return;
|
|
|
|
auto fileWriteable = [](boost::filesystem::path const& p) -> bool {
|
|
return std::ofstream{p.c_str(), std::ios::out | std::ios::app}
|
|
.is_open();
|
|
};
|
|
|
|
if (!BEAST_EXPECT(fileWriteable(fullPath)))
|
|
return;
|
|
|
|
boost::filesystem::permissions(
|
|
fullPath,
|
|
perms::remove_perms | perms::owner_write | perms::others_write |
|
|
perms::group_write);
|
|
|
|
// If the test is running as root, then the write protect may have
|
|
// no effect. Make sure write protect worked before proceeding.
|
|
if (fileWriteable(fullPath))
|
|
{
|
|
log << "Unable to write protect file. Test skipped."
|
|
<< std::endl;
|
|
return;
|
|
}
|
|
|
|
// Now construct a PerfLog. The PerfLog should attempt to shut
|
|
// down the server because it can't open its file.
|
|
BEAST_EXPECT(parent.stopSignaled == false);
|
|
auto perfLog{getPerfLog(parent, WithFile::yes)};
|
|
BEAST_EXPECT(parent.stopSignaled == true);
|
|
|
|
// Start PerfLog and wait long enough for PerfLog::report()
|
|
// to not be able to write to its file. That should cause no
|
|
// problems.
|
|
parent.doStart();
|
|
std::this_thread::sleep_for(parent.getLogInterval() * 10);
|
|
parent.doStop();
|
|
|
|
// Fix file permissions so the file can be cleaned up.
|
|
boost::filesystem::permissions(
|
|
fullPath,
|
|
perms::add_perms | perms::owner_write | perms::others_write |
|
|
perms::group_write);
|
|
}
|
|
}
|
|
|
|
void
|
|
testRPC(WithFile withFile)
|
|
{
|
|
// Exercise the rpc interfaces of PerfLog.
|
|
// Start up the PerfLog that we'll use for testing.
|
|
PerfLogParent parent{j_};
|
|
auto perfLog{getPerfLog(parent, withFile)};
|
|
parent.doStart();
|
|
|
|
// Get the all the labels we can use for RPC interfaces without
|
|
// causing an assert.
|
|
std::vector<char const*> labels{ripple::RPC::getHandlerNames()};
|
|
std::shuffle(labels.begin(), labels.end(), default_prng());
|
|
|
|
// Get two IDs to associate with each label. Errors tend to happen at
|
|
// boundaries, so we pick IDs starting from zero and ending at
|
|
// std::uint64_t>::max().
|
|
std::vector<std::uint64_t> ids;
|
|
ids.reserve(labels.size() * 2);
|
|
std::generate_n(
|
|
std::back_inserter(ids),
|
|
labels.size(),
|
|
[i = std::numeric_limits<std::uint64_t>::min()]() mutable {
|
|
return i++;
|
|
});
|
|
std::generate_n(
|
|
std::back_inserter(ids),
|
|
labels.size(),
|
|
[i = std::numeric_limits<std::uint64_t>::max()]() mutable {
|
|
return i--;
|
|
});
|
|
std::shuffle(ids.begin(), ids.end(), default_prng());
|
|
|
|
// Start all of the RPC commands twice to show they can all be tracked
|
|
// simultaneously.
|
|
for (int labelIndex = 0; labelIndex < labels.size(); ++labelIndex)
|
|
{
|
|
for (int idIndex = 0; idIndex < 2; ++idIndex)
|
|
{
|
|
std::this_thread::sleep_for(std::chrono::microseconds(10));
|
|
perfLog->rpcStart(
|
|
labels[labelIndex], ids[(labelIndex * 2) + idIndex]);
|
|
}
|
|
}
|
|
{
|
|
// Examine current PerfLog::counterJson() values.
|
|
Json::Value const countersJson{perfLog->countersJson()["rpc"]};
|
|
BEAST_EXPECT(countersJson.size() == labels.size() + 1);
|
|
for (auto& label : labels)
|
|
{
|
|
// Expect every label in labels to have the same contents.
|
|
Json::Value const& counter{countersJson[label]};
|
|
BEAST_EXPECT(counter[jss::duration_us] == "0");
|
|
BEAST_EXPECT(counter[jss::errored] == "0");
|
|
BEAST_EXPECT(counter[jss::finished] == "0");
|
|
BEAST_EXPECT(counter[jss::started] == "2");
|
|
}
|
|
// Expect "total" to have a lot of "started"
|
|
Json::Value const& total{countersJson[jss::total]};
|
|
BEAST_EXPECT(total[jss::duration_us] == "0");
|
|
BEAST_EXPECT(total[jss::errored] == "0");
|
|
BEAST_EXPECT(total[jss::finished] == "0");
|
|
BEAST_EXPECT(jsonToUint64(total[jss::started]) == ids.size());
|
|
}
|
|
{
|
|
// Verify that every entry in labels appears twice in currents.
|
|
// If we sort by duration_us they should be in the order the
|
|
// rpcStart() call was made.
|
|
std::vector<Cur> const currents{
|
|
getSortedCurrent(perfLog->currentJson()[jss::methods])};
|
|
BEAST_EXPECT(currents.size() == labels.size() * 2);
|
|
|
|
std::uint64_t prevDur = std::numeric_limits<std::uint64_t>::max();
|
|
for (int i = 0; i < currents.size(); ++i)
|
|
{
|
|
BEAST_EXPECT(currents[i].name == labels[i / 2]);
|
|
BEAST_EXPECT(prevDur > currents[i].dur);
|
|
prevDur = currents[i].dur;
|
|
}
|
|
}
|
|
|
|
// Finish all but the first RPC command in reverse order to show that
|
|
// the start and finish of the commands can interleave. Half of the
|
|
// commands finish correctly, the other half with errors.
|
|
for (int labelIndex = labels.size() - 1; labelIndex > 0; --labelIndex)
|
|
{
|
|
std::this_thread::sleep_for(std::chrono::microseconds(10));
|
|
perfLog->rpcFinish(labels[labelIndex], ids[(labelIndex * 2) + 1]);
|
|
std::this_thread::sleep_for(std::chrono::microseconds(10));
|
|
perfLog->rpcError(labels[labelIndex], ids[(labelIndex * 2) + 0]);
|
|
}
|
|
perfLog->rpcFinish(labels[0], ids[0 + 1]);
|
|
// Note that label[0] id[0] is intentionally left unfinished.
|
|
|
|
auto validateFinalCounters = [this, &labels](
|
|
Json::Value const& countersJson) {
|
|
{
|
|
Json::Value const& jobQueue = countersJson[jss::job_queue];
|
|
BEAST_EXPECT(jobQueue.isObject());
|
|
BEAST_EXPECT(jobQueue.size() == 0);
|
|
}
|
|
|
|
Json::Value const& rpc = countersJson[jss::rpc];
|
|
BEAST_EXPECT(rpc.size() == labels.size() + 1);
|
|
|
|
// Verify that every entry in labels appears in rpc.
|
|
// If we access the entries by label we should be able to correlate
|
|
// their durations with the appropriate labels.
|
|
{
|
|
// The first label is special. It should have "errored" : "0".
|
|
Json::Value const& first = rpc[labels[0]];
|
|
BEAST_EXPECT(first[jss::duration_us] != "0");
|
|
BEAST_EXPECT(first[jss::errored] == "0");
|
|
BEAST_EXPECT(first[jss::finished] == "1");
|
|
BEAST_EXPECT(first[jss::started] == "2");
|
|
}
|
|
|
|
// Check the rest of the labels.
|
|
std::uint64_t prevDur = std::numeric_limits<std::uint64_t>::max();
|
|
for (int i = 1; i < labels.size(); ++i)
|
|
{
|
|
Json::Value const& counter{rpc[labels[i]]};
|
|
std::uint64_t const dur{
|
|
jsonToUint64(counter[jss::duration_us])};
|
|
BEAST_EXPECT(dur != 0 && dur < prevDur);
|
|
prevDur = dur;
|
|
BEAST_EXPECT(counter[jss::errored] == "1");
|
|
BEAST_EXPECT(counter[jss::finished] == "1");
|
|
BEAST_EXPECT(counter[jss::started] == "2");
|
|
}
|
|
|
|
// Check "total"
|
|
Json::Value const& total{rpc[jss::total]};
|
|
BEAST_EXPECT(total[jss::duration_us] != "0");
|
|
BEAST_EXPECT(
|
|
jsonToUint64(total[jss::errored]) == labels.size() - 1);
|
|
BEAST_EXPECT(jsonToUint64(total[jss::finished]) == labels.size());
|
|
BEAST_EXPECT(
|
|
jsonToUint64(total[jss::started]) == labels.size() * 2);
|
|
};
|
|
|
|
auto validateFinalCurrent = [this,
|
|
&labels](Json::Value const& currentJson) {
|
|
{
|
|
Json::Value const& job_queue = currentJson[jss::jobs];
|
|
BEAST_EXPECT(job_queue.isArray());
|
|
BEAST_EXPECT(job_queue.size() == 0);
|
|
}
|
|
|
|
Json::Value const& methods = currentJson[jss::methods];
|
|
BEAST_EXPECT(methods.size() == 1);
|
|
BEAST_EXPECT(methods.isArray());
|
|
|
|
Json::Value const& only = methods[0u];
|
|
BEAST_EXPECT(only.size() == 2);
|
|
BEAST_EXPECT(only.isObject());
|
|
BEAST_EXPECT(only[jss::duration_us] != "0");
|
|
BEAST_EXPECT(only[jss::method] == labels[0]);
|
|
};
|
|
|
|
// Validate the final state of the PerfLog.
|
|
validateFinalCounters(perfLog->countersJson());
|
|
validateFinalCurrent(perfLog->currentJson());
|
|
|
|
// Give the PerfLog enough time to flush it's state to the file.
|
|
waitForFileUpdate(parent);
|
|
|
|
// Politely stop the PerfLog.
|
|
parent.doStop();
|
|
|
|
auto const fullPath =
|
|
parent.getPerfLogPath() / parent.getPerfLogFileName();
|
|
|
|
if (withFile == WithFile::no)
|
|
{
|
|
BEAST_EXPECT(!exists(fullPath));
|
|
}
|
|
else
|
|
{
|
|
// The last line in the log file should contain the same
|
|
// information that countersJson() and currentJson() returned.
|
|
// Verify that.
|
|
|
|
// Get the last line of the log.
|
|
std::ifstream logStream(fullPath.c_str());
|
|
std::string lastLine;
|
|
for (std::string line; std::getline(logStream, line);)
|
|
{
|
|
if (!line.empty())
|
|
lastLine = std::move(line);
|
|
}
|
|
|
|
Json::Value parsedLastLine;
|
|
Json::Reader().parse(lastLine, parsedLastLine);
|
|
if (!BEAST_EXPECT(!RPC::contains_error(parsedLastLine)))
|
|
// Avoid cascade of failures
|
|
return;
|
|
|
|
// Validate the contents of the last line of the log.
|
|
validateFinalCounters(parsedLastLine[jss::counters]);
|
|
validateFinalCurrent(parsedLastLine[jss::current_activities]);
|
|
}
|
|
}
|
|
|
|
void
|
|
testJobs(WithFile withFile)
|
|
{
|
|
using namespace std::chrono;
|
|
|
|
// Exercise the jobs interfaces of PerfLog.
|
|
// Start up the PerfLog that we'll use for testing.
|
|
PerfLogParent parent{j_};
|
|
auto perfLog{getPerfLog(parent, withFile)};
|
|
parent.doStart();
|
|
|
|
// Get the all the JobTypes we can use to call the jobs interfaces
|
|
// without causing an assert.
|
|
struct JobName
|
|
{
|
|
JobType type;
|
|
std::string typeName;
|
|
|
|
JobName(JobType t, std::string name)
|
|
: type(t), typeName(std::move(name))
|
|
{
|
|
}
|
|
};
|
|
|
|
std::vector<JobName> jobs;
|
|
{
|
|
auto const& jobTypes = JobTypes::instance();
|
|
jobs.reserve(jobTypes.size());
|
|
for (auto const& job : jobTypes)
|
|
{
|
|
jobs.emplace_back(job.first, job.second.name());
|
|
}
|
|
}
|
|
std::shuffle(jobs.begin(), jobs.end(), default_prng());
|
|
|
|
// Walk through all of the jobs, enqueuing every job once. Check
|
|
// the jobs data with every addition.
|
|
for (int i = 0; i < jobs.size(); ++i)
|
|
{
|
|
perfLog->jobQueue(jobs[i].type);
|
|
Json::Value const jq_counters{
|
|
perfLog->countersJson()[jss::job_queue]};
|
|
|
|
BEAST_EXPECT(jq_counters.size() == i + 2);
|
|
for (int j = 0; j <= i; ++j)
|
|
{
|
|
// Verify all expected counters are present and contain
|
|
// expected values.
|
|
Json::Value const& counter{jq_counters[jobs[j].typeName]};
|
|
BEAST_EXPECT(counter.size() == 5);
|
|
BEAST_EXPECT(counter[jss::queued] == "1");
|
|
BEAST_EXPECT(counter[jss::started] == "0");
|
|
BEAST_EXPECT(counter[jss::finished] == "0");
|
|
BEAST_EXPECT(counter[jss::queued_duration_us] == "0");
|
|
BEAST_EXPECT(counter[jss::running_duration_us] == "0");
|
|
}
|
|
|
|
// Verify jss::total is present and has expected values.
|
|
Json::Value const& total{jq_counters[jss::total]};
|
|
BEAST_EXPECT(total.size() == 5);
|
|
BEAST_EXPECT(jsonToUint64(total[jss::queued]) == i + 1);
|
|
BEAST_EXPECT(total[jss::started] == "0");
|
|
BEAST_EXPECT(total[jss::finished] == "0");
|
|
BEAST_EXPECT(total[jss::queued_duration_us] == "0");
|
|
BEAST_EXPECT(total[jss::running_duration_us] == "0");
|
|
}
|
|
|
|
// Even with jobs queued, the perfLog should report nothing current.
|
|
{
|
|
Json::Value current{perfLog->currentJson()};
|
|
BEAST_EXPECT(current.size() == 2);
|
|
BEAST_EXPECT(current.isMember(jss::jobs));
|
|
BEAST_EXPECT(current[jss::jobs].size() == 0);
|
|
BEAST_EXPECT(current.isMember(jss::methods));
|
|
BEAST_EXPECT(current[jss::methods].size() == 0);
|
|
}
|
|
|
|
// Current jobs are tracked by Worker ID. Even though it's not
|
|
// realistic, crank up the number of workers so we can have many
|
|
// jobs in process simultaneously without problems.
|
|
perfLog->resizeJobs(jobs.size() * 2);
|
|
|
|
// Start two instances of every job to show that the same job can run
|
|
// simultaneously (on different Worker threads). Admittedly, this
|
|
// will make the jss::queued count look a bit goofy since there will
|
|
// be half as many queued as started...
|
|
for (int i = 0; i < jobs.size(); ++i)
|
|
{
|
|
perfLog->jobStart(
|
|
jobs[i].type, microseconds{i + 1}, steady_clock::now(), i * 2);
|
|
std::this_thread::sleep_for(microseconds(10));
|
|
|
|
// Check each jobType counter entry.
|
|
Json::Value const jq_counters{
|
|
perfLog->countersJson()[jss::job_queue]};
|
|
for (int j = 0; j < jobs.size(); ++j)
|
|
{
|
|
Json::Value const& counter{jq_counters[jobs[j].typeName]};
|
|
std::uint64_t const queued_dur_us{
|
|
jsonToUint64(counter[jss::queued_duration_us])};
|
|
if (j < i)
|
|
{
|
|
BEAST_EXPECT(counter[jss::started] == "2");
|
|
BEAST_EXPECT(queued_dur_us == j + 1);
|
|
}
|
|
else if (j == i)
|
|
{
|
|
BEAST_EXPECT(counter[jss::started] == "1");
|
|
BEAST_EXPECT(queued_dur_us == j + 1);
|
|
}
|
|
else
|
|
{
|
|
BEAST_EXPECT(counter[jss::started] == "0");
|
|
BEAST_EXPECT(queued_dur_us == 0);
|
|
}
|
|
|
|
BEAST_EXPECT(counter[jss::queued] == "1");
|
|
BEAST_EXPECT(counter[jss::finished] == "0");
|
|
BEAST_EXPECT(counter[jss::running_duration_us] == "0");
|
|
}
|
|
{
|
|
// Verify values in jss::total are what we expect.
|
|
Json::Value const& total{jq_counters[jss::total]};
|
|
BEAST_EXPECT(jsonToUint64(total[jss::queued]) == jobs.size());
|
|
BEAST_EXPECT(jsonToUint64(total[jss::started]) == (i * 2) + 1);
|
|
BEAST_EXPECT(total[jss::finished] == "0");
|
|
|
|
// Total queued duration is triangle number of (i + 1).
|
|
BEAST_EXPECT(
|
|
jsonToUint64(total[jss::queued_duration_us]) ==
|
|
(((i * i) + 3 * i + 2) / 2));
|
|
BEAST_EXPECT(total[jss::running_duration_us] == "0");
|
|
}
|
|
|
|
perfLog->jobStart(
|
|
jobs[i].type,
|
|
microseconds{0},
|
|
steady_clock::now(),
|
|
(i * 2) + 1);
|
|
std::this_thread::sleep_for(microseconds{10});
|
|
|
|
// Verify that every entry in jobs appears twice in currents.
|
|
// If we sort by duration_us they should be in the order the
|
|
// rpcStart() call was made.
|
|
std::vector<Cur> const currents{
|
|
getSortedCurrent(perfLog->currentJson()[jss::jobs])};
|
|
BEAST_EXPECT(currents.size() == (i + 1) * 2);
|
|
|
|
std::uint64_t prevDur = std::numeric_limits<std::uint64_t>::max();
|
|
for (int j = 0; j <= i; ++j)
|
|
{
|
|
BEAST_EXPECT(currents[j * 2].name == jobs[j].typeName);
|
|
BEAST_EXPECT(prevDur > currents[j * 2].dur);
|
|
prevDur = currents[j * 2].dur;
|
|
|
|
BEAST_EXPECT(currents[(j * 2) + 1].name == jobs[j].typeName);
|
|
BEAST_EXPECT(prevDur > currents[(j * 2) + 1].dur);
|
|
prevDur = currents[(j * 2) + 1].dur;
|
|
}
|
|
}
|
|
|
|
// Finish every job we started. Finish them in reverse.
|
|
for (int i = jobs.size() - 1; i >= 0; --i)
|
|
{
|
|
// A number of the computations in this loop care about the
|
|
// number of jobs that have finished. Make that available.
|
|
int const finished = ((jobs.size() - i) * 2) - 1;
|
|
perfLog->jobFinish(
|
|
jobs[i].type, microseconds(finished), (i * 2) + 1);
|
|
std::this_thread::sleep_for(microseconds(10));
|
|
|
|
Json::Value const jq_counters{
|
|
perfLog->countersJson()[jss::job_queue]};
|
|
for (int j = 0; j < jobs.size(); ++j)
|
|
{
|
|
Json::Value const& counter{jq_counters[jobs[j].typeName]};
|
|
std::uint64_t const running_dur_us{
|
|
jsonToUint64(counter[jss::running_duration_us])};
|
|
if (j < i)
|
|
{
|
|
BEAST_EXPECT(counter[jss::finished] == "0");
|
|
BEAST_EXPECT(running_dur_us == 0);
|
|
}
|
|
else if (j == i)
|
|
{
|
|
BEAST_EXPECT(counter[jss::finished] == "1");
|
|
BEAST_EXPECT(running_dur_us == ((jobs.size() - j) * 2) - 1);
|
|
}
|
|
else
|
|
{
|
|
BEAST_EXPECT(counter[jss::finished] == "2");
|
|
BEAST_EXPECT(running_dur_us == ((jobs.size() - j) * 4) - 1);
|
|
}
|
|
|
|
std::uint64_t const queued_dur_us{
|
|
jsonToUint64(counter[jss::queued_duration_us])};
|
|
BEAST_EXPECT(queued_dur_us == j + 1);
|
|
BEAST_EXPECT(counter[jss::queued] == "1");
|
|
BEAST_EXPECT(counter[jss::started] == "2");
|
|
}
|
|
{
|
|
// Verify values in jss::total are what we expect.
|
|
Json::Value const& total{jq_counters[jss::total]};
|
|
BEAST_EXPECT(jsonToUint64(total[jss::queued]) == jobs.size());
|
|
BEAST_EXPECT(
|
|
jsonToUint64(total[jss::started]) == jobs.size() * 2);
|
|
BEAST_EXPECT(jsonToUint64(total[jss::finished]) == finished);
|
|
|
|
// Total queued duration should be triangle number of
|
|
// jobs.size().
|
|
int const queuedDur = ((jobs.size() * (jobs.size() + 1)) / 2);
|
|
BEAST_EXPECT(
|
|
jsonToUint64(total[jss::queued_duration_us]) == queuedDur);
|
|
|
|
// Total running duration should be triangle number of finished.
|
|
int const runningDur = ((finished * (finished + 1)) / 2);
|
|
BEAST_EXPECT(
|
|
jsonToUint64(total[jss::running_duration_us]) ==
|
|
runningDur);
|
|
}
|
|
|
|
perfLog->jobFinish(
|
|
jobs[i].type, microseconds(finished + 1), (i * 2));
|
|
std::this_thread::sleep_for(microseconds(10));
|
|
|
|
// Verify that the two jobs we just finished no longer appear in
|
|
// currents.
|
|
std::vector<Cur> const currents{
|
|
getSortedCurrent(perfLog->currentJson()[jss::jobs])};
|
|
BEAST_EXPECT(currents.size() == i * 2);
|
|
|
|
std::uint64_t prevDur = std::numeric_limits<std::uint64_t>::max();
|
|
for (int j = 0; j < i; ++j)
|
|
{
|
|
BEAST_EXPECT(currents[j * 2].name == jobs[j].typeName);
|
|
BEAST_EXPECT(prevDur > currents[j * 2].dur);
|
|
prevDur = currents[j * 2].dur;
|
|
|
|
BEAST_EXPECT(currents[(j * 2) + 1].name == jobs[j].typeName);
|
|
BEAST_EXPECT(prevDur > currents[(j * 2) + 1].dur);
|
|
prevDur = currents[(j * 2) + 1].dur;
|
|
}
|
|
}
|
|
|
|
// Validate the final results.
|
|
auto validateFinalCounters = [this,
|
|
&jobs](Json::Value const& countersJson) {
|
|
{
|
|
Json::Value const& rpc = countersJson[jss::rpc];
|
|
BEAST_EXPECT(rpc.isObject());
|
|
BEAST_EXPECT(rpc.size() == 0);
|
|
}
|
|
|
|
Json::Value const& jobQueue = countersJson[jss::job_queue];
|
|
for (int i = jobs.size() - 1; i >= 0; --i)
|
|
{
|
|
Json::Value const& counter{jobQueue[jobs[i].typeName]};
|
|
std::uint64_t const running_dur_us{
|
|
jsonToUint64(counter[jss::running_duration_us])};
|
|
BEAST_EXPECT(running_dur_us == ((jobs.size() - i) * 4) - 1);
|
|
|
|
std::uint64_t const queued_dur_us{
|
|
jsonToUint64(counter[jss::queued_duration_us])};
|
|
BEAST_EXPECT(queued_dur_us == i + 1);
|
|
|
|
BEAST_EXPECT(counter[jss::queued] == "1");
|
|
BEAST_EXPECT(counter[jss::started] == "2");
|
|
BEAST_EXPECT(counter[jss::finished] == "2");
|
|
}
|
|
|
|
// Verify values in jss::total are what we expect.
|
|
Json::Value const& total{jobQueue[jss::total]};
|
|
const int finished = jobs.size() * 2;
|
|
BEAST_EXPECT(jsonToUint64(total[jss::queued]) == jobs.size());
|
|
BEAST_EXPECT(jsonToUint64(total[jss::started]) == finished);
|
|
BEAST_EXPECT(jsonToUint64(total[jss::finished]) == finished);
|
|
|
|
// Total queued duration should be triangle number of
|
|
// jobs.size().
|
|
int const queuedDur = ((jobs.size() * (jobs.size() + 1)) / 2);
|
|
BEAST_EXPECT(
|
|
jsonToUint64(total[jss::queued_duration_us]) == queuedDur);
|
|
|
|
// Total running duration should be triangle number of finished.
|
|
int const runningDur = ((finished * (finished + 1)) / 2);
|
|
BEAST_EXPECT(
|
|
jsonToUint64(total[jss::running_duration_us]) == runningDur);
|
|
};
|
|
|
|
auto validateFinalCurrent = [this](Json::Value const& currentJson) {
|
|
{
|
|
Json::Value const& j = currentJson[jss::jobs];
|
|
BEAST_EXPECT(j.isArray());
|
|
BEAST_EXPECT(j.size() == 0);
|
|
}
|
|
|
|
Json::Value const& methods = currentJson[jss::methods];
|
|
BEAST_EXPECT(methods.size() == 0);
|
|
BEAST_EXPECT(methods.isArray());
|
|
};
|
|
|
|
// Validate the final state of the PerfLog.
|
|
validateFinalCounters(perfLog->countersJson());
|
|
validateFinalCurrent(perfLog->currentJson());
|
|
|
|
// Give the PerfLog enough time to flush it's state to the file.
|
|
waitForFileUpdate(parent);
|
|
|
|
// Politely stop the PerfLog.
|
|
parent.doStop();
|
|
|
|
// Check file contents if that is appropriate.
|
|
auto const fullPath =
|
|
parent.getPerfLogPath() / parent.getPerfLogFileName();
|
|
|
|
if (withFile == WithFile::no)
|
|
{
|
|
BEAST_EXPECT(!exists(fullPath));
|
|
}
|
|
else
|
|
{
|
|
// The last line in the log file should contain the same
|
|
// information that countersJson() and currentJson() returned.
|
|
// Verify that.
|
|
|
|
// Get the last line of the log.
|
|
std::ifstream logStream(fullPath.c_str());
|
|
std::string lastLine;
|
|
for (std::string line; std::getline(logStream, line);)
|
|
{
|
|
if (!line.empty())
|
|
lastLine = std::move(line);
|
|
}
|
|
|
|
Json::Value parsedLastLine;
|
|
Json::Reader().parse(lastLine, parsedLastLine);
|
|
if (!BEAST_EXPECT(!RPC::contains_error(parsedLastLine)))
|
|
// Avoid cascade of failures
|
|
return;
|
|
|
|
// Validate the contents of the last line of the log.
|
|
validateFinalCounters(parsedLastLine[jss::counters]);
|
|
validateFinalCurrent(parsedLastLine[jss::current_activities]);
|
|
}
|
|
}
|
|
|
|
void
|
|
testInvalidID(WithFile withFile)
|
|
{
|
|
using namespace std::chrono;
|
|
|
|
// The Worker ID is used to identify jobs in progress. Show that
|
|
// the PerLog behaves as well as possible if an invalid ID is passed.
|
|
|
|
// Start up the PerfLog that we'll use for testing.
|
|
PerfLogParent parent{j_};
|
|
auto perfLog{getPerfLog(parent, withFile)};
|
|
parent.doStart();
|
|
|
|
// Randomly select a job type and its name.
|
|
JobType jobType;
|
|
std::string jobTypeName;
|
|
{
|
|
auto const& jobTypes = JobTypes::instance();
|
|
|
|
std::uniform_int_distribution<> dis(0, jobTypes.size() - 1);
|
|
auto iter{jobTypes.begin()};
|
|
std::advance(iter, dis(default_prng()));
|
|
|
|
jobType = iter->second.type();
|
|
jobTypeName = iter->second.name();
|
|
}
|
|
|
|
// Say there's one worker thread.
|
|
perfLog->resizeJobs(1);
|
|
|
|
// Lambda to validate countersJson for this test.
|
|
auto verifyCounters = [this, jobTypeName](
|
|
Json::Value const& countersJson,
|
|
int started,
|
|
int finished,
|
|
int queued_us,
|
|
int running_us) {
|
|
BEAST_EXPECT(countersJson.isObject());
|
|
BEAST_EXPECT(countersJson.size() == 2);
|
|
|
|
BEAST_EXPECT(countersJson.isMember(jss::rpc));
|
|
BEAST_EXPECT(countersJson[jss::rpc].isObject());
|
|
BEAST_EXPECT(countersJson[jss::rpc].size() == 0);
|
|
|
|
BEAST_EXPECT(countersJson.isMember(jss::job_queue));
|
|
BEAST_EXPECT(countersJson[jss::job_queue].isObject());
|
|
BEAST_EXPECT(countersJson[jss::job_queue].size() == 1);
|
|
{
|
|
Json::Value const& job{
|
|
countersJson[jss::job_queue][jobTypeName]};
|
|
|
|
BEAST_EXPECT(job.isObject());
|
|
BEAST_EXPECT(jsonToUint64(job[jss::queued]) == 0);
|
|
BEAST_EXPECT(jsonToUint64(job[jss::started]) == started);
|
|
BEAST_EXPECT(jsonToUint64(job[jss::finished]) == finished);
|
|
|
|
BEAST_EXPECT(
|
|
jsonToUint64(job[jss::queued_duration_us]) == queued_us);
|
|
BEAST_EXPECT(
|
|
jsonToUint64(job[jss::running_duration_us]) == running_us);
|
|
}
|
|
};
|
|
|
|
// Lambda to validate currentJson (always empty) for this test.
|
|
auto verifyEmptyCurrent = [this](Json::Value const& currentJson) {
|
|
BEAST_EXPECT(currentJson.isObject());
|
|
BEAST_EXPECT(currentJson.size() == 2);
|
|
|
|
BEAST_EXPECT(currentJson.isMember(jss::jobs));
|
|
BEAST_EXPECT(currentJson[jss::jobs].isArray());
|
|
BEAST_EXPECT(currentJson[jss::jobs].size() == 0);
|
|
|
|
BEAST_EXPECT(currentJson.isMember(jss::methods));
|
|
BEAST_EXPECT(currentJson[jss::methods].isArray());
|
|
BEAST_EXPECT(currentJson[jss::methods].size() == 0);
|
|
};
|
|
|
|
// Start an ID that's too large.
|
|
perfLog->jobStart(jobType, microseconds{11}, steady_clock::now(), 2);
|
|
std::this_thread::sleep_for(microseconds{10});
|
|
verifyCounters(perfLog->countersJson(), 1, 0, 11, 0);
|
|
verifyEmptyCurrent(perfLog->currentJson());
|
|
|
|
// Start a negative ID
|
|
perfLog->jobStart(jobType, microseconds{13}, steady_clock::now(), -1);
|
|
std::this_thread::sleep_for(microseconds{10});
|
|
verifyCounters(perfLog->countersJson(), 2, 0, 24, 0);
|
|
verifyEmptyCurrent(perfLog->currentJson());
|
|
|
|
// Finish the too large ID
|
|
perfLog->jobFinish(jobType, microseconds{17}, 2);
|
|
std::this_thread::sleep_for(microseconds{10});
|
|
verifyCounters(perfLog->countersJson(), 2, 1, 24, 17);
|
|
verifyEmptyCurrent(perfLog->currentJson());
|
|
|
|
// Finish the negative ID
|
|
perfLog->jobFinish(jobType, microseconds{19}, -1);
|
|
std::this_thread::sleep_for(microseconds{10});
|
|
verifyCounters(perfLog->countersJson(), 2, 2, 24, 36);
|
|
verifyEmptyCurrent(perfLog->currentJson());
|
|
|
|
// Give the PerfLog enough time to flush it's state to the file.
|
|
waitForFileUpdate(parent);
|
|
|
|
// Politely stop the PerfLog.
|
|
parent.doStop();
|
|
|
|
// Check file contents if that is appropriate.
|
|
auto const fullPath =
|
|
parent.getPerfLogPath() / parent.getPerfLogFileName();
|
|
|
|
if (withFile == WithFile::no)
|
|
{
|
|
BEAST_EXPECT(!exists(fullPath));
|
|
}
|
|
else
|
|
{
|
|
// The last line in the log file should contain the same
|
|
// information that countersJson() and currentJson() returned.
|
|
// Verify that.
|
|
|
|
// Get the last line of the log.
|
|
std::ifstream logStream(fullPath.c_str());
|
|
std::string lastLine;
|
|
for (std::string line; std::getline(logStream, line);)
|
|
{
|
|
if (!line.empty())
|
|
lastLine = std::move(line);
|
|
}
|
|
|
|
Json::Value parsedLastLine;
|
|
Json::Reader().parse(lastLine, parsedLastLine);
|
|
if (!BEAST_EXPECT(!RPC::contains_error(parsedLastLine)))
|
|
// Avoid cascade of failures
|
|
return;
|
|
|
|
// Validate the contents of the last line of the log.
|
|
verifyCounters(parsedLastLine[jss::counters], 2, 2, 24, 36);
|
|
verifyEmptyCurrent(parsedLastLine[jss::current_activities]);
|
|
}
|
|
}
|
|
|
|
void
|
|
testRotate(WithFile withFile)
|
|
{
|
|
// We can't fully test rotate because unit tests must run on Windows,
|
|
// and Windows doesn't (may not?) support rotate. But at least call
|
|
// the interface and see that it doesn't crash.
|
|
using namespace boost::filesystem;
|
|
|
|
auto const perfLogPath{PerfLogParent::getPerfLogPath()};
|
|
auto const fullPath = perfLogPath / PerfLogParent::getPerfLogFileName();
|
|
|
|
PerfLogParent parent{j_};
|
|
BEAST_EXPECT(!exists(perfLogPath));
|
|
|
|
auto perfLog{getPerfLog(parent, withFile)};
|
|
|
|
BEAST_EXPECT(parent.stopSignaled == false);
|
|
if (withFile == WithFile::no)
|
|
{
|
|
BEAST_EXPECT(!exists(perfLogPath));
|
|
}
|
|
else
|
|
{
|
|
BEAST_EXPECT(exists(fullPath));
|
|
BEAST_EXPECT(file_size(fullPath) == 0);
|
|
}
|
|
|
|
// Start PerfLog and wait long enough for PerfLog::report()
|
|
// to write to its file.
|
|
parent.doStart();
|
|
waitForFileUpdate(parent);
|
|
|
|
decltype(file_size(fullPath)) firstFileSize{0};
|
|
if (withFile == WithFile::no)
|
|
{
|
|
BEAST_EXPECT(!exists(perfLogPath));
|
|
}
|
|
else
|
|
{
|
|
firstFileSize = file_size(fullPath);
|
|
BEAST_EXPECT(firstFileSize > 0);
|
|
}
|
|
|
|
// Rotate and then wait to make sure more stuff is written to the file.
|
|
perfLog->rotate();
|
|
waitForFileUpdate(parent);
|
|
|
|
parent.doStop();
|
|
|
|
if (withFile == WithFile::no)
|
|
{
|
|
BEAST_EXPECT(!exists(perfLogPath));
|
|
}
|
|
else
|
|
{
|
|
BEAST_EXPECT(file_size(fullPath) > firstFileSize);
|
|
}
|
|
}
|
|
|
|
void
|
|
run() override
|
|
{
|
|
testFileCreation();
|
|
testRPC(WithFile::no);
|
|
testRPC(WithFile::yes);
|
|
testJobs(WithFile::no);
|
|
testJobs(WithFile::yes);
|
|
testInvalidID(WithFile::no);
|
|
testInvalidID(WithFile::yes);
|
|
testRotate(WithFile::no);
|
|
testRotate(WithFile::yes);
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE(PerfLog, basics, ripple);
|
|
|
|
} // namespace ripple
|