feat(telemetry): expose the sweep-trim and rotation costs (WP-B5)

Two suspects from the 3.3.0 slowdown investigation had no signal. Both were
already computing the numbers and throwing them away, so this exposes them
rather than adding measurement.

Per-sweep heap trim. The trim runs after every cache sweep, and its cost
scales with resident heap, so it is the leading explanation for a node with
a populated database syncing slower than a fresh one. The report already
carried duration, fault deltas and reclaimed pages, but the whole
measurement sat behind a debug-journal check, so an ordinary node measured
nothing, and the call site discarded the result. The measurement now always
runs and only the log line stays gated. Records trim duration, minor faults
and reclaimed kilobytes. Measured cost of the always-on path is about six
microseconds per sweep against a trim costing milliseconds, at a cadence of
ten to a hundred and twenty seconds.

Honest limit, stated in the runbook: the fault delta spans only the trim
call, so it shows the trim itself faulting but not the faults that follow as
caches refill. The duration is the signal to correlate against sweep-job
queueing.

Rotation writes. Rotation copies archive-served reads forward and re-stores
nodes missing from both backends, both of which compete with sync I/O and
only happen on a populated online_delete database. The copy-forward count
existed but was reset by the rotation's own log line, so a metric reading it
would drop to zero on every swap; a never-reset total sits beside it now.
The re-store count was not measured at all. Rotation duration is
deliberately not recorded: the health throttle sleeps at eight points inside
the sequence and dominates exactly when the node is unhealthy, so the number
would conflate work with waiting.

Nothing added for the other two suspects. Get-object serving is already
covered by the handler label, the lookup histogram and the deferred and
saturation gauges; peer churn by the disconnect-reason counter.

Also replaces nine per-file cspell ignores with one ignoreRegExpList entry
for the telemetry macro names, and picks up the levelization baseline for the
consensus span-name test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pratik Mankawde
2026-07-27 16:39:09 +01:00
parent 371f10934e
commit 8633df7a3e
25 changed files with 1402 additions and 111 deletions

View File

@@ -8,7 +8,10 @@
namespace xrpl {
// cSpell:ignore ptmalloc
// cSpell:ignore ptmalloc statm
// "statm" is the /proc/self/statm filename the RSS readings come from; it is a
// kernel path, not prose, so it cannot be respelled. Same directive as the two
// MallocTrim .cpp files.
// -----------------------------------------------------------------------------
// Allocator interaction note:
@@ -66,6 +69,19 @@ struct MallocTrimReport
*
* @note Intended for use after operations that free significant memory (e.g.,
* cache sweeps, ledger cleanup, online delete). Consider rate limiting.
*
* @note Every report field is populated on every call, whatever the journal's
* severity. Only the diagnostic JLOG is severity-gated. Measuring costs
* about 6 us (two /proc/self/statm reads and two getrusage calls) against
* a trim that costs milliseconds on a large heap, so callers on a cold
* path -- the cache sweep is the intended one -- can record the numbers
* unconditionally. A caller on a hot path should not use this function at
* all rather than expect the measurement to disappear.
*
* @note `minfltDelta` covers the trim call ONLY. It shows that the trim itself
* faults; it does NOT capture the faults later taken when the caches
* refill and touch the pages the trim returned. Do not read it as the
* total cost of trimming.
*/
MallocTrimReport
mallocTrim(std::string_view tag, beast::Journal journal);

View File

@@ -5,6 +5,7 @@
#include <xrpl/nodestore/Database.h>
#include <xrpl/nodestore/Scheduler.h>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
@@ -54,6 +55,38 @@ public:
*/
virtual void
setRotationInFlight(bool inFlight) = 0;
/**
* Whether an online-delete rotation is in progress right now.
*
* A rotation's extra writes only happen inside this window, so a panel
* reading the copy-forward total needs this to know when to expect it to
* move. Outside the window a flat total is correct, not a broken signal.
*
* @return true between the cache-freshen phase starting and rotate()
* completing.
*/
[[nodiscard]] virtual bool
isRotationInFlight() const = 0;
/**
* Nodes copied forward from the archive backend into the writable one
* during rotation windows, since this process started.
*
* These are writes an ordinary fetch would not have performed: the archive
* is about to be deleted, so a body it served has to be rewritten to
* survive. The count therefore scales with how much of the archive is read
* during a rotation, which is why it appears only on a populated,
* already-rotated online_delete database.
*
* Cumulative for the lifetime of the process, deliberately: the per-rotation
* tally that `rotate()` logs is reset on every swap, and a counter that goes
* backwards cannot be rated. A panel takes the rate of this instead.
*
* @return Monotonic count of copy-forward writes.
*/
[[nodiscard]] virtual std::uint64_t
copyForwardTotal() const = 0;
};
} // namespace xrpl::NodeStore

View File

@@ -73,6 +73,18 @@ public:
void
setRotationInFlight(bool inFlight) override;
[[nodiscard]] bool
isRotationInFlight() const override
{
return rotationInFlight_.load(std::memory_order_acquire);
}
[[nodiscard]] std::uint64_t
copyForwardTotal() const override
{
return copyForwardTotal_.load(std::memory_order_relaxed);
}
private:
std::shared_ptr<Backend> writableBackend_;
std::shared_ptr<Backend> archiveBackend_;
@@ -86,6 +98,13 @@ private:
std::atomic<bool> rotationInFlight_{false};
std::atomic<std::uint64_t> copyForwardCount_{0};
// Same events as copyForwardCount_, but never reset. rotate() zeroes the
// per-rotation tally above to produce its one-line summary, which makes that
// counter unusable as a metric: a series that drops to 0 on every swap
// cannot be rated. This one only ever increases, so rate() over it reads
// correctly and the two uses do not fight over one variable.
std::atomic<std::uint64_t> copyForwardTotal_{0};
std::shared_ptr<NodeObject>
fetchNodeObject(uint256 const& hash, std::uint32_t, FetchReport& fetchReport, bool duplicate)
override;

View File

@@ -1,9 +1,5 @@
#pragma once
// cspell:ignore ISTOGRAM
// The all-caps macro name XRPL_METRIC_HISTOGRAM_RECORD trips cspell's
// compound-word splitter, which emits the subword "ISTOGRAM"; ignore it here.
/**
* Metric names, label keys, label values, and descriptions for the
* `TMGetObjectByHash` request path.