-
Notifications
You must be signed in to change notification settings - Fork 597
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
feat: prefetch more trie keys #10899
Conversation
Could the keys accessed via |
Ah. I suspect ft_balance_of is a read-only query, and these ones are served from FlatStorage, which is only 1 fast disk read. Only write queries matter. Note: with stateless validation everything will be served from memtrie in memory, so hopefully this hack is a short-term. We consider enabling memtries even sooner. |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #10899 +/- ##
==========================================
- Coverage 71.49% 71.44% -0.06%
==========================================
Files 759 759
Lines 152522 152594 +72
Branches 152522 152594 +72
==========================================
- Hits 109042 109015 -27
- Misses 38964 39051 +87
- Partials 4516 4528 +12
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
a7c887d
to
4d7396b
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.
Just nits from my side. I'm not familiar with the prefetch so I'll leave it to @mooori to review and I'll stamp when his review is done.
core/store/src/trie/shard_tries.rs
Outdated
&& (self.0.trie_config.enable_receipt_prefetching | ||
|| (!self.0.trie_config.sweat_prefetch_receivers.is_empty() | ||
&& !self.0.trie_config.sweat_prefetch_senders.is_empty())); | ||
&& !self.0.trie_config.sweat_prefetch_senders.is_empty()) | ||
|| !self.0.trie_config.claim_sweat_prefetch_config.is_empty() | ||
|| !self.0.trie_config.kaiching_prefetch_config.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.
nit: Could this be a method on self.0 or self.0.trie_config?
let prefetch_enabled = !is_view && self.0.trie_config.prefetch_enabled();
@@ -59,6 +59,9 @@ pub struct StoreConfig { | |||
/// This config option is temporary and will be removed once flat storage is implemented. | |||
pub sweat_prefetch_senders: Vec<String>, | |||
|
|||
pub claim_sweat_prefetch_config: Vec<PrefetchConfig>, | |||
pub kaiching_prefetch_config: Vec<PrefetchConfig>, | |||
|
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.
nit: It would be nice to put all of the prefetch configs into a dedicated struct e.g. PrefetchConfig
. The only concern is that if any nodes have this config overrided they may not notice and lose their configuration. Do you know if that is the case? Would it be sufficient to announce this change in the CHANGELOG?
It would make the code nicer but is a bit annoying so up to you if you want to do it.
|
||
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] | ||
#[serde(default)] | ||
pub struct PrefetchConfig { |
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.
Can you add a comment please?
runtime/runtime/src/prefetch.rs
Outdated
for action in &action_receipt.actions { | ||
if let Action::FunctionCall(fn_call) = action { | ||
for action in &action_receipt.actions { | ||
if let Action::FunctionCall(fn_call) = action { |
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.
nit:
let Action::FunctionCall(fn_call) = action else { continue; }
runtime/runtime/src/prefetch.rs
Outdated
if let Action::FunctionCall(fn_call) = action { | ||
for action in &action_receipt.actions { | ||
if let Action::FunctionCall(fn_call) = action { | ||
if self.prefetch_api.sweat_prefetch_receivers.contains(&account_id) |
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.
nit: Each of those ifs can be moved to a small helper method for cleaner code.
runtime/runtime/src/prefetch.rs
Outdated
if let Ok(json) = serde_json::de::from_slice::<serde_json::Value>(arg) { | ||
if json.is_object() { | ||
if let Some(list) = json.get("steps_batch") { | ||
if let Some(list) = list.as_array() { | ||
for tuple in list.iter() { | ||
if let Some(tuple) = tuple.as_array() { | ||
if let Some(user_account) = tuple.first().and_then(|a| a.as_str()) { | ||
let hashed_account = | ||
sha2::Sha256::digest(user_account.as_bytes()).into_iter(); | ||
let mut key = vec![0x74, 0x00]; | ||
key.extend(hashed_account); | ||
let trie_key = TrieKey::ContractData { | ||
account_id: account_id.clone(), | ||
key: key.to_vec(), | ||
}; | ||
near_o11y::io_trace!(count: "prefetch"); | ||
self.prefetch_trie_key(trie_key)?; | ||
if let Some(list) = get_inner_json(&json, "steps_batch") { |
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.
nit: ditto on using the let ... else
pattern for early returns to flatten the code
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.
same in other methods below
Ok(()) | ||
} | ||
|
||
fn prefetch_claim_sweat(&self, account_id: AccountId, arg: &[u8]) -> Result<(), PrefetchError> { |
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.
nit: Add a comment please. There are plenty of magic values and constants that were reverse engineered from the contract right? Let's make it easier for the next person who needs to adjust it after the contract is updated.
same for the other new methods.
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.
and since you are already refactoring the initial sweat prefetcher, maybe also add a comment about that :)
You could add that let mut key = vec![0x74, 0x00];
is just a "t" string used as the unique prefix of the data structure terminated by a null value.
Also do we have any good metrics and logs for prefetcher? If not can you add those and prepare a dashboard? |
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 I can review prefetch_kaiching
. Otherwise, unfortunately I'm not familiar with the prefetcher implementation either. @Longarithm do you know someone who is?
+1 to Wac's comments, I think addressing them would make it much easier to follow/review this code. Especially the deep nestings are hard to follow.
This Zulip topic should probably be brought up as reference for the Kaiching prefetching.
Are there any prefetcher tests that should be updated or added?
Ok(()) | ||
} | ||
|
||
fn prefetch_kaiching(&self, account_id: AccountId, arg: &[u8]) -> Result<(), PrefetchError> { |
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.
Comments with more details on which parts of the ft_transfer_on
logic this prefetch covers would be very helpful, since there are multiple interactions with storage.
Another storage read is triggered by calling self.assert_deposit_whitelist(...)
inside ft_transfer_on
. Would trying to prefetch that too be worth it?
runtime/runtime/src/prefetch.rs
Outdated
let reward_id = tuple.get(2).and_then(|a| a.as_str()); | ||
if user_account_serialize_result.is_some() { | ||
if let Some(reward_id) = reward_id { | ||
let user_account_key_hash = hash(&user_account_key); |
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.
CryptoHash
is opaque, so if sha256
could be used (like in Sweat prefetching), it would be more clear what happens here.
Edit: In case the hashing algorithm of CryptoHash
is an implementation detail this might even break when the hashing algorithm is changed. Though such change is probably unlikely.
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 code looks like it could work. But it's impossible to say without comparing it to the smart contract code and deep analysis. I appreciate that a lot of work went into this. It's unlikely that reviewers will find the time to replicate the full analysis. So ultimately, we need to rely on testing how it performs.
I assume you did the testing. But I didn't look at any resting results, so I cannot say if those are showing without doubt that all prefetchers work as expected. And since the prefetches are all counted the same in this code, I imagine it could be easy to miss if one of the trie keys is not constructed properly. So if not done already, I would suggest to test each prefetcher separately and report how many prefetches it triggers and how by much it reduces the unprefetched trie node reads.
But I'll approve it already because I think even a wrong trie key wouldn't do much harm, it just wouldn't give the desired results.
And of course, it wouldn't hurt to address the comments raised regarding style and documentation (comments) by me and other reviewers before merging. 😄
runtime/runtime/src/prefetch.rs
Outdated
fn get_inner_json<'a>(json: &'a serde_json::Value, key: &str) -> Option<&'a serde_json::Value> { | ||
if json.is_object() { | ||
json.get(key) | ||
} else { | ||
None | ||
} | ||
} | ||
|
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.
What is the difference between get_inner_json(value, "key")
to just value.get("key")
?
It seems the signature is pretty much the same as
https://docs.rs/serde_json/latest/serde_json/enum.Value.html#method.get
I think the only reason why the old code checked for is_object
first was to make the code as obvious as possible, rather than nice and clean. That was a clear priority, given how quick it was all finalized. And maybe another reason was me naively assuming that the more this ugly code sticks out, the more likely we will remove it sooner rather than later...
Today, we can write cleaner code and review it properly, so let's remove unnecessary checks.
Ok(()) | ||
} | ||
|
||
fn prefetch_claim_sweat(&self, account_id: AccountId, arg: &[u8]) -> Result<(), PrefetchError> { |
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.
and since you are already refactoring the initial sweat prefetcher, maybe also add a comment about that :)
You could add that let mut key = vec![0x74, 0x00];
is just a "t" string used as the unique prefix of the data structure terminated by a null value.
runtime/runtime/src/prefetch.rs
Outdated
for tuple in list.iter() { | ||
if let Some(tuple) = tuple.as_array() { | ||
if let Some(user_account) = tuple.first().and_then(|a| a.as_str()) { | ||
let mut key = vec![0, 64, 0, 0, 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.
That's an odd key to prefix... Why 5 bytes? Is this also a data structure prefix in the smart contract?
Looks like maybe a 0 as u8
followed by a 64 as u64
in little endian. Or is it maybe 16384 as u64
in little endian, followed by a separating null value?
runtime/runtime/src/prefetch.rs
Outdated
let mut user_account_key = Vec::new(); | ||
user_account_key.extend([1, 109]); |
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.
let mut user_account_key = Vec::new(); | |
user_account_key.extend([1, 109]); | |
let mut user_account_key = vec![1, 109]; |
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.
Also, it would be interesting again to know what this key is. If we don't understand this 100%, it's hard to argue if it's safe to hardcode the value.
runtime/runtime/src/prefetch.rs
Outdated
near_o11y::io_trace!(count: "prefetch"); | ||
self.prefetch_trie_key(trie_key)?; | ||
|
||
let mut reward_key = vec![0, 24, 0, 0, 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.
Again, an explanation for the key seems quite important to have in comments. Let's not make the bad practice I introduced at the time the new norm. ;)
@Longarithm could we merge this PR :) |
FWIW we can look and keep an eye on the prefetching graphs to get some intuition as to whether the prefetcher is working as we intend it to. But also, we should merge this sooner rather than later? We've a release pending soon and if my memory serves me right we wanted this PR in it? cc @VanBarbascu |
Addressed most comments, prioritising getting this into next release. Had some trouble with merging with prefetching logic on master but came through it. |
Co-authored-by: Longarithm <the.aleksandr.logunov@gmail.com>
No description provided.