-
Notifications
You must be signed in to change notification settings - Fork 843
perf: Share proposal between hash and commit #4697
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
6b31642 to
9ab772f
Compare
201573a to
e7a87c3
Compare
e7a87c3 to
1395fce
Compare
e7ee927 to
51e8ad4
Compare
1395fce to
de5a6aa
Compare
de5a6aa to
9961f98
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR refactors the Firewood database integration to improve proposal management and enable a ~30% performance improvement for bootstrapping nodes. The key optimization is sharing proposals between hashing and commit operations, eliminating redundant proposal creation. Additionally, the refactoring includes better recovery handling through persistent storage of block hashes and heights.
Key changes:
- Proposals are now created during the
Hash()operation and reused duringCommit(), rather than being created and dropped during hashing - Recovery information (committed block hashes and heights) is now persisted to disk
- Configuration structure and naming conventions have been improved for clarity
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| graft/evm/firewood/triedb.go | Major refactoring to track proposals created during hashing for later commit, added recovery state persistence, renamed config fields |
| graft/evm/firewood/account_trie.go | Updated to use new proposal creation API, improved documentation |
| graft/evm/firewood/storage_trie.go | Enhanced documentation for storage trie behavior |
| graft/evm/firewood/recovery.go | New file implementing recovery functions for persisting/reading committed block hashes and heights |
| graft/evm/firewood/hash_test.go | Updated to use new config API |
| graft/subnet-evm/core/genesis.go | Added Firewood-specific handling for empty genesis blocks, added helper function |
| graft/subnet-evm/core/genesis_test.go | Updated to use new config API |
| graft/subnet-evm/core/blockchain.go | Updated config field names to match new API |
| graft/subnet-evm/tests/state_test_util.go | Updated to use new config API |
| graft/coreth/core/genesis.go | Added Firewood-specific handling for empty genesis blocks, added helper function |
| graft/coreth/core/genesis_test.go | Updated to use new config API |
| graft/coreth/core/blockchain.go | Updated config field names to match new API |
| graft/coreth/tests/state_test_util.go | Updated to use new config API |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left a few comments - you definitely know this code better than I do
|
Hey Austin Found a bug while running C-Chain re-execution benchmarks to measure the performance impact of your proposal-sharing changes. The PR CI passes because it runs with When running with The proposal-sharing logic expects To reproduce: To save you time digging through git history: Let me know if you need help reproducing or testing a fix. |
This isn't exactly a bug... I added specific recovery logic in this PR. On This PR makes it safer by tracking the most recently committed height/block hash in leveldb, so upon recovery, we can properly populate these fields in the proposal trie. However, your snapshot does not have that, and I didn't realize this would be a breaking change. Now that you bring this up, I do think this will also be an issue for statesync as well. Let me rework a few things to see if I can find a less fragile way of crash recovery |
|
After some investigation, I've discovered a couple ideas:
I'm leaning towards option 3. |
I see the issue. It does seem like Option 3 is cleanest. BlockChain already knows the committed height/hash after loadLastState(), no need to duplicate it in leveldb (and risk a two-phase commit race). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I profiled master on C-Chain re-execution (blocks 10M-10.05M):
Commit: 18.5% of CPU
(*Database).Propose: 1.76% of CPU
The proposed optimization removes one of two proposal creations, so max improvement is ~0.9%.
Where did the 30% improvement come from in your testing? Different workload? Or maybe the win is in memory, not CPU?
Just want to understand where to look.
To reproduce download pprof artifacts: https://github.com/ava-labs/avalanchego/actions/runs/20661268516/artifacts/5036934243
Run: go tool pprof -top pprof-profiles/cpu.prof | grep -E "(firewood|ffi.*Propose)"
Edit: Re-ran blocks 1 to 10M and got 28% improvement nice!
On early blocks (*Database).Propose is ~6% of CPU, so cutting that in half gives real gains. On 10M+ blocks it's only ~1.7% since reads dominate at larger state sizes. So the optimization is solid for genesis sync only.
I made a draft showing how number 3 could be implemented: #4835. It's definitely messier, but wouldn't break state. However, I do think that we will be making a breaking change to firewood state before the v0.1.0 release |
|
I'm not sure about test coverage for this -- I would assume this refactor would introduce new things to test -- some ideas include -- like if |
|
These tests are not complete, but I do have an outstanding issue for guaranteeing minimum behavior. This just ensure that the basics are laced together correctly, and the specific multiple hashing thing you suggested |
fire - they can be built upon!! |
| // TrieDB is a triedb.DBOverride implementation backed by Firewood. | ||
| // It acts as a HashDB for backwards compatibility with most of the blockchain code. | ||
| type TrieDB struct { | ||
| proposals |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| proposals | |
| proposalTree proposals |
Reasoning: https://github.com/uber-go/guide/blob/master/style.md#avoid-embedding-types-in-public-structs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It says to avoid embedded public fields specifically. Does it make any difference since it's private?
| } else { | ||
| if t, ok := triedb.Backend().(*firewood.TrieDB); ok { | ||
| t.SetHashAndHeight(block.Hash(), 0) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused, why do we call this here only if the root is empty? Shouldn't we call this nevertheless?
Why this should be merged
This is about a 30% performance improvement for bootstrapping nodes.
How this works
See ava-labs/libevm#240 for comments from an initial review. All proposals created at hash time are stored in a map, and some shutdown recovery is added for block hashes and heights.
How this was tested
UT and re-execution of state.
Need to be documented in RELEASES.md?
No