Skip to content

feat(app): record app_block_gas_used per finalized block - #4091

Open
bdchatham wants to merge 1 commit into
mainfrom
feat/app-block-gas-used
Open

feat(app): record app_block_gas_used per finalized block#4091
bdchatham wants to merge 1 commit into
mainfrom
feat/app-block-gas-used

Conversation

@bdchatham

Copy link
Copy Markdown
Contributor

Why

sei-load's Load Test Client dashboard has a Gas Used panel that depends on sei-load's own --track-blocks WebSocket subscription, which most load profiles run without — so the panel shows no data. The fix is to repoint the dashboard at a node-side metric instead of sei-load's own observation of the chain.

Block Number and Block Time already have exact node-side equivalents (tendermint_consensus_latest_block_height, tendermint_consensus_block_interval_seconds_bucket). Gas Used did not: app_tx_gas{type="gas_used"} is a per-tx cumulative counter with no block-height label (a rate, not a per-block distribution), and it also misses both giga execution paths. app_block_gas_wanted is per-block but measures wanted, not used.

What

Adds app_block_gas_used, an OTel histogram recorded once per finalized block, mirroring app_block_gas_wanted's shape:

  • Recorded inside getFinalizeBlockResponse, the one place both FinalizeBlocker return paths that build a response converge — so it fires exactly once per finalized block, not once per ProcessBlock call (which can run twice for a height whose optimistic result is discarded).
  • sumBlockGasUsed totals ExecTxResult.GasUsed across the block, skipping nil entries and guarding int64 overflow. On overflow or a negative entry it skips the record rather than recording a wrapped value — unreachable in practice (would need ~9.2e18 gas against a 50M MaxGasWanted cap) but consensus-critical code gets the defensive guard anyway.
  • Reuses blockGasWantedBuckets: a tx's gas used is bounded by its gas wanted, so both block totals sit under the same 50M cap.
  • app_block_gas_used and app_block_gas_wanted record on different ABCI phases (FinalizeBlock vs. ProcessProposal), so a block-syncing or state-syncing node emits gas_used samples with no matching gas_wanted sample. Documented on the metric description rather than fixed — nothing depends on dividing the two today.

Verification

gofmt -l app/metrics.go app/app.go app/consensus_params_test.go app/block_gas_used_test.go   # exit 0
go vet ./app/...                                                                              # exit 0
go build ./app/...                                                                            # exit 0
go test ./app/ -run 'TestSumBlockGasUsed|TestGetFinalizeBlockResponsePropagatesFullConsensusParams' -count=1   # PASS, 7/7
go test ./app/ -race -run '<same>' -count=1                                                  # PASS
golangci-lint run ./app/...                                                                   # one pre-existing goconst finding, confirmed unrelated by diffing against a stash

Not run: the full ./app/... test suite (out of scope for a single new metric touching no existing behavior) and CI-pinned golangci-lint v2.8.0 (local run used v2.12.2).

Blast radius

getFinalizeBlockResponse is unexported; its only callers are the two FinalizeBlocker paths (app/app.go) plus one test (app/consensus_params_test.go), all updated for the new leading context.Context parameter.

🤖 Generated with Claude Code

app_block_gas_wanted records the block's total estimated gas, checked
in ProcessProposal before execution. Nothing on the used side existed
at block granularity: app_tx_gas{type="gas_used"} is per-tx and also
misses both giga execution paths, so it undercounts.

Add app_block_gas_used, an OTel histogram recorded once per finalized
block inside getFinalizeBlockResponse, the one place both
FinalizeBlocker paths that build a response converge. It sums
ExecTxResult.GasUsed across the block via sumBlockGasUsed, skipping
nil entries and guarding int64 overflow -- on overflow or a negative
entry the metric is skipped rather than recorded wrapped, since a
block realistically cannot reach ~9.2e18 gas against a 50M
MaxGasWanted cap.

The two metrics record on different ABCI phases (ProcessProposal vs
FinalizeBlock), so a block-syncing or state-syncing node emits
gas_used samples with no matching gas_wanted sample. Documented on
the metric description rather than fixed, since dividing the two
today is not something anything currently depends on.

Reuses blockGasWantedBuckets for bucket boundaries: gas used is
bounded by gas wanted per tx, so both block totals sit under the same
50M cap.

Motivated by sei-load's Load Test Client dashboard: its Gas Used panel
currently depends on sei-load's own --track-blocks WebSocket
subscription, which most load profiles run without. This gives the
dashboard a node-side source instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Sep 3, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Observability-only change on the finalize-block response path; no consensus or transaction execution logic is altered beyond defensive gas summation before recording.

Overview
Adds node-side app_block_gas_used OpenTelemetry histogram so dashboards can chart per-block gas consumption without relying on sei-load’s block-tracking WebSocket.

Recording happens in getFinalizeBlockResponse, which now takes a context.Context and is the single convergence point for both FinalizeBlocker response paths—one sample per finalized block, including empty blocks (total 0), and not on discarded optimistic ProcessBlock runs. sumBlockGasUsed sums ExecTxResult.GasUsed (skips nil txs; refuses negative values and int64 overflow, skipping the metric on failure). Histogram buckets match app_block_gas_wanted (0–50M). The metric description notes gas_used is emitted at FinalizeBlock while gas_wanted is at ProcessProposal, so block/state sync can produce unmatched samples.

Unit tests cover sumBlockGasUsed edge cases; consensus_params_test was updated for the new context argument.

Reviewed by Cursor Bugbot for commit ea4d430. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedSep 3, 2026, 5:24 PM

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.25%. Comparing base (c9f9922) to head (ea4d430).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4091      +/-   ##
==========================================
- Coverage   61.31%   60.25%   -1.06%     
==========================================
  Files        2183     2075     -108     
  Lines      191331   178795   -12536     
==========================================
- Hits       117310   107732    -9578     
+ Misses      62942    61032    -1910     
+ Partials    11079    10031    -1048     
Flag Coverage Δ
sei-chain-pr 51.34% <100.00%> (?)
sei-db 69.80% <ø> (ø)
sei-db-state-db ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
app/app.go 71.52% <100.00%> (+0.21%) ⬆️
app/metrics.go 84.61% <ø> (ø)

... and 108 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@seidroid seidroid 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.

Adds an app_block_gas_used OTel histogram recorded once per finalized block from getFinalizeBlockResponse, with a correctly-guarded sumBlockGasUsed helper and unit tests covering the empty, nil-entry, negative, overflow, and exact-MaxInt64 cases. The call site is the true choke point for both FinalizeBlocker response paths, bucket reuse is justified by the 50M MaxGasWanted cap, and the gas_wanted/gas_used ABCI-phase asymmetry is documented on the metric itself.

Findings: 0 blocking | 0 non-blocking | 0 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • None at the file/PR level.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant