Commit Graph

14 Commits

Author SHA1 Message Date
Pratik Mankawde
85fc1c5399 fix(telemetry): keep the workload cluster on [ips], not [ips_fixed]
The previous commit switched the generated node config from [ips] to
[ips_fixed] on the grounds that the variable, the comment and the sibling cfg
template all named ips_fixed, and that ips_fixed is the section whose
documented meaning fits a private cluster. Both of those are still true. The
switch is reverted anyway, because it is a workload change rather than a
naming fix.

Measured on CI, parent commit against this branch's previous tip, one
functional config line apart:

  span.consensus.ledger_close.p95    0.57 ms -> 6.43 ms   (tripped the gate)
  span.consensus.ledger_close.p99    0.94 ms -> 9.50 ms
  span.consensus.accept.p50          0.97 ms -> 2.63 ms
  span.tx.process.p50                0.36 ms -> 0.18 ms   (faster)
  job.acceptLedger.running.p95      21157 us -> 10938 us  (faster)

Every consensus-path span rose and every transaction-path metric fell, which
is the shape a denser always-connected mesh produces and not the shape of
run-to-run variance. [ips_fixed] holds connections open to all four peers
instead of treating the list as a discovery hint, so each node processes
proposals and validations from the full mesh every round. Nothing else in that
commit touches the consensus path: the emitted config differed in exactly
three lines, of which one is a die message and one expands to an identical
string.

The committed baseline describes the [ips] topology. Adopting [ips_fixed]
therefore needs a refreshed baseline and re-derived bounds, which is the
process baselines/README.md already documents for a workload change. Left as
its own work item rather than smuggled in behind a section rename, and the
reason is now recorded beside the line so it is not repeated.

This also falsified a claim the previous commit had written into
baselines/README.md and regression-thresholds.json: that none of the six
weakly-guarded keys fires on any observed run. Corrected in both, and the
measurement above is cited in place of the absolute.
2026-09-17 17:01:29 +01:00
Pratik Mankawde
ffc4ee40fe fix(telemetry): correct node directory naming and stale gates in the workload harness
The harness killed and probed node directories named `node<N>`, but the
directories it creates are `validator-<N>` in run-full-validation.sh and
`bench-node-<N>` in benchmark.sh. Verified with pgrep against processes whose
command lines mimic the real ones: the pattern matched nothing either script
produces. Three consequences, all live:

  - `--cleanup` deleted the workdir and left the xrpld processes running. They
    are host processes, so the compose teardown does not reach them.
  - The pre-run cleanup could not free the previous run's RPC, WS and peer
    ports, which surfaces much later as a cluster that never reaches consensus.
  - The startup crash fast-fail read a pid path that never exists, so its
    `stopped > 0` branch was unreachable and a dead node waited out the full
    120-attempt window.

Rather than patch four literals, derive every node path, kill pattern and log
glob from one NODE_PREFIX per script. The directory name is also the node's
identity: the collector's file_log receiver lifts that segment into
service.instance.id, so the directory and the [telemetry] service_instance_id
must agree. Deriving both from one value is what stops them drifting again.

Also in the same files, each confirmed by test rather than inspection:

  - The collector readiness probe could never fail. curl -w '%{http_code}'
    prints 000 on a refused connection and then exits non-zero, so the
    `|| echo 000` inside the substitution appended a second 000 and the
    "not ready" comparison never matched. Move the fallback outside.
  - The generated config wrote [ips], the starter-list section. A loopback mesh
    that must reach quorum is the [ips_fixed] case, which is what the variable,
    the comment and the sibling cfg template already said.
  - benchmark.sh returned exit 1 for a row it could not measure, though the
    exit-code table reserves 1 for "every metric was measured and one breached".
    Report 2 there instead.
  - Five bc computations fell back to 0, which clears every threshold. The
    guards beside them already fall back to the inconclusive token; these now
    do too.
  - A comment claimed a `|| guard` after a heredoc lands in the heredoc, and
    that claim had removed a real guard from the config write. It does not: the
    guard runs, and fires when cat fails.
  - The EXIT trap was installed 88 lines before stop_workload was defined. If it
    fired in that window, errexit aborted the handler on "command not found" and
    the cluster reap never ran. Install it below both handlers.
  - jq exits 5 on malformed JSON, outside this script's documented codes, so
    read_metric now routes that through cannot_measure.
  - --nodes and --duration were unvalidated, and --nodes 0 made the pid-count
    guard compare 0 with 0 and pass, handing the sampler no pids at all.
  - --cleanup now passes -v so the named tempo-data volume goes with it.
    Otherwise the next run's Tempo still serves the previous run's traces and a
    span assertion can be satisfied by them.
  - Five messages reported an attempt count as seconds, though each attempt is
    a sleep plus every node's probe.

The baselines README and the two regression JSON files had gone stale when the
baseline was refreshed to a three-run median: they described 20 gated keys and
five exclusions, against an actual 19 and six, and cited the superseded run,
date and commit. Re-derive every affected figure from the committed files. The
detection floors are recomputed (2.00x to 7.41x, so a 10x regression is now
caught on all 19 keys), the newly excluded span.ledger.build.p99 is documented,
and figures that no committed artifact can verify are either replaced with
derivable ones or labelled with their numerator.

No baseline value, threshold bound or derivation entry changes.
2026-09-17 15:07:15 +01:00
Pratik Mankawde
00c0265cc6 test(telemetry): refresh the timing baseline from three clean runs
The committed baseline was captured 2026-08-26, before the account-funding
race was detectable. Phases whose funding silently failed submitted no
transactions, so the capture recorded artificially low ledger and transaction
timings, and job.transaction.queued.p95 and job.transaction.running.p95 could
not be captured at all. Once funding worked, span.ledger.build.p99 read
29.00 ms against a 9.11 ms baseline and turned the gate red on a run whose
200 span and metric checks all passed.

Refresh every value to the median of CI runs 34495527952, 34505215266 and
34507425933, the first three with the fix in place, and re-derive each
absolute bound as hi_next - baseline from that median.

Exclude span.ledger.build.p99. Across those three runs it read 29.00, 7.06
and 8.94 ms, a 4.11x spread whose maximum is 1.16x its 25 ms trip point, so a
healthy run reddens CI. Widening cannot fix it: a bound tolerating 29.00 ms
would reach into the bucket above and restore the single-crossing false
positive the derivation rule removes. span.ledger.build.p95 stays gated at
0.48 of its trip point, so ledger construction keeps coverage.

The other 19 keys sit between 0.17 and 0.76 of their trip points.
span.tx.process.p95 is the tightest and is the first to re-measure if the gate
reddens again.

Repoint one bounds-checker test at span.ledger.build.p95, since it mutated the
p99 override this commit removes.
2026-09-10 18:55:53 +01:00
Pratik Mankawde
3a63a17548 docs(telemetry): stop the harness contract narrating its own revisions
Notes across the workload contract described earlier versions of themselves, or
cited commits that only exist inside this chain. A squash merge publishes none of
it, so each reference resolves nowhere.

Notes that described their own earlier text:

- expected_spans.json: 'this note previously concluded', 'this note previously
  said', 'Un-skipped 2026-08-26', 'the reason had simply gone stale for two
  weeks' and 'the claim this entry carried' are replaced by the standing reason
  each entry holds. The wildcard pairs now say the validator globs the child via
  _span_name_matches(), and state the literal-collapse failure as what a
  different validator WOULD do rather than as history.
- regression-thresholds.json: 'an earlier version of this note wrongly claimed',
  'the earlier version oversold it' and 'an earlier note called that' become the
  cautions themselves -- do not reason from 'every ladder step is at least 2x',
  do not oversell the backstop, do not read a false fire as a missing override.
- test_check_regression_bounds.py: the docstring gives the reason a literal is
  wrong here, not the story of two tests that once hard-coded one.

Baseline-refresh history rewritten as measurement:

- README.md, baselines/README.md, telemetry-runbook.md and regression-metrics.json
  no longer attribute threshold moves to 'the 2026-08-26 refresh'. The evidence
  is kept as measurement -- span.tx.apply.p50 has read 0.7917 ms and 0.00597 ms
  on the same workload, 132x apart; job.acceptLedger.running.p95 has measured a
  5.74x floor on one baseline and 16.28x on another -- which is what supports the
  claim that a single-run baseline cannot bound these keys.

Two chain-only commit ids removed, d059f21bf3 and 3860c93db2. Neither is
reachable from develop, so both cease to exist on merge; the second is chain
bookkeeping. The facts they were cited for (the validator globs wildcards; the
span ladder's floor is 0.01 ms) are stated directly instead.

Capture provenance is deliberately kept: baseline-timings.json 'captured_at',
the 2026-08-26 baseline heading, and the 2026-08-24 figures cited as data.

Documentation, JSON note strings and one docstring only, no behaviour change.
2026-09-02 20:32:13 +01:00
Pratik Mankawde
fa9f75d4e7 docs(telemetry): explain the absent path-finding load without the change story
The harness contract described how path-finding load came to be absent rather
than why it is absent. The load exists on no branch before this one, so a squash
merge publishes no revision that ever issued it: the 2026-08-25 date resolves
nowhere, and 'removing it', 'used to satisfy' and 'has now cleared' compare
against a state a reader cannot reach.

- README: state that DEFAULT_WEIGHTS carries no ripple_path_find entry, and give
  the error floor as what WOULD happen if it did, rather than what removing it
  fixed. 'Putting it back' becomes 'Enabling it'.
- expected_spans.json: the pathfind.request and pathfind.compute notes, and both
  hierarchy skip_reasons, now put the span's presence in the conditional -- the
  parent would appear if the RPC were issued, because the ScopedSpanGuard at
  RipplePathFind.cpp:35 sits above the rpcNOT_SUPPORTED guard at :48-49.
- expected_metrics.json: the rpc_method_errored_total, pathfind_fast and
  pathfind_full notes drop the date and keep both independent reasons the
  metrics stay absent.
- regression-thresholds.json: span.ledger.store 'is excluded from' the gated
  surface rather than 'was removed from' it.

The reasoning is unchanged: pathfinding is off because Config.cpp:725-726 zeroes
pathSearchMax when [validation_seed] is present, a refused call still exports an
error span, and at a 3% weight that is a ~3% STATUS_CODE_ERROR floor.

Documentation and JSON note strings only, no behaviour change.
2026-09-02 20:03:57 +01:00
Pratik Mankawde
a734da8b33 test(telemetry): recapture the baseline and stop gating what variance dominates
Refreshes baselines/baseline-timings.json from run 32964262700 at 8418d474a7,
byte-identical to the CI artifact. The previous baseline was captured at
6a82fc6f37, before the path-finding load was removed from the workload, so it
described a load shape the harness no longer runs.

Every absolute bound is re-derived, because the rule is hi_next minus baseline
and the baselines moved.

Three more keys stop being gated: span.tx.apply.p50, span.ledger.build.p50 and
span.consensus.ledger_close.p50. This is the rule the previous commit recorded
being applied, not a new exception -- a key is gateable only when its
run-to-run spread fits inside its bound.

The evidence is span.tx.apply.p50, which read 0.7917 ms in the old baseline and
0.00597 ms in this one. That is a 132x move between two runs of the SAME
workload. The old value happened to land mid-distribution, so hi_next minus
baseline gave a 4.21 ms bound that absorbed the spread; the new value lands in
the ladder's first bucket, so the same rule gives 0.0440 ms and cannot survive
one. Whether the gate functioned was decided by where in the distribution the
captured run happened to fall, which is not a threshold in need of tuning.
Measured spreads across four runs agree: 364x, 25.3x and 5.9x respectively.

All five excluded keys share one shape -- a baseline landing in the ladder's
low buckets, where the derived bound is tiny, together with large run-to-run
spread. Single-run baselines cannot support them; a multi-run baseline, or a
spread measurement captured alongside the baseline, is what would let them be
gated again. Not attempted here.

Both runs that would have reddened CI now replay clean, and an injected 10x
regression is still caught on 19 of the 20 remaining keys, 20 of 20 at 20x.
The exception is job.acceptLedger.running.p95, whose baseline fell while its
hi_next did not, moving its floor to 16.28x. It stays gated with that floor
recorded beside the other weak keys.

Also makes the bounds checker report a zero or negative baseline as a named
rule failure instead of dividing by it and raising.
2026-08-26 14:38:16 +01:00
Pratik Mankawde
3836078a78 fix(telemetry): stop gating ledger.validate p95 and p99, which vary too much
The regression gate has been red on runs with no code change. Only two of
the 25 gated keys ever tripped, both on the same span and never together:
run 32862589645 failed p99 at 25.8750 ms against a 1.0600 ms baseline
(+2341%), run 32867433073 failed p95 at 0.7500 ms against 0.2404 ms
(+212%), and in each run the other quantile sat well inside its own bound.
A real slowdown would move both. This is variance, not a defect.

Measured across four CI runs:

  span.ledger.validate.p50   0.0484 to 0.0778 ms    1.6x spread   kept
  span.ledger.validate.p95   0.1281 to 0.7500 ms    5.9x spread   excluded
  span.ledger.validate.p99   0.3875 to 25.8750 ms  66.8x spread   excluded

Both excluded quantiles reach past their trip point on a healthy run. The
mechanism is arrival timing, not slow code: the span opens only once a
quorum-completing validation arrives (LedgerMaster.cpp:987, inside
checkAccept, past the early return) and wraps the promotion work that
follows, so one slow consensus round dominates the tail of a 3m rate
window and which round that is differs every run.

Widening is not available and must not be attempted later: tolerating
25.8750 ms against a 1.0600 ms baseline needs a bound of about 24.8 ms,
which gates nothing. A bound admitting every healthy run's worst case
admits every regression too. p50 stays gated; it is stable.

THE GENERAL RULE, recorded so this does not recur: an absolute bound
derived as hi_next minus baseline comes from the histogram ladder, so it
budgets for quantization noise and for nothing else. It knows nothing about
how far a metric moves between runs on identical code. Before gating any
key, check its observed maximum across several runs against its trip point
and gate it only with margin. Spread alone proves nothing: tx.apply.p50
swings 364x and never fires, because its 5 ms trip point absorbs the range.
Of the 23 keys still gated the worst reaches 0.67 of its trip point.

Mechanism: spans.names lists span names while _quantiles is shared, so
dropping two quantiles of one span cannot be expressed by deleting a name.
regression-metrics.json gains an excluded_keys map from a flat key to the
reason it is not gated, subtracted by both prom_queries.py (so the key is
never queried) and check_regression_bounds.py rule A. A per-name quantile
override was rejected: a typo there leaves the key gating, whereas a typo
in an exclusion subtracts nothing and new rule F rejects it, along with an
empty reason, a leftover threshold override and a leftover baseline value.

Derived figures recomputed from the committed baseline: 25 gated keys to
23, detection floor 2.02x-9.43x to 2.02x-9.42x, weakly guarded keys ten to
nine, bound over baseline 102%-843% to 102%-842%. The baseline edit is a
deletion of two entries only, with no value rewritten.

Verified: both previously failing runs replay to zero regressions and exit
0; a tenfold increase injected into each of the 23 remaining keys in turn
is still caught in all 23 cases; rule F was confirmed load-bearing by
stubbing it out, which lets a stale exclusion pass.
2026-08-25 18:21:35 +01:00
Pratik Mankawde
e4926f55be fix(telemetry): derive workload gate bounds from the bucket above the baseline
The gate could not catch a regression on any sub-millisecond span.
compare_to_baseline.py requires both the percentage and the absolute bound to
breach, and every span shared one flat absolute bound of 10 ms (15 ms for p99)
calibrated for a 5-25 ms band the spans do not occupy. Against the baseline
captured on 2026-08-24, where 18 of the 28 quantiles gated at the time sat
below 1 ms, that bound sat 1.15x to 2000x above the metric it guarded, so the
AND never fired: a 100x regression injected into span.ledger.store.p95 reported
0 regressions and exit 0. Injecting a 10x regression into each key in turn was
caught on only 5 of 28.

Give every gated key its own absolute bound, equal to the distance from its
baseline to hi_next, the edge above the top of the bucket the baseline sits in.
The trip point is then exactly hi_next, so the gate fires only once the reading
clears the bucket above the baseline's own. That is the property a multiple of
the enclosing bucket width cannot provide: after the quantile crosses hi, the
interpolation happens across the next bucket, which on this ladder is up to
eight times wider, so no multiple of the enclosing width bounds the excursion.
Measured with a model-free reachability test, a single bucket crossing can
produce a false regression on 2 of 25 keys under the old flat bound and 0 of 25
under this rule. The smallest catchable regression is 2.02x to 9.43x per key.

The job queue bound had the same shape of problem on three of its four keys
(42x, 47x, 220x before). Defaults now sit at each ladder floor, leaving the
percentage bound operative for a metric that somehow reaches them.

Drop span.ledger.store from the gated surface. Its captured quantiles were
0.005, 0.0095 and 0.0099 ms, which is the ladder's 0.01 ms floor times the
quantile: every sample lands under 10 us, so the reported value does not move
even if each store slows from 2 us to 9 us. No bound can gate it. Presence is
still asserted by expected_spans.json and the integration test, and the rate is
still on the ledger-operations dashboard.

Add check_regression_bounds.py, wired into the same workflow step as the bucket
parity check. It fails when a bound is not the one its own baseline implies,
when a gated key has no override, when the baseline and metric surface disagree,
when the percentage bound would become operative, and when a baseline carries
the ladder floor signature. This gate has now broken three times through the
same drift between ladder, baseline and bounds, so documentation alone is not
enough.

compare_to_baseline.py is unchanged: its existing per-metric override mechanism
already expresses all of this.

A missing, unreadable or malformed input makes that check exit 1 naming the
input, rather than reporting success without having checked anything; only a
placeholder baseline, the documented bootstrap state, still exits 0. Its own
tests cover both halves of that contract plus one case per rule, and run in the
workflow before the check so a broken rule reads as a broken rule.
2026-08-25 13:02:12 +01:00
Pratik Mankawde
d1b80e47a2 test(telemetry): void span baselines captured on the old span ladder 2026-08-24 19:35:00 +01:00
Pratik Mankawde
1282645289 test(telemetry): invalidate job-queue baselines captured on the old ladder
The workload harness gates regressions on histogram_quantile over
job_queued_us / job_running_us, so re-cutting the microsecond ladder changes
what those queries return and the stored baselines no longer describe the
same measurement.

baseline-timings.json's job.acceptLedger.queued.p95 was 96.79us, which is
0.95 / 0.9926 x 100 -- the old 100us bucket edge scaled by the quantile, with
99.3% of samples beneath it. It was never a latency. Keeping it would make the
gate LESS sensitive rather than more: a genuine regression from a real 40us to
90us would still sit under 96.79us + 50% and pass.

Removes the four job.* entries and records why, including their values. The
comparer reports a metric absent from the baseline as "new metric (not in
baseline)" and skips it, so the span baselines stay live and gating continues
for everything unaffected. is_placeholder() still returns False, so this does
not disable the gate wholesale. Recapture the job.* numbers on a node running
the re-cut ladder.

Also corrects _bucket_note in regression-thresholds.json. It described the
spanmetrics ladder as 15 edges starting at 1ms; the collector config has 20,
including five sub-millisecond edges. The note's own reasoning was void too --
it justified the 10ms absolute span bound as "~2 low-end bucket widths", but
the low-end bucket width is 0.01ms, not 5ms. The bound is kept and justified
on the band where span quantiles actually sit, rather than on a derivation
from a ladder that no longer exists.
2026-08-21 12:49:56 +01:00
Pratik Mankawde
8dd64d4dcd fix(telemetry): add second-scale spanmetrics histogram buckets
P95 of second-scale spans was a meaningless interpolation. The spanmetrics
histogram topped out at [.. 1s, 5s], so consensus.round (~3.9s) and
consensus.establish (~1.9s) all fell into one 1s-5s bucket and
histogram_quantile interpolated linearly across that 4s-wide gap — the
"Build vs Close" / "Ledger Close Duration" panels' P95 read ~4800ms purely
as an artifact (verified: sum/count avg = 3824ms). ledger.acquire was worse:
~17% of samples exceeded the 5s ceiling, so its p95/p99 were unmeasurable.

Add 2s, 3s, 4s (resolve the 1-5s pile-up) and 10s, 30s (give the
ledger.acquire catch-up tail a measurable home). All ten existing boundaries
are preserved and the list stays strictly ascending (the connector
binary-searches buckets and silently misbuckets otherwise). Pin unit=ms so a
future collector default-unit flip can't rename the metric to _seconds.

Buckets chosen from the live mainnet distribution, not guessed. Native
beast::insight histograms (ms-scale RPC/IO timers in Telemetry.cpp) are 100%
under 5s, so they keep the original buckets — this is collector-only.

Applies on collector restart (cumulative series reset once, handled by
rate()). Runbook and regression-threshold bucket notes updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 14:27:06 +01:00
Pratik Mankawde
d83cb0bdb3 fix(telemetry): refresh regression baseline + widen bucket-noise thresholds
With validation now passing 133/133, the only remaining job failure was the
regression gate flagging 4 timing "regressions". Two compounding causes:

1. Stale baseline: the committed baseline was captured (2026-04-24) under the
   old, lighter workload — before the new txq-burst phase (60 TPS) existed. The
   heavier per-ledger work genuinely raises ledger.build / tx.apply /
   ledger.validate / acceptLedger timings, so every run regressed against it.
   Refreshed the baseline from the latest CI-measured timings (same workload).
2. Histogram quantization: SpanMetrics latency buckets are
   [1,5,10,25,...]ms, so a sub-millisecond quantile near a low-end boundary can
   jump a full bucket (1ms->5ms) between runs with no real change. The old
   absolute bounds (2-5ms) were narrower than one bucket width, so that jitter
   tripped the gate. Widened the default span bounds to 10-15ms (~2 low-end
   buckets) and pct to 50%, and the job_queue running bound to 20ms, to tolerate
   quantization noise while still catching genuine multi-bucket regressions. The
   consensus.* overrides (tight pct, large abs) are unchanged.

The refreshed baseline also picks up real rpc.ws_message timings (previously null
under the phantom rpc.request key).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:58:07 +01:00
Pratik Mankawde
dc13e9d680 fix: populate baseline from CI run, remove dead rpc_methods metrics
Populate baselines/baseline-timings.json from the green CI run
(24906110133, commit f11ebc1253). 25/31 metrics have non-null values;
6 span.rpc.* are null due to sparse data in the 3m window.

Remove the rpc_methods section from regression-metrics.json and its
thresholds. rippled_rpc_method_duration_us_bucket is never populated
because PerfLogImp::rpcEnd never calls MetricsRegistry::recordRpcFinished
— only recordRpcStarted is wired up (Phase 9 instrumentation gap).
The span-based rpc.request/rpc.process metrics via spanmetrics already
cover RPC latency.
2026-04-24 20:08:52 +01:00
Pratik Mankawde
df79d5e74b feat: add OTel-driven regression gate for Phase 10 telemetry validation
Captures per-span / per-RPC / per-job timings from Prometheus after the
workload run and diffs them against a committed baseline. Regression
requires breaching both a percentage and an absolute bound, tolerating
small-value noise. When the baseline is a placeholder, the comparator
emits the captured JSON in the exact schema for one-time paste into
baselines/baseline-timings.json, and the CI Step Summary surfaces that
block for the reviewer.

Scope: gate only — automated baseline persistence, benchmark.sh
PromQL migration, and the historical trend dashboard remain follow-ups.
2026-04-24 18:53:44 +01:00