Repository navigation
perf: split count from page queries, cache deposit queue anchor, async debug page stats - #919
Merged
Merged
Conversation
…c debug page stats The filtered list queries (withdrawals, deposits, deposit txs, mev blocks) materialized the whole filtered set through a twice-referenced CTE just to return 20 rows plus a count, which took >90s on the 19M-row hoodi withdrawals table and saturated the database for every other page. The page query now runs on its own with index-friendly `DESC NULLS LAST` ordering, and the count runs separately: on PostgreSQL the planner estimate is used when an exact count would be expensive (EXPLAIN via simple protocol so real values are planned), exact otherwise and on SQLite. The withdrawals slot filter is expressed as a block_uid range and the deposits "valid only" filter as a semi-join, so both are served from indexes and estimated correctly. The deposit queue anchor (most recent included deposit) is cached per queue snapshot root; it was previously re-resolved on every deposits page, validator deposits tab, builder page and deposit API call, each one a full count over the deposits table when no deposit was in the block cache. The debug page computes its stored-object stats (slots aggregates plus a full pebble key scan) in the background and serves a timestamped snapshot, and the default HTTP write timeout leaves headroom above the page call timeout so a timed-out page returns an error page instead of a cut connection (502). Migration adds an index on tx_pending_signatures(queuetime), whose poller sorted 5.6M rows every 10s, and a (type, block_uid) index on withdrawals replacing the single-column type index.
This comment has been minimized.
This comment has been minimized.
… empty pages past the end
There was a problem hiding this comment.
Summary
The PR splits filtered-list counts from page queries, caches the deposit queue anchor per snapshot root, and moves debug stored-object stats to a background snapshot; the new commits since the last review are only merges from master that touch no PR-scope files. The previously reported out-of-range pagination bug is still present: an empty page past the end of the result set reports the offset as the total row count.
Issues
- 🟡
db/query_helpers.go:123— out-of-range page returns offset as the total count — see the thread on that line
Reviewed @ 975d0a5e
"Documentation is a love letter you write to your future self." — Damian Conway
barnabasbusa
approved these changes
Oct 23, 2026
… page as the count floor; test filtered queries on both engines
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Triage of slow pages on dora.hoodi.ethpandaops.io and the
/debug/cache502 on light-hoodi.Filtered list queries (
GetWithdrawalsFiltered,GetDepositsFiltered,GetDepositTxsFiltered,GetMevBlocksFiltered)WITH cte AS (...) SELECT count(*) FROM cte UNION ALL (SELECT * FROM cte ORDER BY .. LIMIT n)pattern references the CTE twice, so Postgres materializes the whole filtered set even for the 20-row page. On the 19M-row hoodiwithdrawalstable that was >90s per view and saturated the DB for everyone else.DESC NULLS LASTordering (the schema'sASC NULLS FIRSTindexes can only serve that form via a backward scan), count runs separately viacountFilteredRows:EXPLAIN (FORMAT JSON)first (simple protocol, so the planner sees real values); exact count if cost < 10k, also exact if estimate < 10k rows and cost < 100k (small estimates are unreliable), otherwise the planner estimateblock_uid >> 16to ablock_uidrange (index-served, estimable).EXISTSsemi-join: the two-column join was estimated at 12 rows (real 648k) which forced a hash join for the page and a count of 30.Deposit queue anchor: cached per queue snapshot root. It was re-resolved on every deposits page, validator deposits tab, builder page and deposit API call, each one a full count over
depositswhen no deposit was in the block cache (1–7 concurrent on hoodi from external API traffic).Debug page: stored-object stats (slots aggregates + full pebble key scan, ~20s on light-hoodi) are computed in a background goroutine and served as a timestamped snapshot (10 min refresh). Default
httpWriteTimeoutis now page call timeout + 5s (35s default) so a timed-out page delivers the error page instead of a cut connection.Migration
20261022000000_perf-indexes.sql:tx_pending_signatures(queuetime)(poller sorted 5.6M rows every 10s) andwithdrawals(type, block_uid)replacing the single-column type index (rare-type filter walked 212k rows backward otherwise). Note: the withdrawals index builds at startup inside the goose transaction; a few minutes on mainnet-scale tables.Measured on hoodi (in-pod EXPLAIN ANALYZE, read-only)
Single-table count estimates are within 1–3 % of exact.
Test plan
go build ./...,go vet ./...,go test ./...--new-from-rev=origin/master: 0 issues