Uh oh!
There was an error while loading. Please reload this page.
perf(block): use sync/atomic instead of mutexes - #2735
Conversation
Summary of ChangesHello @julienrbrt, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a performance enhancement by refactoring critical state management within the block execution and submission components. It replaces traditional mutex-based synchronization with Go's Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
The latest Buf updates on your PR. Results from workflow CI and Release / buf-check (pull_request).
|
Code Review: Performance Optimization using |
There was a problem hiding this comment.
Code Review
This pull request is a performance optimization that replaces sync.RWMutex with sync/atomic for managing state in Executor and Submitter. The changes are well-implemented and correctly use atomic operations for uint64 and struct pointers. I have one suggestion for Executor.GetLastState to improve thread safety by preventing potential data races that could occur if callers modify the returned state object. This is a pre-existing condition, but worth addressing during this refactoring.
Uh oh!
There was an error while loading. Please reload this page.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@## main #2735 +/- ##
==========================================
- Coverage 62.28% 62.17% -0.12%
==========================================
Files 79 79 Lines 8490 8497 +7 ==========================================
- Hits 5288 5283 -5 - Misses 2711 2721 +10 - Partials 491 493 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Code Review - PR #2735: Use |
PR Review: perf(block): use |
Code Review: Performance Optimization with sync/atomicSummaryThis PR replaces mutex-based synchronization with ✅ Strengths
|
Code Review: Performance Optimization with |
Code Review: Performance Optimization with sync/atomicSummaryThis PR replaces mutex-based synchronization with 🚨 Critical Issues1. Data Race in |
Code Review: perf(block): use |
benchmark.go// BenchmarkGetLastState measures the performance of reading statefuncBenchmarkGetLastState(b*testing.B) {
state:= types.State{
Version: types.Version{Block: 1, App: 1},
LastBlockHeight: 100,
AppHash: []byte("apphash1234567890"),
LastResultsHash: []byte("results1234567890"),
}
e:=&Executor{
lastState: &atomic.Pointer[types.State]{},
}
e.lastState.Store(&state)
b.ResetTimer()
b.RunParallel(func(pb*testing.PB) {
forpb.Next() {
_=e.GetLastState()
}
})
}
// BenchmarkStateReadWrite_Mixed benchmarks mixed read/write workload (90% reads, 10% writes)funcBenchmarkStateReadWrite_Mixed(b*testing.B) {
state:= types.State{
Version: types.Version{Block: 1, App: 1},
LastBlockHeight: 100,
AppHash: []byte("apphash1234567890"),
LastResultsHash: []byte("results1234567890"),
}
e:=&Executor{
lastState: &atomic.Pointer[types.State]{},
}
e.lastState.Store(&state)
b.ResetTimer()
b.RunParallel(func(pb*testing.PB) {
i:=0forpb.Next() {
ifi%10==0 {
// 10% writesnewState:=statenewState.LastBlockHeight=uint64(i)
e.SetLastState(newState)
} else {
// 90% reads_=e.GetLastState()
}
i++
}
})
}// BenchmarkGetLastState_WithMutex compares against mutex-based implementationfuncBenchmarkGetLastState_WithMutex(b*testing.B) {
state:= types.State{
Version: types.Version{Block: 1, App: 1},
LastBlockHeight: 100,
AppHash: []byte("apphash1234567890"),
LastResultsHash: []byte("results1234567890"),
}
typemutexExecutorstruct {
lastState types.StatelastStateMtx sync.RWMutex
}
e:=&mutexExecutor{lastState: state}
b.ResetTimer()
b.RunParallel(func(pb*testing.PB) {
forpb.Next() {
e.lastStateMtx.RLock()
s:=e.lastStatee.lastStateMtx.RUnlock()
_=s
}
})
}
// BenchmarkStateReadWrite_Mixed_WithMutex compares mixed workload against mutexfuncBenchmarkStateReadWrite_Mixed_WithMutex(b*testing.B) {
state:= types.State{
Version: types.Version{Block: 1, App: 1},
LastBlockHeight: 100,
AppHash: []byte("apphash1234567890"),
LastResultsHash: []byte("results1234567890"),
}
typemutexExecutorstruct {
lastState types.StatelastStateMtx sync.RWMutex
}
e:=&mutexExecutor{lastState: state}
b.ResetTimer()
b.RunParallel(func(pb*testing.PB) {
i:=0forpb.Next() {
ifi%10==0 {
// 10% writese.lastStateMtx.Lock()
e.lastState.LastBlockHeight=uint64(i)
e.lastStateMtx.Unlock()
} else {
// 90% readse.lastStateMtx.RLock()
_=e.lastStatee.lastStateMtx.RUnlock()
}
i++
}
})
}goos: linux
goarch: arm64
pkg: github.com/evstack/ev-node/block/internal/executingBenchmarkGetLastState-127143047118.99ns/op48B/op2allocs/opPASSokgithub.com/evstack/ev-node/block/internal/executing1.384s
goos: linux
goarch: arm64
pkg: github.com/evstack/ev-node/block/internal/executingBenchmarkStateReadWrite_Mixed-124077986725.97ns/op56B/op1allocs/opPASSokgithub.com/evstack/ev-node/block/internal/executing1.109s
goos: linux
goarch: arm64
pkg: github.com/evstack/ev-node/block/internal/executingBenchmarkGetLastState_WithMutex-128119104150.4ns/op0B/op0allocs/opPASSokgithub.com/evstack/ev-node/block/internal/executing1.381s
goos: linux
goarch: arm64
pkg: github.com/evstack/ev-node/block/internal/executingBenchmarkStateReadWrite_Mixed_WithMutex-124934558279.34ns/op0B/op0allocs/opPASSokgithub.com/evstack/ev-node/block/internal/executing3.952sAllocations are due to the deep copy, suggested by Claude. I personally think we can remove it, we do not mutate current state. We create a new one each block with Removing it gives as well obviously an even better result: goos: linux
goarch: arm64
pkg: github.com/evstack/ev-node/block/internal/executingBenchmarkGetLastState-1210000000000.05766ns/op0B/op0allocs/opPASSokgithub.com/evstack/ev-node/block/internal/executing0.078s
goos: linux
goarch: arm64
pkg: github.com/evstack/ev-node/block/internal/executingBenchmarkStateReadWrite_Mixed-121307896579.246ns/op12B/op0allocs/opPASSokgithub.com/evstack/ev-node/block/internal/executing2.150s |
alpe
commented
Oct 6, 2025
Thanks for adding some benchmark results! There is a significant difference without the deep copy. When I looked at the code, there is no mutation indeed but with public methods this is not guaranteed to be true in the future. The |
Code Review: Performance Optimization with sync/atomicThank you for this performance optimization PR! The migration from mutexes to ✅ Strengths
🔴 Critical Issues1. Data Race in GetLastState() - BLOCKINGLocation: The current implementation has a critical data race: func (e*Executor) GetLastState() types.State {
state:=e.lastState.Load()
ifstate==nil {
return types.State{}
}
stateCopy:=*state// Race condition here!stateCopy.AppHash=bytes.Clone(state.AppHash)
stateCopy.LastResultsHash=bytes.Clone(state.LastResultsHash)
returnstateCopy
}Problem: While the pointer load is atomic, dereferencing ( Why bytes.Clone doesn't help: Cloning happens after the struct copy, so the race already occurred when reading the slice pointers from the original struct. Solution: Store the entire state including cloned slices: func (e*Executor) SetLastState(state types.State) {
stateCopy:= types.State{
Version: state.Version,
ChainID: state.ChainID,
InitialHeight: state.InitialHeight,
LastBlockHeight: state.LastBlockHeight,
LastBlockTime: state.LastBlockTime,
DAHeight: state.DAHeight,
AppHash: bytes.Clone(state.AppHash),
LastResultsHash: bytes.Clone(state.LastResultsHash),
}
e.lastState.Store(&stateCopy)
}
func (e*Executor) GetLastState() types.State {
state:=e.lastState.Load()
ifstate==nil {
return types.State{}
}
return*state// Safe now - stored state already has cloned slices
}This ensures the stored pointer always points to an immutable, fully independent copy. 2. Missing time.Time handling
|
Code Review: PR #2735 - Use |
Uh oh!
There was an error while loading. Please reload this page.
* main: feat(store)!: add batching for atomicity (#2746) refactor(apps): rollback cmd updates (#2744) chore: add makefile for tools (#2743) chore: fix markdown lint (#2742) build(deps): Bump the all-go group across 5 directories with 6 updates (#2738) refactor(block): improve cancellation (#2741) chore: make the prompt go oriented (#2739) perf(block): use `sync/atomic` instead of mutexes (#2735)
Overview
Use sync/atomic` instead of mutexes.