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