db/state: give CommitmentDomain its own lock in TemporalMemBatch - #23137
db/state: give CommitmentDomain its own lock in TemporalMemBatch#23137sudeepdino008 wants to merge 6 commits into
Conversation
committedStorage is a write-once, immutable pre-block view (like committedAccounts,
already a sync.Map) but sat behind the shared BlockStateCache.mu: read under RLock
and filled under a full Lock on every SLOAD first-touch, contending with the
exec-loop's writeLog appends. Convert it to a sync.Map keyed by {addr,key}.
GetCurrentStorage keeps the committed read under RLock so current+committed stay
an atomic snapshot.
benchstat (16-core, n=8), GetCommittedStorage/PutCommittedStorage:
read_warm-16 47.8n -> 3.6n -92%
fill_read-16 48.7n -> 2.8n -94%
Uncontended (1 cpu) is ~25-50% slower (sync.Map overhead), but this cache is only
used by the parallel executor, i.e. always under 8-16 way concurrency.
…urrentAccount Release c.mu before the committed sync.Map read, matching GetCurrentAccount. committedStorage is a write-once immutable pre-block view, so the two reads need not be one atomic snapshot.
TemporalMemBatch (sd.mem's latest-state layer) guarded every domain under one latestStateLock. The commitment calculator writes CommitmentDomain branches during the fold on its own goroutine, contending that lock with the exec workers' state reads (getLatest on Accounts/Storage/Code). Give CommitmentDomain its own lock: state ops keep latestStateLock, commitment ops use commitmentLock, and multi-domain ops (Flush/Unwind) take both in a fixed order (state before commitment) so there is no deadlock. Mutex profile, live chaintip mainnet — sd.mem latestStateLock contention: getLatestMetered (state reads) 16.3s -> 0.34s putLatest (writes) 13.5s -> 0.21s ~98% less sd.mem lock contention. End-to-end gas/s is unchanged (tip throughput is gated by a separate dispatch bottleneck) — this removes a latent scaling wall. -race coverage added for concurrent commitment-write vs state-read and the both-lock Unwind path.
d63896f to
f411f73
Compare
…ommitment-lock-split # Conflicts: # execution/state/rw_v3.go
There was a problem hiding this comment.
Pull request overview
This PR reduces contention in the in-RAM “latest state” layers used by parallel execution by (1) splitting TemporalMemBatch locking so CommitmentDomain no longer contends with hot state-domain reads/writes, and (2) switching BlockStateCache’s committed storage cache to a lock-free sync.Map keyed by (addr, slot).
Changes:
db/state: introduce a dedicatedcommitmentLockforkv.CommitmentDomain, and make multi-domain operations (Flush/Unwind) take both locks in a fixed order.execution/state: changeBlockStateCache.committedStoragetosync.Mapwith a composite key, and adjust read paths to avoidmuon committed fallback.- Add concurrency/race-focused tests for the new locking behavior and committed-storage cache semantics.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
db/state/temporal_mem_batch.go |
Splits locking: CommitmentDomain uses commitmentLock; Flush/Unwind take both locks in order. |
db/state/temporal_mem_batch_test.go |
Adds -race-oriented concurrency test covering commitment writes vs state reads and both-lock Unwind. |
execution/state/rw_v3.go |
Converts committed storage cache to sync.Map keyed by (addr, key) and adjusts committed fallback ordering. |
execution/state/block_cache_committed_storage_test.go |
Adds benchmarks and tests for committed-storage semantics and concurrent access. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (1)
db/state/temporal_mem_batch_test.go:66
- This workload also passes if every domain uses the former single mutex, so it does not guard the PR's core lock-separation behavior. Add a deterministic assertion that the commitment and state domains resolve to different lock instances; the concurrent loop can continue covering races and deadlocks.
const nKeys = 512
|
Review notes. Grouped by what I think blocks vs. what is cleanup. The split may not buy what it claimsFalse sharing between the two locks. Measured on this branch:
Locking correctness
The test does not test the split
Two more gaps:
Design
Smaller things
|
TemporalMemBatch(the in-RAM latest-state layer behindsd.mem) guarded every domain under a singlelatestStateLock. The parallel commitment calculator writesCommitmentDomainbranches during the fold — on its own goroutine — so those writes contend that one lock with the exec workers' state reads (getLateston Accounts/Storage/Code).This gives
CommitmentDomainits own lock:latestStateLock,commitmentLock,Flush/Unwind) take both, in a fixed order (state before commitment) so there is no deadlock.The domains are already stored in separate maps (
domains[kv.DomainLen]), so this only splits the lock — no data restructuring.Numbers (matched-window)
Mutex "delay" profile over an identical 300-block window on each binary (live chaintip, mainnet, on top of #23134):
getLatestMetered(state reads)A measured, low-risk reduction in
sd.memlock contention by separating the commitment writer off the state lock. Single matched sample, so treat the exact percentage as approximate.Scope, stated honestly: this reduces lock contention, not read wall-time — the
sd.memstate read is dominated by the domain btree traversal, not the lock, so end-to-end gas/s is unchanged (tip throughput is gated by a separate dispatch bottleneck). It's lock hygiene that also helps as core count / pipeline depth grows (the contention is larger in batch/initial-sync).Tests
-racecoverage added for concurrent commitment-write vs state-read plus the both-lockUnwindpath (deadlock-freedom). Thedb/stateandexecution/stagedsyncsuites pass under-race.