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