Files
rippled/src/xrpld/app/main/NodeStoreScheduler.cpp
Pratik Mankawde 51f6544f7b feat(telemetry): record nodestore read latency as a histogram
kSubMillisecondBoundaries existed but nothing used it, so per-fetch read
latency never reached Grafana -- only the coarse read_mean_us gauge did,
which cannot separate "every read took 9us" from "most took 2 and a few
took 900".

Add a nodestore_read_us histogram, register its view against the sub-
millisecond ladder rather than kMicrosecondBoundaries (whose first edge
is 100us, above the entire range a warm read occupies), and record into
it from NodeStoreScheduler::onFetch using FetchReport::elapsed, which a
previous change widened to microseconds for exactly this purpose.

The name and its labels live in a new include/xrpl/telemetry header
because the view registration (xrpld.telemetry) and the record site
(xrpld.app) sit in different levelization modules; a copy-pasted literal
would let them drift and silently drop the bucket override. Same reason
and same placement as GetObjectMetricNames.h. No new levelization edge:
xrpld.app > xrpl.telemetry already exists.

NodeStoreScheduler had no registry access, so it now takes a
ServiceRegistry and resolves the registry per call. It is constructed in
Application's initializer list, long before metricsRegistry_ is assigned
in setup() and started in startTelemetry(), so capturing a pointer at
construction would capture nullptr forever; the metric macros null-check
the registry, the meter and the instrument, so early fetches are simply
not recorded.

Labels are fetch_type and found, both already carried on the report --
4 series, fixed at compile time. A slow async read delays prefetch while
a slow sync read blocks a caller, and a miss can cost a read of every
backend, so neither dimension can be collapsed.

Negative elapsed times are skipped: the SDK rejects them and logs a
warning on every call, which on a per-fetch path is a log flood. Zero is
still recorded, since a page-cache-served read genuinely rounds to it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 12:04:37 +01:00

92 lines
2.9 KiB
C++

// cspell:ignore ISTOGRAM
// The all-caps macro name XRPL_METRIC_HISTOGRAM_RECORD_LABELED trips cspell's
// compound-word splitter, which emits the subword "ISTOGRAM"; ignore it here.
#include <xrpld/app/main/NodeStoreScheduler.h>
#include <xrpld/telemetry/MetricMacros.h>
#include <xrpl/core/Job.h>
#include <xrpl/core/JobQueue.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/nodestore/Scheduler.h>
#include <xrpl/nodestore/Task.h>
#include <xrpl/telemetry/NodeStoreMetricNames.h>
#include <chrono>
#include <string>
namespace xrpl {
NodeStoreScheduler::NodeStoreScheduler([[maybe_unused]] ServiceRegistry& app, JobQueue& jobQueue)
#ifdef XRPL_ENABLE_TELEMETRY
: app_(app), jobQueue_(jobQueue)
#else
: jobQueue_(jobQueue)
#endif
{
}
void
NodeStoreScheduler::scheduleTask(node_store::Task& task)
{
if (jobQueue_.isStopped())
return;
if (!jobQueue_.addJob(JtWrite, "NObjStore", [&task]() { task.performScheduledTask(); }))
{
// Job not added, presumably because we're shutting down.
// Recover by executing the task synchronously.
task.performScheduledTask();
}
}
void
NodeStoreScheduler::onFetch(node_store::FetchReport const& report)
{
if (jobQueue_.isStopped())
return;
auto const isAsync = report.fetchType == node_store::FetchType::Async;
// The report is in microseconds but addLoadEvents takes milliseconds, so
// cast explicitly. The load monitor only tracks whole-millisecond load,
// so the sub-millisecond detail is deliberately dropped here; the
// histogram below keeps it.
jobQueue_.addLoadEvents(
isAsync ? JtNsAsyncRead : JtNsSyncRead,
1,
std::chrono::duration_cast<std::chrono::milliseconds>(report.elapsed));
// Skip a negative elapsed time rather than hand it to the SDK, which
// rejects it and logs a warning on every single call. The clock is
// monotonic, so this needs a clock bug to happen -- but a per-fetch log
// flood would be worse than the missing sample.
if (!telemetry::shouldRecordFetchLatency(report.elapsed.count()))
return;
// Two labels, both already on the report. fetch_type because a slow async
// read only delays prefetch while a slow sync read blocks a caller;
// found because a miss can cost a read of every backend, so mixing the
// two blurs the distribution.
XRPL_METRIC_HISTOGRAM_RECORD_LABELED(
app_,
telemetry::kNodeStoreReadUs,
telemetry::kNodeStoreReadUsDesc,
report.elapsed.count(),
{{telemetry::kFetchTypeLabel, std::string(telemetry::fetchTypeLabelValue(isAsync))},
{telemetry::kFetchFoundLabel,
std::string(telemetry::fetchFoundLabelValue(report.wasFound))}});
}
void
NodeStoreScheduler::onBatchWrite(node_store::BatchWriteReport const& report)
{
if (jobQueue_.isStopped())
return;
jobQueue_.addLoadEvents(JtNsWrite, report.writeCount, report.elapsed);
}
} // namespace xrpl