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