feat(app): record app_block_gas_used per finalized block - #4091
Conversation
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>
PR SummaryLow Risk Overview Recording happens in Unit tests cover Reviewed by Cursor Bugbot for commit ea4d430. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ 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
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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.
Why
sei-load's Load Test Client dashboard has a Gas Used panel that depends on sei-load's own
--track-blocksWebSocket 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_wantedis per-block but measures wanted, not used.What
Adds
app_block_gas_used, an OTel histogram recorded once per finalized block, mirroringapp_block_gas_wanted's shape:getFinalizeBlockResponse, the one place bothFinalizeBlockerreturn paths that build a response converge — so it fires exactly once per finalized block, not once perProcessBlockcall (which can run twice for a height whose optimistic result is discarded).sumBlockGasUsedtotalsExecTxResult.GasUsedacross 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 50MMaxGasWantedcap) but consensus-critical code gets the defensive guard anyway.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_usedandapp_block_gas_wantedrecord on different ABCI phases (FinalizeBlock vs. ProcessProposal), so a block-syncing or state-syncing node emitsgas_usedsamples with no matchinggas_wantedsample. Documented on the metric description rather than fixed — nothing depends on dividing the two today.Verification
Not run: the full
./app/...test suite (out of scope for a single new metric touching no existing behavior) and CI-pinnedgolangci-lintv2.8.0 (local run used v2.12.2).Blast radius
getFinalizeBlockResponseis unexported; its only callers are the twoFinalizeBlockerpaths (app/app.go) plus one test (app/consensus_params_test.go), all updated for the new leadingcontext.Contextparameter.🤖 Generated with Claude Code