Skip to content
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

Preload state trie node #4737

Merged
merged 19 commits into from
Dec 1, 2022
Merged

Conversation

matkt
Copy link
Contributor

@matkt matkt commented Nov 25, 2022

Signed-off-by: Karim TAAM [email protected]
Co-authored-by: Ameziane H [email protected]

PR description

The idea behind this PR is to preload asynchronously account nodes and storage nodes from the database during the transaction processing to use these nodes during the calculate root hash step.
We've created two caches, one for account nodes and one for storage nodes. The size of these caches is 100k for accounts and 200k for storage. We've tested other values but this configuration is the one that works better.
We also exporter cache metrics as Prometheus metrics to check cache efficiency.

We didn't see any impact on GC activity even on 4 GiB Heaps (-Xmx4g).

The results

We did our tests on different AWS EC instances, here're the results.

M6a.xlarge (4 vCPU, 16 GiB)
Block processing time is 34% better for median values and 41% better for 95th percentile.

image

M5.xlarge (4 vCPU, 16 GiB)
Block processing time is 28% better for median values and 95th percentile.

image

I3.2xlarge (8 vCPU, 61 GiB)
Block processing time is 21% better for median values and 95th percentile.

image

Cache Efficiency
We can see in the screenshots below that these two caches are very efficient (>99%) and increasing storage cache size more than 200k is not necessary.

Account cache size = 100k and Storage cache size = 200k
image

Account cache size = 100k and Storage cache size = 1 million
image

Fixed Issue(s)

Documentation

  • I thought about documentation and added the doc-change-required label to this PR if
    updates are required.

Changelog

Signed-off-by: Karim TAAM <[email protected]>
Signed-off-by: Karim TAAM <[email protected]>
@matkt matkt changed the title [POW-PERF] Preload state trie node [POC-PERF] Preload state trie node Nov 25, 2022
@matkt matkt changed the title [POC-PERF] Preload state trie node Preload state trie node Nov 25, 2022
@matkt matkt changed the title Preload state trie node [WORK IN PROGRESS] Preload state trie node Nov 25, 2022
Signed-off-by: Karim TAAM <[email protected]>
private final Cache<Bytes, Bytes> storageNodes;

public OptimizedMerkleTrieLoader(final ObservableMetricsSystem metricsSystem) {
accountsNodes = CacheBuilder.newBuilder().recordStats().maximumSize(ACCOUNT_CACHE_SIZE).build();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid running complicated logic in a constructor. Maybe extract it into a factory method.

final BonsaiWorldStateKeyValueStorage worldStateStorage,
final Hash worldStateRootHash,
final Address account) {
CompletableFuture.runAsync(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels to me weird that nobody handles the future when it is done. Maybe you can have the Optional that you were preloading to be a return value of the future and when the future completes, update your accountsNodes. That would also prevent the need of modifying a field in the middle of a function. Functions in functional programming should preferably not have side effects, so this might help.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, the idea was to execute asynchronously as much work as possible to fill the cache. These completable futures doesn't return any result, but I totally agree that it should be more clean to control the execution of the completable future.
This is a first version, and this can be improved in a future PR. The control of completable futures' executions will introduce a lot of changes that can be done in the future PR. We can for example stop all the futures when we start reading from the cache, this is not done in this current PR.

@matkt matkt force-pushed the feature/cached-state-node branch 2 times, most recently from 0315742 to 1562550 Compare November 30, 2022 16:18
@matkt matkt force-pushed the feature/cached-state-node branch 2 times, most recently from 3b97e76 to 24b47be Compare November 30, 2022 16:52
Signed-off-by: Karim TAAM <[email protected]>
Signed-off-by: Karim TAAM <[email protected]>
@matkt matkt marked this pull request as ready for review November 30, 2022 20:50
@matkt matkt changed the title [WORK IN PROGRESS] Preload state trie node Preload state trie node Nov 30, 2022
Copy link
Contributor

@ahamlat ahamlat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Contributor

@gezero gezero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@matkt matkt added the TeamChupa GH issues worked on by Chupacabara Team label Dec 1, 2022
@@ -116,7 +126,10 @@ protected Hash calculateRootHash(
// next walk the account trie
final StoredMerklePatriciaTrie<Bytes, Bytes> accountTrie =
new StoredMerklePatriciaTrie<>(
this::getAccountStateTrieNode,
(location, hash) ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it appears that Optional<Bytes> getAccountStateTrieNode() is no longer used anywhere

@@ -300,8 +301,10 @@ public BesuController build() {
reorgLoggingThreshold,
dataDirectory.toString());

final CachedMerkleTrieLoader cachedMerkleTrieLoader = new CachedMerkleTrieLoader(metricsSystem);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this feels like a worldstateArchive concern that is leaking. Non-blocking, but I think we could benefit from a builder and/or pass metricsSystem instead

import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.apache.tuweni.units.bigints.UInt256;
import org.jetbrains.annotations.NotNull;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think only intellij is going to use this annotation. Findbugs uses NonNull and would probably be a better choice.

}

@VisibleForTesting
public void cacheAccountNodes(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be default visibility rather than public

}
}

public void preLoadStorageSlot(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be default viz

Copy link
Contributor

@garyschulte garyschulte left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking feedback, can be addressed in a subsequent PR.


import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.apache.tuweni.units.bigints.UInt256;

public class TrieGenerator {

public static MerklePatriciaTrie<Bytes32, Bytes> generateTrie(
public static MerklePatriciaTrie<Bytes, Bytes> generateTrie(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the change from Bytes32 to Bytes for the MPTs ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because the keys are not necessarily only hashes. storage flat db has longer keys

worldStateRootHash,
Function.identity(),
Function.identity());
accountTrie.get(Hash.hash(account));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should comment that loading the cache is a side effect of calling get

final StoredMerklePatriciaTrie<Bytes, Bytes> accountTrie =
new StoredMerklePatriciaTrie<>(
(location, hash) -> {
Optional<Bytes> node = worldStateStorage.getAccountStateTrieNode(location, hash);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason we are doing the ifPresent check inside the MPT rather than before we create it, like we do in cacheStorageNodes ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand the comment. The if is different in this case unless I didn't understand what you mean 😄

@matkt
Copy link
Contributor Author

matkt commented Dec 1, 2022

I merge and I will fix in a next PR. Thank you for your reviews

@matkt matkt merged commit fae615f into hyperledger:main Dec 1, 2022
Gabriel-Trintinalia pushed a commit to Gabriel-Trintinalia/besu that referenced this pull request Dec 8, 2022
The idea behind this commit is to preload asynchronously account nodes and storage nodes from the database during the transaction processing to use these nodes during the calculate root hash step.
We've created two caches, one for account nodes and one for storage nodes. The size of these caches is 100k for accounts and 200k for storage. We've tested other values but this configuration is the one that works better.
We also use exporter cache metrics as Prometheus metrics to check cache efficiency.

Signed-off-by: Karim TAAM <[email protected]>

Co-authored-by: Ameziane H <[email protected]>
Signed-off-by: Gabriel-Trintinalia <[email protected]>
macfarla pushed a commit to jflo/besu that referenced this pull request Jan 10, 2023
The idea behind this commit is to preload asynchronously account nodes and storage nodes from the database during the transaction processing to use these nodes during the calculate root hash step.
We've created two caches, one for account nodes and one for storage nodes. The size of these caches is 100k for accounts and 200k for storage. We've tested other values but this configuration is the one that works better.
We also use exporter cache metrics as Prometheus metrics to check cache efficiency.

Signed-off-by: Karim TAAM <[email protected]>

Co-authored-by: Ameziane H <[email protected]>
Signed-off-by: Sally MacFarlane <[email protected]>
@matkt matkt deleted the feature/cached-state-node branch August 17, 2023 07:27
eum602 pushed a commit to lacchain/besu that referenced this pull request Nov 3, 2023
The idea behind this commit is to preload asynchronously account nodes and storage nodes from the database during the transaction processing to use these nodes during the calculate root hash step.
We've created two caches, one for account nodes and one for storage nodes. The size of these caches is 100k for accounts and 200k for storage. We've tested other values but this configuration is the one that works better.
We also use exporter cache metrics as Prometheus metrics to check cache efficiency.

Signed-off-by: Karim TAAM <[email protected]>

Co-authored-by: Ameziane H <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
TeamChupa GH issues worked on by Chupacabara Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants