Skip to content

perf(evmonly): let parse workers claim their own ranges - #4234

Merged
bdchatham merged 5 commits into
giga-1from
brandon/serial-1-parse-workers
Sep 19, 2026
Merged

bdchatham merged 5 commits into
giga-1from
brandon/serial-1-parse-workers

Conversation

@bdchatham

@bdchatham bdchatham commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

One of four independent PRs from the serial-budget survey (#4234, #4235, #4236, #4237); each targets giga-1 on its own.

parseBlockTxs fed indices to its workers through an unbuffered channel, so a block's 1,848 transactions cost 1,848 rendezvous on a stage the execute loop waits for. A block carries a sender for nearly every transaction, so the work behind each handoff is a decode rather than a signature recovery — and the handoff cost more than the parse. The parallel path ran slower than the serial path it was meant to beat.

Workers now claim ranges off an atomic counter.

Measured

Measured locally with a throwaway BenchmarkParseBlockTxs (not included in this PR). 1,848 transactions each with a known sender, which is the shape the node path builds:

workers before after
1 768 µs 808 µs serial path, untouched
8 1,610 µs 355 µs 4.5×
32 1,516 µs 332 µs 4.6×

Validators set ParseWorkers: runtime.GOMAXPROCS(0) (sei-tendermint/internal/evmonlyapp/app.go:119), so on a 32-vCPU validator the 32-worker row is the live one: 1.25 ms off a 28.83 ms loop, 679 ns per transaction, ~4.3% of loop wall clock.

This is the largest win in the stack.

Correctness

parsed[i] is written by index, so output order never depended on dispatch order. Cancellation is still checked per claim. TestParseBlockTxsMixesKnownAndRecoveredSenders covers the mixed known/recovered path; -race clean over repeated runs.

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

parseBlockTxs fed indices to its workers through an unbuffered channel, so a
block's 1,848 transactions cost 1,848 rendezvous on a stage the execute loop
waits for. A block carries a sender for nearly every transaction, so the work
behind each handoff is a decode rather than a signature recovery, and the
handoff cost more than the parse: the parallel path ran slower than the serial
one it was meant to beat.

Workers now claim ranges off an atomic counter. Measured on a 1,848-tx block
with a sender for every transaction, which is the shape the node path builds:

    workers=1    768 us -> 808 us   (serial path, unchanged)
    workers=8  1,610 us -> 355 us   4.5x
    workers=32 1,516 us -> 332 us   4.6x

Validators run ParseWorkers at GOMAXPROCS, so the 32-worker figure is the live
one: 1.25 ms off a 28.83 ms loop, or 679 ns per transaction.

Output is written by index, so order never depended on dispatch order.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

PR Summary

Low Risk
Localized scheduling change in block tx parsing; behavior and ordering stay index-based with existing tests plus a multi-claim case.

Overview
Replaces per-transaction channel handoffs in parallel parseBlockTxs with atomic range claiming so workers grab chunks of 16 indices at a time instead of rendezvousing on an unbuffered job channel for every tx.

The serial path (workers <= 1 or a single tx) is unchanged. Parallel workers still write parsed[i] by index and honor groupCtx cancellation between claims. A new test runs 40 txs with 4 workers to cover multiple claims beyond one batch.

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

@github-actions

github-actions Bot commented Sep 17, 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 17, 2026, 1:23 PM

@codecov

codecov Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 65.55%. Comparing base (7e3ca24) to head (ba31dbb).
⚠️ Report is 26 commits behind head on giga-1.

Files with missing lines Patch % Lines
giga/evmonly/parser.go 91.66% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##           giga-1    #4234    +/-   ##
========================================
  Coverage   65.55%   65.55%            
========================================
  Files        2081     2078     -3     
  Lines      157460   157237   -223     
========================================
- Hits       103222   103078   -144     
+ Misses      54097    54018    -79     
  Partials      141      141            
Flag Coverage Δ
sei-chain-pr 89.69% <91.66%> (?)
sei-db ?

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

Files with missing lines Coverage Δ
giga/evmonly/parser.go 95.91% <91.66%> (+0.26%) ⬆️

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

Replacing the per-transaction job channel with atomic range claiming is correct — output stays index-addressed, cancellation is still checked before each claim is parsed, and the counter is bounded because each worker exits on its first out-of-range claim. The one gap is test coverage: at parseClaim = 16 the existing 8-transaction parallel test now runs entirely inside a single worker's first claim, so multi-claim and partial-tail behavior is unverified.

Findings: 0 blocking | 1 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.

Comment thread giga/evmonly/parser.go
bdchatham and others added 4 commits September 17, 2026 12:44
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@bdchatham
bdchatham added this pull request to the merge queue Sep 19, 2026
Merged via the queue into giga-1 with commit 85706b8 Sep 19, 2026
63 checks passed
@bdchatham
bdchatham deleted the brandon/serial-1-parse-workers branch September 19, 2026 11:52
revofusion pushed a commit to revofusion/sei-chain that referenced this pull request Sep 26, 2026
…sei-protocol#4345)

Three small hot-loop optimisations from the evmonly serial-budget survey
landed on `giga-1` as sei-protocol#4234, sei-protocol#4236 and sei-protocol#4237 but were never brought to
`main` (PLT-1302, PLT-1304, PLT-1305 under PLT-1297). All three
cherry-pick cleanly; this PR carries them as-is, one commit each.

`parseBlockTxs` fed indices to its workers over an unbuffered channel,
so with a known sender on nearly every transaction the rendezvous cost
more than the decode and the parallel path ran slower than the serial
one; workers now claim ranges off an atomic counter (about 4.5x faster
at 8-32 workers). `hashEVMOnlyResult` wrote the app-hash stream to
sha256 a few bytes at a time with a fresh length prefix per write; it
now goes through a 32 KiB buffer with a scratch prefix, cutting roughly
7,000 allocations per block. The FlatKV changeset encoder allocated a
`KVPair`, a key and a value per emitted pair; it now cuts all three from
one slab per kind, the way `EncodeMemoryStoreChangeSet` already does,
cutting roughly 10,500 allocations per block.

None of this changes bytes on the wire or in state:
`TestHashEVMOnlyResultGoldenVector` and
`TestEncodeFlatKVChangeSetGolden*` pin the hashed stream and the emitted
pairs to values captured from the previous implementations, and
`parsed[i]` was always written by index so parse order never mattered.
Validated with `go test -race ./giga/evmonly/
./sei-tendermint/internal/evmonlyapp/` on this branch.

---------

Co-authored-by: FromTheRain <bdchatham@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant