Skip to content

Improve giga execution performance - #4317

Merged
yzang2019 merged 6 commits into
mainfrom
yzang/backport-giga1-optimization
Sep 26, 2026
Merged

yzang2019 merged 6 commits into
mainfrom
yzang/backport-giga1-optimization

Conversation

@yzang2019

@yzang2019 yzang2019 commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Ports the execution optimizations from #4219 (giga-1) onto main, adapted to changes that have landed on main since.

  • Pipelined state commits (load test only for now): a block's CommitStateChanges runs in the background while the next block executes, reading the previous block's writes through a pending overlay. ExecuteBlock still waits for its commit, so Autobahn's evmonlyapp, which calls it from FinalizeBlock, keeps committing synchronously; wiring the pipeline into Autobahn is a follow-up. AwaitCommits is the sync point for the pipelined path.
  • Single-row account reads: EVMStateView.ReadAccount returns an account's balance, nonce and code hash in one lookup, used by nativeStateDB and the OCC merge.
  • Parallel OCC merge prefetch and parallel receipt encoding across the OCC worker pool.
  • evmonly-loadtest: --accounts sender pool with streaming --blocks=0, --gc-percent, OTel phase timers, plus a Grafana dashboard and Prometheus scrape target.
  • Dropped the AccountExists short-circuit from Improve EVM execution performance #4219, since main's Skip the redundant AccountExists probe in gigaSnapshotStateReader #4239 already covers it.

Scope and risk: Autobahn e2e gets the read, merge and receipt improvements but not the pipelined commit. On the pipelined path a block's state becomes visible later, and ResultSink runs once the commit has started rather than after it lands.

Results from evmonly-loadtest (pipelined path) on a 12-core M2 Pro, 0 failed txs, 0 OCC fallbacks:

  • Same workload as main: ~48.9k → ~54.3k TPS
  • With --accounts=200000 --blocks=0 --executor-workers=8 --gc-percent=400: ~73–77k TPS steady state

@cursor

cursor Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Core executor changes (overlay reads, async commits on the prepared-block path) affect state visibility timing; pipelined commits are confined to load-test wiring today, but shared merge/read/receipt paths run in production execution too.

Overview
Adds pipelined state commits for the load-test path: ExecutePreparedBlock starts CommitStateChanges in the background while the next block runs, reading the predecessor’s writes through a pending overlay; AwaitCommits and run shutdown surface commit failures. ExecuteBlock still waits for its commit, so synchronous callers (e.g. Autobahn) are unchanged on that API.

Execution performance on all store-backed paths: single-row ReadAccount on giga snapshots, OCC merge account prefetch via the worker pool (large blocks), and parallel receipt encoding above a threshold.

evmonly-loadtest gains --accounts (reusable sender pool), --blocks=0 streaming with parallel builders and a reorder buffer, --gc-percent, OTel block and pipeline phase timers (wired through SetupOtelPrometheus so storage/view metrics scrape), plus evmonly_loadtest_reorder_pending_blocks. Workloads seed the pool up front and assign senders/nonces deterministically per block height.

Monitoring: Prometheus scrape job for loadtest metrics on :9698 and a new EVMOnly Loadtest Grafana dashboard (throughput, executor wall time by phase/stage, pipeline upstream work, OCC, view cache, commit breakdown).

Tests cover overlay/pipeline equivalence, reorder bounds, account read coalescing, and pool sizing rules.

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

@github-actions

github-actions Bot commented Sep 24, 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 25, 2026, 11:51 PM

@codecov

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.41699% with 60 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.87%. Comparing base (9a3f882) to head (c97e110).

Files with missing lines Patch % Lines
giga/evmonly/cmd/evmonly-loadtest/pipeline.go 82.02% 16 Missing ⚠️
giga/evmonly/occ.go 87.83% 9 Missing ⚠️
giga/evmonly/cmd/evmonly-loadtest/main.go 0.00% 6 Missing ⚠️
...y/cmd/evmonly-loadtest/scenarios/erc20_transfer.go 60.00% 6 Missing ⚠️
giga/evmonly/giga_store.go 92.20% 6 Missing ⚠️
giga/evmonly/pipeline_overlay.go 93.82% 5 Missing ⚠️
giga/evmonly/receipt.go 93.15% 5 Missing ⚠️
.../evmonly/cmd/evmonly-loadtest/scenarios/helpers.go 80.95% 4 Missing ⚠️
...evmonly/cmd/evmonly-loadtest/scenarios/transfer.go 92.30% 1 Missing ⚠️
giga/evmonly/executor.go 93.75% 1 Missing ⚠️
... and 1 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4317      +/-   ##
==========================================
- Coverage   67.58%   66.87%   -0.72%     
==========================================
  Files        2163     2068      -95     
  Lines      167636   159076    -8560     
==========================================
- Hits       113299   106375    -6924     
+ Misses      54328    52692    -1636     
  Partials        9        9              
Flag Coverage Δ
sei-chain-pr 85.68% <88.16%> (?)
sei-db 74.50% <ø> (ø)
sei-db-state-db ?
sei-db-state-db-pr 83.79% <100.00%> (?)

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

Files with missing lines Coverage Δ
giga/evmonly/cmd/evmonly-loadtest/config.go 83.80% <100.00%> (+0.89%) ⬆️
giga/evmonly/cmd/evmonly-loadtest/metrics.go 74.14% <100.00%> (+0.35%) ⬆️
giga/evmonly/cmd/evmonly-loadtest/phases.go 100.00% <100.00%> (ø)
...ga/evmonly/cmd/evmonly-loadtest/scenarios/types.go 87.50% <100.00%> (+7.50%) ⬆️
giga/evmonly/memory_store.go 88.06% <100.00%> (+0.24%) ⬆️
giga/evmonly/phases.go 100.00% <100.00%> (ø)
giga/evmonly/types.go 90.00% <ø> (ø)
sei-db/state_db/sc/flatkv/state_view.go 82.78% <100.00%> (+1.70%) ⬆️
sei-db/state_db/sc/flatkv/store_write.go 81.57% <ø> (ø)
...evmonly/cmd/evmonly-loadtest/scenarios/transfer.go 86.44% <92.30%> (+0.16%) ⬆️
... and 10 more

... and 99 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.

The pipelined commit, the overlay, and the single-row account reads look correct. The ordering, the latched failure, and the read-pending-before-OpenView race are each covered by a focused test. The remaining notes are robustness gaps in Close and in the load-test streaming path. None of them blocks the merge.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • None at the file/PR level.
  • 3 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread giga/evmonly/executor.go
Comment thread giga/evmonly/cmd/evmonly-loadtest/pipeline.go
Comment thread giga/evmonly/cmd/evmonly-loadtest/pipeline.go

@cody-littley cody-littley 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.

LGTM. I only reviewed the DB stuff, probably good to have somebody with EVM knowledge review the execution stuff.

Comment on lines +95 to +99
type AccountReader interface {
// ReadAccount returns addr's row fields, or false when addr has no account. CodeHash is the
// empty-code hash for an account that holds no code, matching GetCodeHash.
ReadAccount(addr Address) (AccountSnapshot, bool)
}

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.

Personal preference. Would it be possible to just put this method into the EVMStateView interface? In general, I prefer to avoid creating lots of interfaces there is a specific reason.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, make sense to merge this into EVMStateView interface

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

Looks reasonable to me. Just the one style nit.

We will need to get some way to test the durability of in-flight but unlanded pipeline stages in presence of unexpected failure (disk error, evm panic) but I'm not sure how to induce those in a test environment or how it would impact the code.

timer := phases.Build()
defer timer.Reset()
for groupCtx.Err() == nil {
timer.SetPhase(phaseWaitingForWork)

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.

There is an inconsistency in calls to SetPhase where some are done via magic string and some are constants. And some strings are reused, some are not, and lastPhase is only read for metrics.

Calls to SetPhase should be consistent, and the quickest path to victory is string constants.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Make sense, will fix

Comment thread giga/evmonly/giga_store.go Outdated
// predates them, leaving nothing to supply them. Read first, the worst case is a view that
// already holds them and an overlay that replays the same values over the top.
pending := e.pipelinePending()
e.blockPhases.SetPhase("open_view")

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.

nit - can we standardize using constants for these phases? i.e.

  const (
      phaseExecute         = "execute"
      phaseOpenView        = "open_view"
      phaseAwaitCommit     = "await_commit"
      phaseWaitingForBlock = "waiting_for_block"
  )

maybe defined here

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.

there are a few other ones like encode_receipts

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call

@@ -0,0 +1,121 @@
package evmonly

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.

do you think it makes sense to add some more test cases i.e.:

  • pipeline execution == synchronous execution
  • a bunch of writes, then clear. we should not see any dirty entries

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Definitely worth it, will add more tests here to cover more edge cases

* main: (21 commits)
  Backport evmonly parse, app-hash and changeset perf fixes from giga-1 (#4345)
  Remove the oracle module behind a v6.8 upgrade (#4319)
  fix(seidb): report only the current migration boundary on the snapshot gauge (#4327)
  fix(flatkv): keep 10 old checkpoints instead of mirroring memIAVL's count (#4322)
  Backport Autobahn execute-loop and produced-tx metrics from giga-1 (#4330)
  Add giga.storage.receipts to toggle the Autobahn receipt store (#4333)
  optimize gather phase (#4326)
  Regenerate the Unreleased changelog as a plain PR list (#4336)
  Bump sei-protocol/go-ethereum to v1.15.7-sei-21 (#4332)
  Fail dynamic-gas precompile out-of-gas as an EVM out-of-gas call (#4318)
  Add dashboard and topology option for Autobahn e2e (#4167)
  Add Giga fetch/serve and BlockDB prune metrics (#4329)
  Add eth_getLogs to the EVM-only Giga RPC (#4308)
  Generate v6.8 precompiles (#4320)
  Add [giga] app.toml section and honor it on the Autobahn node (#4323)
  feat(evmonly): add eth_estimateGas via existing libraries (#4325)
  Use Pebble batch directly in SS (#4300)
  Fix pruning issue in SS causing huge disk spike (#4321)
  Make Autobahn always run the EVM-only executor, disable/remove some integration tests (#4316)
  reduce seal lock contention (#4314)
  ...
@yzang2019

Copy link
Copy Markdown
Contributor Author

Looks reasonable to me. Just the one style nit.

We will need to get some way to test the durability of in-flight but unlanded pipeline stages in presence of unexpected failure (disk error, evm panic) but I'm not sure how to induce those in a test environment or how it would impact the code.

A failed commit is remembered and returned by every later AwaitCommits. The load test calls that before reporting success, and TestFailedPipelinedCommitIsReportedToEveryWaiter covers it. Testing a disk error or an EVM panic in the middle of a commit is worth tracking separately. Note that this pipelining optimization is for evmonly app, the production code path is different

* main:
  Backport stale-nonce block continuation from giga-1 (#4238, #4229) (#4346)
@yzang2019

Copy link
Copy Markdown
Contributor Author

@seidroid review

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

All three findings from the previous review are fixed and covered by tests. The Close path now waits on storeMu, run-ahead in streamBlocks is capped at 2 * builders slots, and the account pool is seeded for bounded runs. One new gap remains: a block executed after Close fails at receipt encoding once it has 64 or more transactions, which breaks the promise in Close's own godoc.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • None at the file/PR level.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.
  • 1 non-blocking pre-existing issue(s) listed below under pre-existing issues.

Pre-existing issues

  • [suggestion] forwardPreparedBlocksInOrder in giga/evmonly/cmd/evmonly-loadtest/pipeline.go has no cap on its pending reorder map. If one prepare worker stalls, the others keep filling it. The new builder slot limit does not bound this downstream buffer. Streaming --blocks=0 runs are unbounded, so this buffer is more likely to grow there. Load-test only. (Raised by Codex.)

Comment thread giga/evmonly/receipt.go Outdated
// the same records in the same order as receiptRecords.
func (e *Executor) receiptRecordsParallel(ctx context.Context, blockNumber uint64, result *BlockResult) ([]receipt.ReceiptRecord, error) {
count := len(result.Receipts)
if e.occPool == nil || count < occParallelReceiptThreshold {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] This guard misses a closed pool. Executor.Close calls occPool.Close() but leaves e.occPool non-nil, so once Close returns, any store-backed block with a receipt store and ≥64 transactions reaches e.occPool.Run. That returns errOCCWorkerPoolClosed, and the block fails with encode receipts for block N. Close's godoc says such blocks "commit synchronously", and TestExecutorCloseDisablesOCC relies on blocks still running after Close. The shutdown tests use fewer than 64 transactions, so they do not catch this. Suggested fix: add e.closed.Load() to this condition, as useOCC does, or fall back to receiptRecords on errOCCWorkerPoolClosed. Add a test that executes a block of 64 or more transactions after Close with a receipt store. (Also raised by Codex.)

@yzang2019
yzang2019 added this pull request to the merge queue Sep 26, 2026
Merged via the queue into main with commit 2e0df46 Sep 26, 2026
61 checks passed
@yzang2019
yzang2019 deleted the yzang/backport-giga1-optimization branch September 26, 2026 00:31
revofusion pushed a commit to revofusion/sei-chain that referenced this pull request Sep 29, 2026
…tocol#4366)

The EVM-only executor on main can already pipeline commits
(`PrepareBlock`, `ExecutePreparedBlock`, `AwaitCommits`, from sei-protocol#4317),
but `evmOnlyApplication.FinalizeBlock` still calls the synchronous
`ExecuteBlock`. That call waits for the FlatKV commit of block N before
it returns, so the node can't start N+1 until the write lands. This PR
brings the application-layer half of sei-protocol#4270 over from `giga-1`
(PLT-1312), adapted to main's `evmOnlyState` / sender-cache layout
instead of the older cursor/executor split that sei-protocol#4270 assumes.

`FinalizeBlock` now runs `PrepareBlock` and then `ExecutePreparedBlock`
(in `executeBlockPipelined`), and returns once the state commit has
started. The next block still waits on the previous commit inside the
executor before it starts its own. Committed-state readers settle the
in-flight commit first. `EvmNonce`, `EvmBalance` and `EvmCode` go
through `openSettledView`, which calls `AwaitCommits` on the executor
published through a lock-free `settler`, so they never block on
`FinalizeBlock`. `currentExecutionContext`, used by `EvmCall` and
`EvmEstimateGas`, awaits while it holds `state`, so no new block can
start between the settle and the snapshot, and it returns a failed
commit as an error. `openSettledView` logs a failed commit once and
serves the last version that landed. Its return values have no error
channel, and the failure halts the node through the next
`FinalizeBlock`. `Proxy.AwaitCommits` exposes the settle, and
`nodeImpl.closeGigaStorage` calls it before `manager.Close()` so
shutdown never closes the store under an in-flight write.

Consensus and app hash are unchanged: the app hash is still computed
from the execution result, not the store write. The behaviour change is
that a failed store commit now surfaces one block later, from the next
`FinalizeBlock`, rather than from its own. Between `FinalizeBlock` and
`Commit`, `EvmNonce` and friends already see the finalized block's
state; before this PR they could not, because the write had already
landed by then anyway. New tests cover reads settling across four
pipelined blocks and a commit failure surfacing from the next block,
from `EvmCall` and from `AwaitCommits`, and a `-race` test that drives
`EvmNonce`/`EvmCall` from a goroutine while blocks are finalized and
committed, asserting the nonce never goes backwards. The test helpers
now settle before closing storage. I validated with `scripts/ramtest.sh
-race` over `evmonlyapp`, `proxy`, `node` and `giga/evmonly`, plus `make
fmtcheck` and `make lint`.

Link to Devin session:
https://app.devin.ai/sessions/d808db90106341278344623de10b92c0
Open in Devin Desktop:
https://app.devin.ai/desktop/session/d808db90106341278344623de10b92c0?variant=devin
Requested by: @bdchatham

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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.

4 participants