rippled
Loading...
Searching...
No Matches
PerfLog_test.cpp
1#include <test/jtx/Env.h>
2#include <test/jtx/TestHelpers.h>
3
4#include <xrpld/rpc/detail/Handler.h>
5
6#include <xrpl/basics/random.h>
7#include <xrpl/beast/unit_test.h>
8#include <xrpl/beast/utility/Journal.h>
9#include <xrpl/core/PerfLog.h>
10#include <xrpl/json/json_reader.h>
11#include <xrpl/protocol/jss.h>
12
13#include <atomic>
14#include <chrono>
15#include <cmath>
16#include <string>
17#include <thread>
18
19//------------------------------------------------------------------------------
20
21namespace xrpl {
22
24{
25 enum class WithFile : bool { no = false, yes = true };
26
27 using path = boost::filesystem::path;
28
29 // We're only using Env for its Journal. That Journal gives better
30 // coverage in unit tests.
32 beast::Journal j_{env_.app().journal("PerfLog_test")};
33
34 struct Fixture
35 {
38 bool stopSignaled{false};
39
40 explicit Fixture(Application& app, beast::Journal j) : app_(app), j_(j)
41 {
42 }
43
45 {
46 using namespace boost::filesystem;
47
48 auto const dir{logDir()};
49 auto const file{logFile()};
50 if (exists(file))
51 remove(file);
52
53 if (!exists(dir) || !is_directory(dir) || !is_empty(dir))
54 {
55 return;
56 }
57 remove(dir);
58 }
59
60 void
62 {
63 stopSignaled = true;
64 }
65
66 path
67 logDir() const
68 {
69 using namespace boost::filesystem;
70 return temp_directory_path() / "perf_log_test_dir";
71 }
72
73 path
74 logFile() const
75 {
76 return logDir() / "perf_log.txt";
77 }
78
81 {
83 }
84
86 perfLog(WithFile withFile)
87 {
88 perf::PerfLog::Setup const setup{withFile == WithFile::no ? "" : logFile(), logInterval()};
89 return perf::make_PerfLog(setup, app_, j_, [this]() { return signalStop(); });
90 }
91
92 // Block until the log file has grown in size, indicating that the
93 // PerfLog has written new values to the file and _should_ have the
94 // latest update.
95 void
96 wait() const
97 {
98 using namespace boost::filesystem;
99
100 auto const path = logFile();
101 if (!exists(path))
102 return;
103
104 // We wait for the file to change size twice. The first file size
105 // change may have been in process while we arrived.
106 std::uintmax_t const firstSize{file_size(path)};
107 std::uintmax_t secondSize{firstSize};
108 do
109 {
111 secondSize = file_size(path);
112 } while (firstSize >= secondSize);
113
114 do
115 {
117 } while (secondSize >= file_size(path));
118 }
119 };
120
121 // Return a uint64 from a JSON string.
122 static std::uint64_t
123 jsonToUint64(Json::Value const& jsonUintAsString)
124 {
125 return std::stoull(jsonUintAsString.asString());
126 }
127
128 // The PerfLog's current state is easier to sort by duration if the
129 // duration is converted from string to integer. The following struct
130 // is a way to think about the converted entry.
131 struct Cur
132 {
135
136 Cur(std::uint64_t d, std::string n) : dur(d), name(std::move(n))
137 {
138 }
139 };
140
141 // A convenience function to convert JSON to Cur and sort. The sort
142 // goes from longest to shortest duration. That way stuff that was started
143 // earlier goes to the front.
144 static std::vector<Cur>
145 getSortedCurrent(Json::Value const& currentJson)
146 {
147 std::vector<Cur> currents;
148 currents.reserve(currentJson.size());
149 for (Json::Value const& cur : currentJson)
150 {
151 currents.emplace_back(
152 jsonToUint64(cur[jss::duration_us]),
153 cur.isMember(jss::job) ? cur[jss::job].asString() : cur[jss::method].asString());
154 }
155
156 // Note that the longest durations should be at the front of the
157 // vector since they were started first.
158 std::sort(currents.begin(), currents.end(), [](Cur const& lhs, Cur const& rhs) {
159 if (lhs.dur != rhs.dur)
160 return (rhs.dur < lhs.dur);
161 return (lhs.name < rhs.name);
162 });
163 return currents;
164 }
165
166public:
167 void
169 {
170 using namespace boost::filesystem;
171
172 {
173 // Verify a PerfLog creates its file when constructed.
174 Fixture fixture{env_.app(), j_};
175 BEAST_EXPECT(!exists(fixture.logFile()));
176
177 auto perfLog{fixture.perfLog(WithFile::yes)};
178
179 BEAST_EXPECT(fixture.stopSignaled == false);
180 BEAST_EXPECT(exists(fixture.logFile()));
181 }
182 {
183 // Create a file where PerfLog wants to put its directory.
184 // Make sure that PerfLog tries to shutdown the server since it
185 // can't open its file.
186 Fixture fixture{env_.app(), j_};
187 if (!BEAST_EXPECT(!exists(fixture.logDir())))
188 return;
189
190 {
191 // Make a file that prevents PerfLog from creating its file.
192 std::ofstream nastyFile;
193 nastyFile.open(fixture.logDir().c_str(), std::ios::out | std::ios::app);
194 if (!BEAST_EXPECT(nastyFile))
195 return;
196 nastyFile.close();
197 }
198
199 // Now construct a PerfLog. The PerfLog should attempt to shut
200 // down the server because it can't open its file.
201 BEAST_EXPECT(fixture.stopSignaled == false);
202 auto perfLog{fixture.perfLog(WithFile::yes)};
203 BEAST_EXPECT(fixture.stopSignaled == true);
204
205 // Start PerfLog and wait long enough for PerfLog::report()
206 // to not be able to write to its file. That should cause no
207 // problems.
208 perfLog->start();
209 std::this_thread::sleep_for(fixture.logInterval() * 10);
210 perfLog->stop();
211
212 // Remove the file.
213 remove(fixture.logDir());
214 }
215 {
216 // Put a write protected file where PerfLog wants to write its
217 // file. Make sure that PerfLog tries to shutdown the server
218 // since it can't open its file.
219 Fixture fixture{env_.app(), j_};
220 if (!BEAST_EXPECT(!exists(fixture.logDir())))
221 return;
222
223 // Construct and write protect a file to prevent PerfLog
224 // from creating its file.
225 boost::system::error_code ec;
226 boost::filesystem::create_directories(fixture.logDir(), ec);
227 if (!BEAST_EXPECT(!ec))
228 return;
229
230 auto fileWriteable = [](boost::filesystem::path const& p) -> bool {
231 return std::ofstream{p.c_str(), std::ios::out | std::ios::app}.is_open();
232 };
233
234 if (!BEAST_EXPECT(fileWriteable(fixture.logFile())))
235 return;
236
237 boost::filesystem::permissions(
238 fixture.logFile(), perms::remove_perms | perms::owner_write | perms::others_write | perms::group_write);
239
240 // If the test is running as root, then the write protect may have
241 // no effect. Make sure write protect worked before proceeding.
242 if (fileWriteable(fixture.logFile()))
243 {
244 log << "Unable to write protect file. Test skipped." << std::endl;
245 return;
246 }
247
248 // Now construct a PerfLog. The PerfLog should attempt to shut
249 // down the server because it can't open its file.
250 BEAST_EXPECT(fixture.stopSignaled == false);
251 auto perfLog{fixture.perfLog(WithFile::yes)};
252 BEAST_EXPECT(fixture.stopSignaled == true);
253
254 // Start PerfLog and wait long enough for PerfLog::report()
255 // to not be able to write to its file. That should cause no
256 // problems.
257 perfLog->start();
258 std::this_thread::sleep_for(fixture.logInterval() * 10);
259 perfLog->stop();
260
261 // Fix file permissions so the file can be cleaned up.
262 boost::filesystem::permissions(
263 fixture.logFile(), perms::add_perms | perms::owner_write | perms::others_write | perms::group_write);
264 }
265 }
266
267 void
269 {
270 // Exercise the rpc interfaces of PerfLog.
271 // Start up the PerfLog that we'll use for testing.
272 Fixture fixture{env_.app(), j_};
273 auto perfLog{fixture.perfLog(withFile)};
274 perfLog->start();
275
276 // Get the all the labels we can use for RPC interfaces without
277 // causing an assert.
279 std::shuffle(labels.begin(), labels.end(), default_prng());
280
281 // Get two IDs to associate with each label. Errors tend to happen at
282 // boundaries, so we pick IDs starting from zero and ending at
283 // std::uint64_t>::max().
285 ids.reserve(labels.size() * 2);
287 std::back_inserter(ids), labels.size(), [i = std::numeric_limits<std::uint64_t>::min()]() mutable {
288 return i++;
289 });
291 std::back_inserter(ids), labels.size(), [i = std::numeric_limits<std::uint64_t>::max()]() mutable {
292 return i--;
293 });
294 std::shuffle(ids.begin(), ids.end(), default_prng());
295
296 // Start all of the RPC commands twice to show they can all be tracked
297 // simultaneously.
298 for (int labelIndex = 0; labelIndex < labels.size(); ++labelIndex)
299 {
300 for (int idIndex = 0; idIndex < 2; ++idIndex)
301 {
303 perfLog->rpcStart(labels[labelIndex], ids[(labelIndex * 2) + idIndex]);
304 }
305 }
306 {
307 // Examine current PerfLog::counterJson() values.
308 Json::Value const countersJson{perfLog->countersJson()[jss::rpc]};
309 BEAST_EXPECT(countersJson.size() == labels.size() + 1);
310 for (auto& label : labels)
311 {
312 // Expect every label in labels to have the same contents.
313 Json::Value const& counter{countersJson[label]};
314 BEAST_EXPECT(counter[jss::duration_us] == "0");
315 BEAST_EXPECT(counter[jss::errored] == "0");
316 BEAST_EXPECT(counter[jss::finished] == "0");
317 BEAST_EXPECT(counter[jss::started] == "2");
318 }
319 // Expect "total" to have a lot of "started"
320 Json::Value const& total{countersJson[jss::total]};
321 BEAST_EXPECT(total[jss::duration_us] == "0");
322 BEAST_EXPECT(total[jss::errored] == "0");
323 BEAST_EXPECT(total[jss::finished] == "0");
324 BEAST_EXPECT(jsonToUint64(total[jss::started]) == ids.size());
325 }
326 {
327 // Verify that every entry in labels appears twice in currents.
328 // If we sort by duration_us they should be in the order the
329 // rpcStart() call was made.
330 std::vector<Cur> const currents{getSortedCurrent(perfLog->currentJson()[jss::methods])};
331 BEAST_EXPECT(currents.size() == labels.size() * 2);
332
334 for (int i = 0; i < currents.size(); ++i)
335 {
336 BEAST_EXPECT(currents[i].name == labels[i / 2]);
337 BEAST_EXPECT(prevDur > currents[i].dur);
338 prevDur = currents[i].dur;
339 }
340 }
341
342 // Finish all but the first RPC command in reverse order to show that
343 // the start and finish of the commands can interleave. Half of the
344 // commands finish correctly, the other half with errors.
345 for (int labelIndex = labels.size() - 1; labelIndex > 0; --labelIndex)
346 {
348 perfLog->rpcFinish(labels[labelIndex], ids[(labelIndex * 2) + 1]);
350 perfLog->rpcError(labels[labelIndex], ids[(labelIndex * 2) + 0]);
351 }
352 perfLog->rpcFinish(labels[0], ids[0 + 1]);
353 // Note that label[0] id[0] is intentionally left unfinished.
354
355 auto validateFinalCounters = [this, &labels](Json::Value const& countersJson) {
356 {
357 Json::Value const& jobQueue = countersJson[jss::job_queue];
358 BEAST_EXPECT(jobQueue.isObject());
359 BEAST_EXPECT(jobQueue.size() == 0);
360 }
361
362 Json::Value const& rpc = countersJson[jss::rpc];
363 BEAST_EXPECT(rpc.size() == labels.size() + 1);
364
365 // Verify that every entry in labels appears in rpc.
366 // If we access the entries by label we should be able to correlate
367 // their durations with the appropriate labels.
368 {
369 // The first label is special. It should have "errored" : "0".
370 Json::Value const& first = rpc[labels[0]];
371 BEAST_EXPECT(first[jss::duration_us] != "0");
372 BEAST_EXPECT(first[jss::errored] == "0");
373 BEAST_EXPECT(first[jss::finished] == "1");
374 BEAST_EXPECT(first[jss::started] == "2");
375 }
376
377 // Check the rest of the labels.
379 for (int i = 1; i < labels.size(); ++i)
380 {
381 Json::Value const& counter{rpc[labels[i]]};
382 std::uint64_t const dur{jsonToUint64(counter[jss::duration_us])};
383 BEAST_EXPECT(dur != 0 && dur < prevDur);
384 prevDur = dur;
385 BEAST_EXPECT(counter[jss::errored] == "1");
386 BEAST_EXPECT(counter[jss::finished] == "1");
387 BEAST_EXPECT(counter[jss::started] == "2");
388 }
389
390 // Check "total"
391 Json::Value const& total{rpc[jss::total]};
392 BEAST_EXPECT(total[jss::duration_us] != "0");
393 BEAST_EXPECT(jsonToUint64(total[jss::errored]) == labels.size() - 1);
394 BEAST_EXPECT(jsonToUint64(total[jss::finished]) == labels.size());
395 BEAST_EXPECT(jsonToUint64(total[jss::started]) == labels.size() * 2);
396 };
397
398 auto validateFinalCurrent = [this, &labels](Json::Value const& currentJson) {
399 {
400 Json::Value const& job_queue = currentJson[jss::jobs];
401 BEAST_EXPECT(job_queue.isArray());
402 BEAST_EXPECT(job_queue.size() == 0);
403 }
404
405 Json::Value const& methods = currentJson[jss::methods];
406 BEAST_EXPECT(methods.size() == 1);
407 BEAST_EXPECT(methods.isArray());
408
409 Json::Value const& only = methods[0u];
410 BEAST_EXPECT(only.size() == 2);
411 BEAST_EXPECT(only.isObject());
412 BEAST_EXPECT(only[jss::duration_us] != "0");
413 BEAST_EXPECT(only[jss::method] == labels[0]);
414 };
415
416 // Validate the final state of the PerfLog.
417 validateFinalCounters(perfLog->countersJson());
418 validateFinalCurrent(perfLog->currentJson());
419
420 // Give the PerfLog enough time to flush it's state to the file.
421 fixture.wait();
422
423 // Politely stop the PerfLog.
424 perfLog->stop();
425
426 auto const fullPath = fixture.logFile();
427
428 if (withFile == WithFile::no)
429 {
430 BEAST_EXPECT(!exists(fullPath));
431 }
432 else
433 {
434 // The last line in the log file should contain the same
435 // information that countersJson() and currentJson() returned.
436 // Verify that.
437
438 // Get the last line of the log.
439 std::ifstream logStream(fullPath.c_str());
440 std::string lastLine;
441 for (std::string line; std::getline(logStream, line);)
442 {
443 if (!line.empty())
444 lastLine = std::move(line);
445 }
446
447 Json::Value parsedLastLine;
448 Json::Reader().parse(lastLine, parsedLastLine);
449 if (!BEAST_EXPECT(!RPC::contains_error(parsedLastLine)))
450 // Avoid cascade of failures
451 return;
452
453 // Validate the contents of the last line of the log.
454 validateFinalCounters(parsedLastLine[jss::counters]);
455 validateFinalCurrent(parsedLastLine[jss::current_activities]);
456 }
457 }
458
459 void
461 {
462 using namespace std::chrono;
463
464 // Exercise the jobs interfaces of PerfLog.
465 // Start up the PerfLog that we'll use for testing.
466 Fixture fixture{env_.app(), j_};
467 auto perfLog{fixture.perfLog(withFile)};
468 perfLog->start();
469
470 // Get the all the JobTypes we can use to call the jobs interfaces
471 // without causing an assert.
472 struct JobName
473 {
474 JobType type;
475 std::string typeName;
476
477 JobName(JobType t, std::string name) : type(t), typeName(std::move(name))
478 {
479 }
480 };
481
483 {
484 auto const& jobTypes = JobTypes::instance();
485 jobs.reserve(jobTypes.size());
486 for (auto const& job : jobTypes)
487 {
488 jobs.emplace_back(job.first, job.second.name());
489 }
490 }
491 std::shuffle(jobs.begin(), jobs.end(), default_prng());
492
493 // Walk through all of the jobs, enqueuing every job once. Check
494 // the jobs data with every addition.
495 for (int i = 0; i < jobs.size(); ++i)
496 {
497 perfLog->jobQueue(jobs[i].type);
498 Json::Value const jq_counters{perfLog->countersJson()[jss::job_queue]};
499
500 BEAST_EXPECT(jq_counters.size() == i + 2);
501 for (int j = 0; j <= i; ++j)
502 {
503 // Verify all expected counters are present and contain
504 // expected values.
505 Json::Value const& counter{jq_counters[jobs[j].typeName]};
506 BEAST_EXPECT(counter.size() == 5);
507 BEAST_EXPECT(counter[jss::queued] == "1");
508 BEAST_EXPECT(counter[jss::started] == "0");
509 BEAST_EXPECT(counter[jss::finished] == "0");
510 BEAST_EXPECT(counter[jss::queued_duration_us] == "0");
511 BEAST_EXPECT(counter[jss::running_duration_us] == "0");
512 }
513
514 // Verify jss::total is present and has expected values.
515 Json::Value const& total{jq_counters[jss::total]};
516 BEAST_EXPECT(total.size() == 5);
517 BEAST_EXPECT(jsonToUint64(total[jss::queued]) == i + 1);
518 BEAST_EXPECT(total[jss::started] == "0");
519 BEAST_EXPECT(total[jss::finished] == "0");
520 BEAST_EXPECT(total[jss::queued_duration_us] == "0");
521 BEAST_EXPECT(total[jss::running_duration_us] == "0");
522 }
523
524 // Even with jobs queued, the perfLog should report nothing current.
525 {
526 Json::Value current{perfLog->currentJson()};
527 BEAST_EXPECT(current.size() == 2);
528 BEAST_EXPECT(current.isMember(jss::jobs));
529 BEAST_EXPECT(current[jss::jobs].size() == 0);
530 BEAST_EXPECT(current.isMember(jss::methods));
531 BEAST_EXPECT(current[jss::methods].size() == 0);
532 }
533
534 // Current jobs are tracked by Worker ID. Even though it's not
535 // realistic, crank up the number of workers so we can have many
536 // jobs in process simultaneously without problems.
537 perfLog->resizeJobs(jobs.size() * 2);
538
539 // Start two instances of every job to show that the same job can run
540 // simultaneously (on different Worker threads). Admittedly, this
541 // will make the jss::queued count look a bit goofy since there will
542 // be half as many queued as started...
543 for (int i = 0; i < jobs.size(); ++i)
544 {
545 perfLog->jobStart(jobs[i].type, microseconds{i + 1}, steady_clock::now(), i * 2);
547
548 // Check each jobType counter entry.
549 Json::Value const jq_counters{perfLog->countersJson()[jss::job_queue]};
550 for (int j = 0; j < jobs.size(); ++j)
551 {
552 Json::Value const& counter{jq_counters[jobs[j].typeName]};
553 std::uint64_t const queued_dur_us{jsonToUint64(counter[jss::queued_duration_us])};
554 if (j < i)
555 {
556 BEAST_EXPECT(counter[jss::started] == "2");
557 BEAST_EXPECT(queued_dur_us == j + 1);
558 }
559 else if (j == i)
560 {
561 BEAST_EXPECT(counter[jss::started] == "1");
562 BEAST_EXPECT(queued_dur_us == j + 1);
563 }
564 else
565 {
566 BEAST_EXPECT(counter[jss::started] == "0");
567 BEAST_EXPECT(queued_dur_us == 0);
568 }
569
570 BEAST_EXPECT(counter[jss::queued] == "1");
571 BEAST_EXPECT(counter[jss::finished] == "0");
572 BEAST_EXPECT(counter[jss::running_duration_us] == "0");
573 }
574 {
575 // Verify values in jss::total are what we expect.
576 Json::Value const& total{jq_counters[jss::total]};
577 BEAST_EXPECT(jsonToUint64(total[jss::queued]) == jobs.size());
578 BEAST_EXPECT(jsonToUint64(total[jss::started]) == (i * 2) + 1);
579 BEAST_EXPECT(total[jss::finished] == "0");
580
581 // Total queued duration is triangle number of (i + 1).
582 BEAST_EXPECT(jsonToUint64(total[jss::queued_duration_us]) == (((i * i) + 3 * i + 2) / 2));
583 BEAST_EXPECT(total[jss::running_duration_us] == "0");
584 }
585
586 perfLog->jobStart(jobs[i].type, microseconds{0}, steady_clock::now(), (i * 2) + 1);
588
589 // Verify that every entry in jobs appears twice in currents.
590 // If we sort by duration_us they should be in the order the
591 // rpcStart() call was made.
592 std::vector<Cur> const currents{getSortedCurrent(perfLog->currentJson()[jss::jobs])};
593 BEAST_EXPECT(currents.size() == (i + 1) * 2);
594
596 for (int j = 0; j <= i; ++j)
597 {
598 BEAST_EXPECT(currents[j * 2].name == jobs[j].typeName);
599 BEAST_EXPECT(prevDur > currents[j * 2].dur);
600 prevDur = currents[j * 2].dur;
601
602 BEAST_EXPECT(currents[(j * 2) + 1].name == jobs[j].typeName);
603 BEAST_EXPECT(prevDur > currents[(j * 2) + 1].dur);
604 prevDur = currents[(j * 2) + 1].dur;
605 }
606 }
607
608 // Finish every job we started. Finish them in reverse.
609 for (int i = jobs.size() - 1; i >= 0; --i)
610 {
611 // A number of the computations in this loop care about the
612 // number of jobs that have finished. Make that available.
613 int const finished = ((jobs.size() - i) * 2) - 1;
614 perfLog->jobFinish(jobs[i].type, microseconds(finished), (i * 2) + 1);
616
617 Json::Value const jq_counters{perfLog->countersJson()[jss::job_queue]};
618 for (int j = 0; j < jobs.size(); ++j)
619 {
620 Json::Value const& counter{jq_counters[jobs[j].typeName]};
621 std::uint64_t const running_dur_us{jsonToUint64(counter[jss::running_duration_us])};
622 if (j < i)
623 {
624 BEAST_EXPECT(counter[jss::finished] == "0");
625 BEAST_EXPECT(running_dur_us == 0);
626 }
627 else if (j == i)
628 {
629 BEAST_EXPECT(counter[jss::finished] == "1");
630 BEAST_EXPECT(running_dur_us == ((jobs.size() - j) * 2) - 1);
631 }
632 else
633 {
634 BEAST_EXPECT(counter[jss::finished] == "2");
635 BEAST_EXPECT(running_dur_us == ((jobs.size() - j) * 4) - 1);
636 }
637
638 std::uint64_t const queued_dur_us{jsonToUint64(counter[jss::queued_duration_us])};
639 BEAST_EXPECT(queued_dur_us == j + 1);
640 BEAST_EXPECT(counter[jss::queued] == "1");
641 BEAST_EXPECT(counter[jss::started] == "2");
642 }
643 {
644 // Verify values in jss::total are what we expect.
645 Json::Value const& total{jq_counters[jss::total]};
646 BEAST_EXPECT(jsonToUint64(total[jss::queued]) == jobs.size());
647 BEAST_EXPECT(jsonToUint64(total[jss::started]) == jobs.size() * 2);
648 BEAST_EXPECT(jsonToUint64(total[jss::finished]) == finished);
649
650 // Total queued duration should be triangle number of
651 // jobs.size().
652 int const queuedDur = ((jobs.size() * (jobs.size() + 1)) / 2);
653 BEAST_EXPECT(jsonToUint64(total[jss::queued_duration_us]) == queuedDur);
654
655 // Total running duration should be triangle number of finished.
656 int const runningDur = ((finished * (finished + 1)) / 2);
657 BEAST_EXPECT(jsonToUint64(total[jss::running_duration_us]) == runningDur);
658 }
659
660 perfLog->jobFinish(jobs[i].type, microseconds(finished + 1), (i * 2));
662
663 // Verify that the two jobs we just finished no longer appear in
664 // currents.
665 std::vector<Cur> const currents{getSortedCurrent(perfLog->currentJson()[jss::jobs])};
666 BEAST_EXPECT(currents.size() == i * 2);
667
669 for (int j = 0; j < i; ++j)
670 {
671 BEAST_EXPECT(currents[j * 2].name == jobs[j].typeName);
672 BEAST_EXPECT(prevDur > currents[j * 2].dur);
673 prevDur = currents[j * 2].dur;
674
675 BEAST_EXPECT(currents[(j * 2) + 1].name == jobs[j].typeName);
676 BEAST_EXPECT(prevDur > currents[(j * 2) + 1].dur);
677 prevDur = currents[(j * 2) + 1].dur;
678 }
679 }
680
681 // Validate the final results.
682 auto validateFinalCounters = [this, &jobs](Json::Value const& countersJson) {
683 {
684 Json::Value const& rpc = countersJson[jss::rpc];
685 BEAST_EXPECT(rpc.isObject());
686 BEAST_EXPECT(rpc.size() == 0);
687 }
688
689 Json::Value const& jobQueue = countersJson[jss::job_queue];
690 for (int i = jobs.size() - 1; i >= 0; --i)
691 {
692 Json::Value const& counter{jobQueue[jobs[i].typeName]};
693 std::uint64_t const running_dur_us{jsonToUint64(counter[jss::running_duration_us])};
694 BEAST_EXPECT(running_dur_us == ((jobs.size() - i) * 4) - 1);
695
696 std::uint64_t const queued_dur_us{jsonToUint64(counter[jss::queued_duration_us])};
697 BEAST_EXPECT(queued_dur_us == i + 1);
698
699 BEAST_EXPECT(counter[jss::queued] == "1");
700 BEAST_EXPECT(counter[jss::started] == "2");
701 BEAST_EXPECT(counter[jss::finished] == "2");
702 }
703
704 // Verify values in jss::total are what we expect.
705 Json::Value const& total{jobQueue[jss::total]};
706 int const finished = jobs.size() * 2;
707 BEAST_EXPECT(jsonToUint64(total[jss::queued]) == jobs.size());
708 BEAST_EXPECT(jsonToUint64(total[jss::started]) == finished);
709 BEAST_EXPECT(jsonToUint64(total[jss::finished]) == finished);
710
711 // Total queued duration should be triangle number of
712 // jobs.size().
713 int const queuedDur = ((jobs.size() * (jobs.size() + 1)) / 2);
714 BEAST_EXPECT(jsonToUint64(total[jss::queued_duration_us]) == queuedDur);
715
716 // Total running duration should be triangle number of finished.
717 int const runningDur = ((finished * (finished + 1)) / 2);
718 BEAST_EXPECT(jsonToUint64(total[jss::running_duration_us]) == runningDur);
719 };
720
721 auto validateFinalCurrent = [this](Json::Value const& currentJson) {
722 {
723 Json::Value const& j = currentJson[jss::jobs];
724 BEAST_EXPECT(j.isArray());
725 BEAST_EXPECT(j.size() == 0);
726 }
727
728 Json::Value const& methods = currentJson[jss::methods];
729 BEAST_EXPECT(methods.size() == 0);
730 BEAST_EXPECT(methods.isArray());
731 };
732
733 // Validate the final state of the PerfLog.
734 validateFinalCounters(perfLog->countersJson());
735 validateFinalCurrent(perfLog->currentJson());
736
737 // Give the PerfLog enough time to flush it's state to the file.
738 fixture.wait();
739
740 // Politely stop the PerfLog.
741 perfLog->stop();
742
743 // Check file contents if that is appropriate.
744 auto const fullPath = fixture.logFile();
745
746 if (withFile == WithFile::no)
747 {
748 BEAST_EXPECT(!exists(fullPath));
749 }
750 else
751 {
752 // The last line in the log file should contain the same
753 // information that countersJson() and currentJson() returned.
754 // Verify that.
755
756 // Get the last line of the log.
757 std::ifstream logStream(fullPath.c_str());
758 std::string lastLine;
759 for (std::string line; std::getline(logStream, line);)
760 {
761 if (!line.empty())
762 lastLine = std::move(line);
763 }
764
765 Json::Value parsedLastLine;
766 Json::Reader().parse(lastLine, parsedLastLine);
767 if (!BEAST_EXPECT(!RPC::contains_error(parsedLastLine)))
768 // Avoid cascade of failures
769 return;
770
771 // Validate the contents of the last line of the log.
772 validateFinalCounters(parsedLastLine[jss::counters]);
773 validateFinalCurrent(parsedLastLine[jss::current_activities]);
774 }
775 }
776
777 void
779 {
780 using namespace std::chrono;
781
782 // The Worker ID is used to identify jobs in progress. Show that
783 // the PerLog behaves as well as possible if an invalid ID is passed.
784
785 // Start up the PerfLog that we'll use for testing.
786 Fixture fixture{env_.app(), j_};
787 auto perfLog{fixture.perfLog(withFile)};
788 perfLog->start();
789
790 // Randomly select a job type and its name.
791 JobType jobType;
792 std::string jobTypeName;
793 {
794 auto const& jobTypes = JobTypes::instance();
795
796 std::uniform_int_distribution<> dis(0, jobTypes.size() - 1);
797 auto iter{jobTypes.begin()};
798 std::advance(iter, dis(default_prng()));
799
800 jobType = iter->second.type();
801 jobTypeName = iter->second.name();
802 }
803
804 // Say there's one worker thread.
805 perfLog->resizeJobs(1);
806
807 // Lambda to validate countersJson for this test.
808 auto verifyCounters =
809 [this, jobTypeName](
810 Json::Value const& countersJson, int started, int finished, int queued_us, int running_us) {
811 BEAST_EXPECT(countersJson.isObject());
812 BEAST_EXPECT(countersJson.size() == 2);
813
814 BEAST_EXPECT(countersJson.isMember(jss::rpc));
815 BEAST_EXPECT(countersJson[jss::rpc].isObject());
816 BEAST_EXPECT(countersJson[jss::rpc].size() == 0);
817
818 BEAST_EXPECT(countersJson.isMember(jss::job_queue));
819 BEAST_EXPECT(countersJson[jss::job_queue].isObject());
820 BEAST_EXPECT(countersJson[jss::job_queue].size() == 1);
821 {
822 Json::Value const& job{countersJson[jss::job_queue][jobTypeName]};
823
824 BEAST_EXPECT(job.isObject());
825 BEAST_EXPECT(jsonToUint64(job[jss::queued]) == 0);
826 BEAST_EXPECT(jsonToUint64(job[jss::started]) == started);
827 BEAST_EXPECT(jsonToUint64(job[jss::finished]) == finished);
828
829 BEAST_EXPECT(jsonToUint64(job[jss::queued_duration_us]) == queued_us);
830 BEAST_EXPECT(jsonToUint64(job[jss::running_duration_us]) == running_us);
831 }
832 };
833
834 // Lambda to validate currentJson (always empty) for this test.
835 auto verifyEmptyCurrent = [this](Json::Value const& currentJson) {
836 BEAST_EXPECT(currentJson.isObject());
837 BEAST_EXPECT(currentJson.size() == 2);
838
839 BEAST_EXPECT(currentJson.isMember(jss::jobs));
840 BEAST_EXPECT(currentJson[jss::jobs].isArray());
841 BEAST_EXPECT(currentJson[jss::jobs].size() == 0);
842
843 BEAST_EXPECT(currentJson.isMember(jss::methods));
844 BEAST_EXPECT(currentJson[jss::methods].isArray());
845 BEAST_EXPECT(currentJson[jss::methods].size() == 0);
846 };
847
848 // Start an ID that's too large.
849 perfLog->jobStart(jobType, microseconds{11}, steady_clock::now(), 2);
851 verifyCounters(perfLog->countersJson(), 1, 0, 11, 0);
852 verifyEmptyCurrent(perfLog->currentJson());
853
854 // Start a negative ID
855 perfLog->jobStart(jobType, microseconds{13}, steady_clock::now(), -1);
857 verifyCounters(perfLog->countersJson(), 2, 0, 24, 0);
858 verifyEmptyCurrent(perfLog->currentJson());
859
860 // Finish the too large ID
861 perfLog->jobFinish(jobType, microseconds{17}, 2);
863 verifyCounters(perfLog->countersJson(), 2, 1, 24, 17);
864 verifyEmptyCurrent(perfLog->currentJson());
865
866 // Finish the negative ID
867 perfLog->jobFinish(jobType, microseconds{19}, -1);
869 verifyCounters(perfLog->countersJson(), 2, 2, 24, 36);
870 verifyEmptyCurrent(perfLog->currentJson());
871
872 // Give the PerfLog enough time to flush it's state to the file.
873 fixture.wait();
874
875 // Politely stop the PerfLog.
876 perfLog->stop();
877
878 // Check file contents if that is appropriate.
879 auto const fullPath = fixture.logFile();
880
881 if (withFile == WithFile::no)
882 {
883 BEAST_EXPECT(!exists(fullPath));
884 }
885 else
886 {
887 // The last line in the log file should contain the same
888 // information that countersJson() and currentJson() returned.
889 // Verify that.
890
891 // Get the last line of the log.
892 std::ifstream logStream(fullPath.c_str());
893 std::string lastLine;
894 for (std::string line; std::getline(logStream, line);)
895 {
896 if (!line.empty())
897 lastLine = std::move(line);
898 }
899
900 Json::Value parsedLastLine;
901 Json::Reader().parse(lastLine, parsedLastLine);
902 if (!BEAST_EXPECT(!RPC::contains_error(parsedLastLine)))
903 // Avoid cascade of failures
904 return;
905
906 // Validate the contents of the last line of the log.
907 verifyCounters(parsedLastLine[jss::counters], 2, 2, 24, 36);
908 verifyEmptyCurrent(parsedLastLine[jss::current_activities]);
909 }
910 }
911
912 void
914 {
915 // We can't fully test rotate because unit tests must run on Windows,
916 // and Windows doesn't (may not?) support rotate. But at least call
917 // the interface and see that it doesn't crash.
918 using namespace boost::filesystem;
919
920 Fixture fixture{env_.app(), j_};
921 BEAST_EXPECT(!exists(fixture.logDir()));
922
923 auto perfLog{fixture.perfLog(withFile)};
924
925 BEAST_EXPECT(fixture.stopSignaled == false);
926 if (withFile == WithFile::no)
927 {
928 BEAST_EXPECT(!exists(fixture.logDir()));
929 }
930 else
931 {
932 BEAST_EXPECT(exists(fixture.logFile()));
933 BEAST_EXPECT(file_size(fixture.logFile()) == 0);
934 }
935
936 // Start PerfLog and wait long enough for PerfLog::report()
937 // to write to its file.
938 perfLog->start();
939 fixture.wait();
940
941 decltype(file_size(fixture.logFile())) firstFileSize{0};
942 if (withFile == WithFile::no)
943 {
944 BEAST_EXPECT(!exists(fixture.logDir()));
945 }
946 else
947 {
948 firstFileSize = file_size(fixture.logFile());
949 BEAST_EXPECT(firstFileSize > 0);
950 }
951
952 // Rotate and then wait to make sure more stuff is written to the file.
953 perfLog->rotate();
954 fixture.wait();
955
956 perfLog->stop();
957
958 if (withFile == WithFile::no)
959 {
960 BEAST_EXPECT(!exists(fixture.logDir()));
961 }
962 else
963 {
964 BEAST_EXPECT(file_size(fixture.logFile()) > firstFileSize);
965 }
966 }
967
968 void
981};
982
983BEAST_DEFINE_TESTSUITE(PerfLog, basics, xrpl);
984
985} // namespace xrpl
T advance(T... args)
T back_inserter(T... args)
T begin(T... args)
Unserialize a JSON document into a Value.
Definition json_reader.h:17
bool parse(std::string const &document, Value &root)
Read a Value from a JSON document.
Represents a JSON value.
Definition json_value.h:130
bool isArray() const
UInt size() const
Number of values in array or object.
bool isObject() const
std::string asString() const
Returns the unquoted string value.
bool isMember(char const *key) const
Return true if the object has a member named key.
A generic endpoint for log messages.
Definition Journal.h:40
A testsuite class.
Definition suite.h:51
log_os< char > log
Logging output stream.
Definition suite.h:144
virtual beast::Journal journal(std::string const &name)=0
static JobTypes const & instance()
Definition JobTypes.h:97
static std::uint64_t jsonToUint64(Json::Value const &jsonUintAsString)
void testJobs(WithFile withFile)
void testRPC(WithFile withFile)
test::jtx::Env env_
void testRotate(WithFile withFile)
static std::vector< Cur > getSortedCurrent(Json::Value const &currentJson)
boost::filesystem::path path
void run() override
Runs the suite.
void testInvalidID(WithFile withFile)
beast::Journal j_
A transaction testing environment.
Definition Env.h:119
Application & app()
Definition Env.h:251
T close(T... args)
T emplace_back(T... args)
T end(T... args)
T endl(T... args)
T generate_n(T... args)
T getline(T... args)
T is_open(T... args)
T max(T... args)
STL namespace.
std::set< char const * > getHandlerNames()
Return names of all methods.
Definition Handler.cpp:242
bool contains_error(Json::Value const &json)
Returns true if the json contains an rpc error specification.
std::unique_ptr< PerfLog > make_PerfLog(PerfLog::Setup const &setup, Application &app, beast::Journal journal, std::function< void()> &&signalStop)
auto make_vector(Input const &input)
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:34
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ current
This was a new validation and was added.
JobType
Definition Job.h:14
beast::xor_shift_engine & default_prng()
Return the default random engine.
T open(T... args)
T shuffle(T... args)
T reserve(T... args)
T size(T... args)
T sleep_for(T... args)
T sort(T... args)
T stoull(T... args)
Cur(std::uint64_t d, std::string n)
std::unique_ptr< perf::PerfLog > perfLog(WithFile withFile)
std::chrono::milliseconds logInterval() const
Fixture(Application &app, beast::Journal j)
Configuration from [perf] section of xrpld.cfg.
Definition PerfLog.h:45