Skip to content

feat(server,types): stage-timing observability — queue-wait metric, self-describing completions, identified spans#71

Open
chetanyb wants to merge 3 commits into
eth-act:masterfrom
chetanyb:feat/queue-wait-metric
Open

feat(server,types): stage-timing observability — queue-wait metric, self-describing completions, identified spans#71
chetanyb wants to merge 3 commits into
eth-act:masterfrom
chetanyb:feat/queue-wait-metric

Conversation

@chetanyb

@chetanyb chetanyb commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Problem

Per-stage timing of a proof request is only partially observable:

  • zkboost_prove_duration_seconds starts after a worker dequeues the job — time waiting in the worker channel between dispatch and dequeue appears in no metric. Under sustained load that wait dominates: we observed traces where request_proof spanned 5m16s containing only ~6.6s of witness fetch and ~21s of proving, the rest being invisible queue wait.
  • Consumers wanting per-request stage attribution must either poll /dashboard/state (bounded retention, lost on restart) or subtract span timestamps in a trace backend — both lossy reconstructions of numbers the server already holds at completion time.
  • The stage spans (request_proof, fetch_witness, prove/*) carry no identifying attributes, so span rows in trace UIs can't be related to a block without walking the parent chain.

Changes

Commit 1 — metrics:

  1. New zkboost_queue_wait_duration_seconds{proof_type} — an Instant stamped into WorkerInput at dispatch, observed at dequeue. Long-tail buckets (0.05s … 900s): backlogs of minutes are exactly what this metric exists to expose.
  2. zkboost_witness_fetch_duration_seconds bucket fix — the default buckets end at 10.0, hiding successful 10–12s fetches in +Inf despite the slot-aligned witness timeout; now the same 0.5s-step bounds to 12.0 as the prove histogram.

Commit 2 — self-describing completions + span identity:

  1. ProofComplete gains optional witness_ms / queue_wait_ms / prove_ms (Option<u64> + #[serde(default)]). The three stages exactly partition the request lifecycle: admission → witness available → dequeue → done. Absence means unknown, not zero — old producers and replayed cache entries (which hold only proof bytes) send None. Producers and consumers can upgrade in either order; serde-compat tests cover both directions plus a populated round trip.
  2. Identity attributes on stage spans: new_payload_request_root on request_proof and prove; block_hash on fetch_witness (the witness service is keyed by execution block hash and serves all requests for that block, so the hash is its identity).

Commit 3 — the same fields on failures:

  1. ProofFailure gains the same optional trio, with one semantic addition: on failures, None also means the stage never ran, so the populated/absent shape locates how far a request got before dying — a witness-timeout failure carries only witness_ms; a proving-timeout carries all three with prove_ms = the exhausted budget; an admission-time rejection carries none. Timings are threaded to each failure site via a small all-None-default StageTimings carrier. Replayed failures keep their timings for free (the failure cache stores the whole event). Without this, stage data would exist only for successes — while failures are the primary subjects of stage-attribution analysis.

Notes

  • No behavior change — measurement and event enrichment only; existing consumers are unaffected (unknown JSON keys are ignored, no deny_unknown_fields anywhere on this path).
  • cargo fmt / clippy --all-targets --locked -- -D warnings / cargo test --locked (80 passed) all clean.
  • With this, witness + queue_wait + prove ≈ request_proof closes arithmetically, per-request stage data arrives with completion and failure events instead of being polled, and every stage span self-identifies in trace UIs.

chetanyb and others added 2 commits July 23, 2026 16:12
…to the slot budget

The prove duration histogram starts only after a worker dequeues the job, so
time spent waiting in the worker channel between dispatch and dequeue is
invisible in every metric. Under sustained load that wait dominates: with
arrivals fixed at one block per slot and heavy blocks proving slower than a
slot, multi-minute backlogs were observed in traces as unexplained gaps
inside request_proof (witness fetched at T0, proving starting minutes later).

Add zkboost_queue_wait_duration_seconds{proof_type}, stamped at dispatch
(WorkerInput.queued_at) and observed at dequeue, with long-tail buckets
(0.05s..900s) — backlogs of minutes are exactly what this metric exists to
expose, so a 12s ceiling would defeat it.

Also give zkboost_witness_fetch_duration_seconds the same 0.5s-step bounds
up to 12.0 as the prove histogram: the default buckets end at 10.0, hiding
successful 10-12s fetches in +Inf even though the slot-aligned witness
timeout makes that range meaningful.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…age spans

Completion events become self-describing: ProofComplete gains optional
witness_ms / queue_wait_ms / prove_ms fields (serde(default), so producers
and consumers upgrade in either order — absent fields read as None, meaning
unknown rather than zero). The three stages partition the request lifecycle
exactly: request admission -> witness available -> worker dequeue -> proof
done. Replayed completions from the proof cache send None — the cache holds
only proof bytes, so replay timings are honestly unknown.

Consumers today must either poll the dashboard state (bounded retention,
lost on restart) or subtract span timestamps in a trace backend to attribute
stage time; both are lossy reconstructions of numbers the server already
holds at completion time.

Also stamp identity attributes on the stage spans so each is self-describing
in trace UIs: new_payload_request_root on request_proof and prove,
block_hash on fetch_witness (the witness service is keyed by execution block
hash and serves all requests for that block, so the hash is its identity).

Serde-compat tests cover both upgrade directions plus a populated round trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chetanyb chetanyb changed the title feat(server): measure worker queue wait; align witness-fetch buckets to the slot budget feat(server,types): stage-timing observability — queue-wait metric, self-describing completions, identified spans Jul 23, 2026
Failures get the same optional witness_ms / queue_wait_ms / prove_ms fields
as completions, with one semantic addition: on failures, None also means the
stage never ran for this request — so the populated/absent shape locates how
far a request got before dying. A witness-timeout failure carries only
witness_ms; a proving-timeout failure carries all three, with prove_ms the
exhausted budget; an admission-time rejection carries none.

Timings known at each failure site are threaded via a small StageTimings
carrier defaulting to all-None. Replayed failures keep their timings for
free: the failure cache stores the whole event, unlike the proof cache.

Without this, per-request stage data would exist only for successes — while
failed requests, the primary subjects of stage-attribution analysis, would
be the one population without it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant