Skip to content

Add bottleneck and run-time metrics for indexer utilisation - #1486

Merged
DZakh merged 11 commits into
mainfrom
claude/indexer-bottleneck-metrics-yjmhos
Jul 27, 2026
Merged

Add bottleneck and run-time metrics for indexer utilisation#1486
DZakh merged 11 commits into
mainfrom
claude/indexer-bottleneck-metrics-yjmhos

Conversation

@DZakh

@DZakh DZakh commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

The existing utilisation metrics (envio_storage_load_seconds, envio_processing_seconds, envio_preload_seconds, …) show which stage is busy, but not which one everything else is waiting on. In a pipeline where fetching runs concurrently with processing and writes happen off the processing path, busy-time alone can't identify the bottleneck.

This adds the missing wait-side accounting, plus the time gauges needed to read a scrape without a query-time clock.

New metrics

Stall counters — attribute processing idle time to its cause:

Metric Meaning
envio_processing_stalled_on_fetch_seconds Loop idle with an empty buffer, waiting for fetched events
envio_processing_stalled_on_storage_write_seconds Processing blocked on write backpressure (Writing.awaitCapacity)

The write stall closes a real blind spot: awaitCapacity previously ran before any timer started, so when Postgres writes were the constraint, every panel went down and nothing went up.

Together with the existing counters, the processing side now accounts for its own wall clock — preload + processing + stalled_on_fetch + stalled_on_storage_write covers busy and both wait causes, so one stacked panel points at the constraint.

Process time gauges — rendered at the top of the payload, so a single scrape is self-describing:

Metric Meaning
envio_process_start_time_seconds Run start (moved to the top; unchanged otherwise)
envio_process_metric_time_seconds When the snapshot was generated
envio_process_elapsed_seconds Seconds since start

elapsed makes counter shares readable straight off one payload, with no rate() and no external clock — e.g. envio_processing_stalled_on_storage_write_seconds / envio_process_elapsed_seconds is the fraction of the run spent stalled on writes. metric_time dates a saved scrape.

Implementation notes

  • Fetch stall is event-driven, not sampled: the processing loop exits when starved and is re-kicked by fetch, so a timestamp is recorded at loop exit and folded in at beginProcessing. toMetrics adds the in-progress interval so a scrape mid-stall isn't stale, matching the existing SourceManager status pattern. The counter stays monotonic across a stall.
  • At-head gating: the mark is skipped when every chain has buffered up to its known head, via ChainState.isFetchingAtHead (which accounts for blockLag and endBlock rather than a raw buffer-equals-height check). Without this the counter climbed while simply idling at the tip, where nothing is bottlenecked.
  • Reorg settling: beginReorg closes any open stall interval. Otherwise an interval opened before a reorg stayed open across the rollback and was folded into the stall on the next beginProcessing — double-counting time envio_rollback_seconds already owns. Reachable on multichain indexers where one chain backfills while another reorgs at head.

Caveat worth knowing

stalled_on_fetch also accrues briefly at startup, before the first height query returns (knownHeight === 0). The at-head gate covers the steady-state tip case. The metric is named "stalled" rather than "bottlenecked" for this reason, and the help text points at envio_indexing_source_waiting_seconds for disambiguation.

Tests

  • Metrics_test.res: the golden snapshot covers all five new metrics with explicit values.
  • IndexerStateStall_test.res (new): covers the two invariants the counter depends on — a repeated mark must not restart an open interval, and beginReorg must settle it. Drives a real IndexerState via MockIndexer.InMemoryStore.make and reads the counter through toMetrics.

The reorg test is a genuine regression test, not just a passing assertion: reverting the beginReorg settle makes it fail with the full simulated rollback span attributed to envio_processing_stalled_on_fetch_seconds (expected 0.0502 to be 0.00004).

Not included

  • Dashboard panels for the existing metrics that already help (buffer ratio envio_indexing_buffer_size / envio_indexing_target_buffer_size, envio_indexing_idle_seconds, envio_effect_queue_wait_seconds, nodejs_eventloop_utilization) — no code change needed for those.
  • A per-chain caught-up timestamp for time-to-sync, which would need timestampCaughtUpToHeadOrEndblock exposed as a gauge.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TCWAgbg4CSpwkYctA4LWa1

Summary by CodeRabbit

  • New Features

    • Added Prometheus metrics for process timing, including metric timestamp and elapsed processing time.
    • Added metrics tracking time stalled while fetching and waiting for storage writes.
    • Improved attribution of idle processing time when fetching is unavailable.
  • Bug Fixes

    • Prevented fetch-stall durations from being double-counted during reorgs or resumed processing.
    • Added accurate measurement of storage backpressure delays.

Add two counters that attribute batch-processing idle time to its cause,
so dashboards can tell whether fetching or storage writes is the
constraint rather than only seeing which stage is busy:

- envio_processing_stalled_on_fetch_seconds: time the processing loop sat
  with an empty buffer waiting for fetched events.
- envio_processing_stalled_on_storage_write_seconds: time processing was
  blocked on write backpressure (Writing.awaitCapacity).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCWAgbg4CSpwkYctA4LWa1
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Processing stall durations are tracked for fetch starvation and storage-write waits, exposed through indexer metrics, rendered as Prometheus counters and process-time gauges, and covered by collection and state-accounting tests.

Changes

Processing stall metrics

Layer / File(s) Summary
Stall state tracking
packages/envio/src/IndexerState.res, packages/envio/src/IndexerState.resi
Indexer state tracks fetch and storage-write stall durations, settles active fetch intervals, exposes recording functions, and includes live values in metric snapshots.
Processing wait instrumentation
packages/envio/src/BatchProcessing.res, packages/envio/src/EventProcessing.res
Idle processing gaps are marked as fetch stalls, and storage-capacity wait time is recorded as storage-write stall duration.
Prometheus metrics and validation
packages/envio/src/Metrics.res, packages/envio-tests/test/lib_tests/Metrics_test.res
Metric snapshots and Prometheus output add process timing gauges and fetch/storage stall counters, with expected output updated.
Stall accounting validation
scenarios/test_codegen/test/IndexerStateStall_test.res
Tests verify that repeated fetch-stall marks do not restart timing and that reorg handling settles the interval.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BatchProcessing
  participant EventProcessing
  participant IndexerState
  participant Metrics
  BatchProcessing->>IndexerState: markProcessingStalledOnFetch()
  EventProcessing->>IndexerState: recordStalledOnStorageWrite(seconds)
  IndexerState->>Metrics: toMetrics()
  Metrics->>Metrics: render process and stall metrics
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main addition of bottleneck and runtime metrics for indexer utilization.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/envio/src/IndexerState.res`:
- Around line 403-412: Update the transition handlers beginReorg, stop, and
errorExit to finalize the active processingStalledOnFetchSince interval
consistently with beginProcessing, adding its elapsed duration to
processingStalledOnFetchSeconds before clearing the ref. Ensure each transition
clears the stall state so toMetrics does not count rollback or shutdown time and
later beginProcessing cannot flush it again.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b53d1afa-7282-41a4-b0ff-31f471518787

📥 Commits

Reviewing files that changed from the base of the PR and between dc505fd and 85d54c4.

📒 Files selected for processing (6)
  • packages/envio-tests/test/lib_tests/Metrics_test.res
  • packages/envio/src/BatchProcessing.res
  • packages/envio/src/EventProcessing.res
  • packages/envio/src/IndexerState.res
  • packages/envio/src/IndexerState.resi
  • packages/envio/src/Metrics.res

Comment on lines +403 to +412
let beginProcessing = (state: t) => {
switch state.processingStalledOnFetchSince {
| Some(since) =>
state.processingStalledOnFetchSeconds =
state.processingStalledOnFetchSeconds +. since->Performance.secondsSince
state.processingStalledOnFetchSince = None
| None => ()
}
state.isProcessing = true
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Terminate active fetch stalls on reorg and shutdown.

processingStalledOnFetchSince is only cleared by beginProcessing; beginReorg, stop, and errorExit leave it set. toMetrics therefore keeps adding elapsed time during rollback/shutdown, and a later beginProcessing flushes that transition time into the cumulative counter. Finalize the interval at each transition—or explicitly discard it per the metric contract—and clear the ref; guarding only the scrape is insufficient.

Also applies to: 508-513

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/envio/src/IndexerState.res` around lines 403 - 412, Update the
transition handlers beginReorg, stop, and errorExit to finalize the active
processingStalledOnFetchSince interval consistently with beginProcessing, adding
its elapsed duration to processingStalledOnFetchSeconds before clearing the ref.
Ensure each transition clears the stall state so toMetrics does not count
rollback or shutdown time and later beginProcessing cannot flush it again.

Don't attribute processing idle time to fetch starvation when every
chain has already buffered up to its known head. In that state the loop
is idling at the tip waiting for new blocks, not bottlenecked on fetch,
so counting it inflated envio_processing_stalled_on_fetch_seconds.

Uses ChainState.isFetchingAtHead, which accounts for blockLag and
endBlock rather than a raw buffer-equals-height comparison.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCWAgbg4CSpwkYctA4LWa1

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 85d54c4e96

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/envio/src/IndexerState.res Outdated
Comment on lines +406 to +408
state.processingStalledOnFetchSeconds =
state.processingStalledOnFetchSeconds +. since->Performance.secondsSince
state.processingStalledOnFetchSince = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop fetch-stall timer before rollback

Folding every open fetch-stall interval here misattributes rollback time when a fetch response arrives while processing is already idle and detects a reorg: that path calls beginReorg/scheduleRollback() without beginProcessing, so processingStalledOnFetchSince stays open through the async rollback work and is only added when rollback later schedules processing. In reorg-heavy runs this makes envio_processing_stalled_on_fetch_seconds spike even though the processor was resolving rollback, not waiting for fetched events; clear or close this timer when entering rollback/reorg instead.

Useful? React with 👍 / 👎.

Expose two render-time gauges so a single /metrics scrape is
self-describing without a query-time clock:

- envio_process_elapsed_seconds: seconds since the indexer started.
  Dividing a cumulative counter by it yields that stage's share of the
  whole run straight from one snapshot.
- envio_scrape_time_seconds: unix time the snapshot was generated, so a
  scrape can be dated on its own.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCWAgbg4CSpwkYctA4LWa1
Move the three process-time gauges to the top of the render, right below
envio_info, ordered start time, metric time, elapsed. Rename
envio_scrape_time_seconds to envio_process_metric_time_seconds for a
consistent envio_process_* prefix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCWAgbg4CSpwkYctA4LWa1
Extract the fetch-stall fold into settleStalledOnFetch and call it from
beginReorg. Without it, a stall interval opened before a reorg stayed
open across the rollback and was folded into
envio_processing_stalled_on_fetch_seconds on the next beginProcessing,
double-counting time envio_rollback_seconds already owns.

Rename the Metrics.t field scrapeTime to metricTime to match the
envio_process_metric_time_seconds metric it renders.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCWAgbg4CSpwkYctA4LWa1

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dfd8aad84b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/envio/src/EventProcessing.res Outdated
Comment on lines +355 to +358
let writeStallRef = Performance.now()
await indexerState->Writing.awaitCapacity
indexerState->IndexerState.recordStalledOnStorageWrite(
~seconds=writeStallRef->Performance.secondsSince,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record only actual storage-capacity waits

When Writing.awaitCapacity returns immediately because the change count is below keepLatestChangesLimit, awaiting its already-resolved promise still defers this continuation to a microtask, so every processed batch adds scheduling latency to envio_processing_stalled_on_storage_write_seconds despite no storage backpressure. On high-throughput or event-loop-busy indexers this can make the new metric falsely identify storage writes as a bottleneck; measure only the branch that actually waits for a commit/capacity to be freed.

Useful? React with 👍 / 👎.

->Array.every(ChainState.isFetchingAtHead)
)
) {
state->IndexerState.markProcessingStalledOnFetch

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recheck buffered work before starting the fetch-stall timer

When a fetch response is applied after an empty processNextBatch has taken its snapshot but before the caller resumes from await, that response can enqueue ready items while its scheduleProcessing() call is ignored because isProcessing is still true. This path then starts the fetch-stall interval solely from the frontier check without rechecking for buffered work; if the response also fills the shared fetch budget, the subsequent fetch kick has nothing to dispatch, so the metric can keep reporting fetch starvation while processable events are already buffered. Recheck for work or rerun processing before marking the stall.

Useful? React with 👍 / 👎.

Comment thread packages/envio/src/Metrics.res Outdated
~name="envio_process_elapsed_seconds",
~help="Seconds elapsed since the indexer started. Divide a cumulative counter (e.g. envio_processing_seconds) by this to get its share of the whole run without a query-time clock.",
~kind="gauge",
~value=(metrics.metricTime->Date.getTime -. metrics.startTime->Date.getTime) /. 1000.,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive elapsed runtime from a monotonic clock

If the host wall clock is corrected after the indexer starts—for example by NTP synchronization, a manual clock change, or a VM clock adjustment—subtracting these two Date values makes envio_process_elapsed_seconds jump forward, move backward, or even become negative. The cumulative processing and stall counters use monotonic Performance durations, so dividing them by this wall-clock-derived gauge can produce invalid utilization shares; retain a monotonic start reference and use it for elapsed time while keeping metricTime as the wall-clock timestamp.

Useful? React with 👍 / 👎.

Cover the two invariants the stall counter depends on: a repeated mark
must not restart an open interval, and beginReorg must settle it so the
rollback that follows isn't folded into the stall.

The reorg case is a regression test for the double-count fixed in the
previous commit — reverting that fix makes it fail with the full
rollback span attributed to envio_processing_stalled_on_fetch_seconds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCWAgbg4CSpwkYctA4LWa1

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5c3e0ccd2a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

processingSeconds: state.processingSeconds,
processingStalledOnFetchSeconds: state.processingStalledOnFetchSeconds +.
switch state.processingStalledOnFetchSince {
| Some(since) => since->Performance.secondsSince

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Settle the fetch-stall timer when stopping

When a state is already fetch-stalled and a fetch/write failure invokes errorExit (or a test/dev handoff calls stop), those shutdown transitions only set isStopped and leave processingStalledOnFetchSince open. Because this getter unconditionally adds secondsSince for an open interval, subsequent metrics snapshots keep increasing envio_processing_stalled_on_fetch_seconds after every indexer loop has halted; settle the interval as part of both stop transitions so the counter freezes at shutdown.

Useful? React with 👍 / 👎.

Replace the minimal Utils.magic state with MockIndexer.InMemoryStore.make,
so the tests drive a real IndexerState and read the counter through
toMetrics rather than a hand-built stub that would break confusingly if a
transition started reading another field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCWAgbg4CSpwkYctA4LWa1
@DZakh DZakh changed the title Add metrics for processing stalls on fetch and storage write Add bottleneck and run-time metrics for indexer utilisation Jul 27, 2026
Drop internal vocabulary (backpressure, buffer, query-time clock) and say
what each metric is for: how to spot the bottleneck and where to look
next when a rate is high.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCWAgbg4CSpwkYctA4LWa1

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scenarios/test_codegen/test/IndexerStateStall_test.res`:
- Around line 25-30: Strengthen the test around IndexerState.beginReorg by
adding a delay after markProcessingStalledOnFetch and before beginReorg, then
assert that settledOnReorg is positive. Retain the existing later equality
assertion so rollback time remains excluded.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 27cb8fff-bdc5-4bf1-acf9-e1977e77d577

📥 Commits

Reviewing files that changed from the base of the PR and between 5c3e0cc and 84b558c.

📒 Files selected for processing (3)
  • packages/envio-tests/test/lib_tests/Metrics_test.res
  • packages/envio/src/Metrics.res
  • scenarios/test_codegen/test/IndexerStateStall_test.res
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/envio/src/Metrics.res
  • packages/envio-tests/test/lib_tests/Metrics_test.res

Comment thread scenarios/test_codegen/test/IndexerStateStall_test.res
claude added 2 commits July 27, 2026 11:52
- Derive envio_process_elapsed_seconds from a monotonic reference taken at
  startup instead of subtracting two wall-clock Dates. An NTP correction
  could otherwise skew, or invert, the denominator used to divide the
  Performance-based counters.
- Settle an open fetch stall in stop and errorExit, so the counter freezes
  at shutdown rather than growing for as long as the process serves
  metrics.
- Time only the over-limit path in awaitCapacity. Timing every call charged
  each batch the microtask hop of awaiting an already-resolved promise,
  reading as storage backpressure that wasn't there.
- Wait before beginReorg in the reorg test, so it distinguishes settling
  the interval from discarding it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCWAgbg4CSpwkYctA4LWa1
…s-yjmhos' into claude/indexer-bottleneck-metrics-yjmhos
@DZakh
DZakh merged commit 23f9e0c into main Jul 27, 2026
8 checks passed
@DZakh
DZakh deleted the claude/indexer-bottleneck-metrics-yjmhos branch July 27, 2026 12:07
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.

2 participants