-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
fix: add cache to snapshot and re-cache irregular states #4819
fix: add cache to snapshot and re-cache irregular states #4819
Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
@@ -948,6 +944,7 @@ impl<LoggerErrorT: Debug> ProviderData<LoggerErrorT> { | |||
|
|||
let snapshot = Snapshot { | |||
block_number: self.blockchain.last_block_number(), | |||
block_state_cache: self.block_state_cache.clone(), |
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.
Add the cache to the snapshot to ensure that people can revert to previous snapshots (excluding newer cache entries).
// We cannot use `LruCache::try_get_or_insert`, because it needs &mut self, but | ||
// we would need &self in the callback to construct the context. | ||
if !self.block_context_cache.contains(&block_number) { | ||
let block = if let Some(block_spec) = block_spec { |
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 had to collapse the two functions into one to make the borrow checker happy with the Cow<'state, _>
's 'state
lifetime
crates/edr_provider/src/data.rs
Outdated
let block_number = block_header.number; | ||
|
||
// Avoid caching of states that may contain state overrides | ||
let can_cache = self.irregular_state.state_overrides().is_empty(); |
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.
The original caching strategy wasn't taking into consideration the possibility of having irregular state changes to a block. If you made such changes, it would still return previously cached versions of the block.
My approach is too conservative and results in performance degradation for certain tests. I didn't have time before end-of-day to make this criteria more flexible though.
This function shows how irregular state overrides affect the construction of a block.
There are 3 approaches to consider and experiment with:
-
Update the cached entry when an irregular state change happens
-
Instead of just caching per block number, we can also incorporate the irregular state overrides that would impact the requested block number (everything up until this block number). I'm not sure how expensive it would be to validate this; e.g. using a hash; but you could experiment with this.
-
Even if we never cache irregular state, we can make this condition less strict by looking whether there are any state overrides up until the requested block number.
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.
Thanks for the detailed explanation! I think something else might be going on here as well regarding the performance degradation. Running the Rocketpool scenario on my machine is almost 10x slower with this change than in the base branch, and Rocketpool doesn't use any hardhat_*
methods which as I understand is the only way to set irregular state.
I created a flame graph using the Rockepool scenario on which two things stand out:
- Most of the time is spent in
compute_state_at_block
- Dropping a snapshot takes significant time
I cherry picked this commit in the edr/save-scenarios
branch so you can run the Rocketpool scenario yourself (after downloading it from the shared Google Drive) from the crates/tools
directory:
cargo run --release scenario ../../benchmark-scenarios/rocketpool-6a9dbfd8/06549782-8dc5-4700-bdde-9457cdd7b388
And to generate a flame graph:
cargo install flamegraph
CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph --release -- scenario ../../benchmark-scenarios/rocketpool-6a9dbfd8/06549782-8dc5-4700-bdde-9457cdd7b388
Make sure to ignore the part of the flame graph that loads the scenario files. This is easily identifiable as it uses rayon, not tokio.
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 think something else might be going on here as well regarding the performance degradation. Running the Rocketpool scenario on my machine is almost 10x slower with this change than in the base branch, and Rocketpool doesn't use any hardhat_* methods which as I understand is the only way to set irregular state.
I'd need to look at the specific scenarios, but when forking a remote network with a configuration that has "genesis accounts", you're always creating an irregular state override for those, which is applied to that first block.
Additionally, we're always at least calculating the block state's once, even when using latest. This can be avoided by caching when we change the state.
I'm having a look at the scenario you highlighted to see whether either of these is the case and make the caching more aggressive.
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 made two optimisations:
- I ensure that we always cache changes to a block, such that we can always use the latest version of the requested block number. This is the same behaviour as generating a state using `state_at_block_number.
- I use a custom global allocator that retains memory pages such that we don't always have to go through the system allocator.
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.
Thanks for jumping on this @Wodann I left some comments, but looks like the right direction.
crates/edr_provider/src/data.rs
Outdated
let contextual_state = if can_cache && self.block_state_cache.contains(&block_number) { | ||
// We cannot use `LruCache::try_get_or_insert`, because it needs &mut self, but | ||
// we would need &self in the callback to construct the context. | ||
Cow::Borrowed(self.block_state_cache.get(&block_number).expect( |
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 think the expect invariant doesn't hold anymore as the push is after the get in this change
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.
There is a contains
check (previously it was the inverse) which guarantees that the get will succeed.
crates/edr_provider/src/data.rs
Outdated
let block_number = block_header.number; | ||
|
||
// Avoid caching of states that may contain state overrides | ||
let can_cache = self.irregular_state.state_overrides().is_empty(); |
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.
Thanks for the detailed explanation! I think something else might be going on here as well regarding the performance degradation. Running the Rocketpool scenario on my machine is almost 10x slower with this change than in the base branch, and Rocketpool doesn't use any hardhat_*
methods which as I understand is the only way to set irregular state.
I created a flame graph using the Rockepool scenario on which two things stand out:
- Most of the time is spent in
compute_state_at_block
- Dropping a snapshot takes significant time
I cherry picked this commit in the edr/save-scenarios
branch so you can run the Rocketpool scenario yourself (after downloading it from the shared Google Drive) from the crates/tools
directory:
cargo run --release scenario ../../benchmark-scenarios/rocketpool-6a9dbfd8/06549782-8dc5-4700-bdde-9457cdd7b388
And to generate a flame graph:
cargo install flamegraph
CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph --release -- scenario ../../benchmark-scenarios/rocketpool-6a9dbfd8/06549782-8dc5-4700-bdde-9457cdd7b388
Make sure to ignore the part of the flame graph that loads the scenario files. This is easily identifiable as it uses rayon, not tokio.
self.irregular_state | ||
.state_override_at_block_number(block_number) | ||
.or_insert_with(|| StateOverride::with_state_root(state_root)) | ||
.diff | ||
.apply_account_change(address, account_info.clone()); | ||
|
||
self.mem_pool.update(&self.state)?; | ||
self.block_state_cache.push(block_number, state.clone()); | ||
self.state = state; |
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.
This ensures that the state is not modified when there are previous failures in fn state_root
or fn update
calls.
This occurs at the cost of an extra clone. Once we adopt an immutable Merkle-Patricia trie, this cost should become negligible.
Thanks @Wodann , it all looks good, but there is a segfault in the MacOS build: https://github.com/NomicFoundation/hardhat/actions/runs/7790363699/job/21244112219 It's probably due to the custom allocator. I'll cherry pick your commits to the base branch and I'll try to remove the cache from the snapshot as that seems to be the biggest performance hit now. |
5eb5b62
to
b21b1ec
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.
Thanks for removing the custom allocator!
28064d8
into
edr/fix/cache-block-contexts
No description provided.