Skip to content

Commit bc3e5a0

Browse files
wqfishclaude
andcommitted
[vm] Record txn read sets for hot state promotion
Hot state promotions are currently derived from BlockSTM's captured reads, which exist for validation and differ between the parallel and sequential paths (e.g. aggregator v1 reads served through delta resolution are excluded in parallel but recorded in sequential), and the accumulation is coupled to the `conflict_penalty_window` config. The write-side exclusion also misses in-place delayed field rewrites, aggregator v1 writes/deltas and module writes, so keys written in the block can still be promoted by the epilogue. Record the read set at the VM boundary instead, where it is a function of the transaction and the pre-state only: - `StorageAdapter` records resource, resource group, table item, aggregator v1 and config reads. - A new `ReadRecordingCodeStorage` wrapper records module fetches, including those served by the global module cache (which bypass the state view today and require cache priming). The merged set rides on `AptosTransactionOutput` next to the `VMOutput`; the committed incarnation's reads are fed to the hot state accumulator in commit order via `accumulate_hot_state_rw()`, gated only on the accumulator being enabled. Keys treated as written are enumerated completely from the change set in `hot_state_write_keys()`. The derived promotion set intentionally differs from the old one (e.g. `exists<T>` loads the resource through the resolver and now counts as a read), so enabling this in production requires its own gating. Read kind/hotness tagging, an on-chain promotion cap, and removal of the old summary-based derivation are left for follow-ups. Also adds a per-block promotions histogram, a `StorageAdapter` recording unit test, and an e2e test asserting that promotions are identical across sequential and parallel execution, include module keys, and exclude every key the block writes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 21438b3 commit bc3e5a0

13 files changed

Lines changed: 512 additions & 10 deletions

File tree

aptos-move/aptos-vm-types/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@ aptos-gas-schedule = { workspace = true, features = ["testing"] }
3838
aptos-transaction-simulation = { workspace = true }
3939
aptos-vm = { workspace = true }
4040
test-case = { workspace = true }
41+
42+
[lints.rust]
43+
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(fuzzing)"] }

aptos-move/aptos-vm-types/src/module_and_script_storage/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
pub mod code_storage;
55
pub mod module_storage;
6+
pub mod read_recording;
67

78
mod state_view_adapter;
89
pub use state_view_adapter::{AptosCodeStorageAdapter, AsAptosCodeStorage};
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// Copyright (c) Aptos Foundation
2+
// Licensed pursuant to the Innovation-Enabling Source Code License, available at https://github.com/aptos-labs/aptos-core/blob/main/LICENSE
3+
4+
use crate::{
5+
module_and_script_storage::module_storage::AptosModuleStorage,
6+
resolver::BlockSynchronizationKillSwitch,
7+
};
8+
use aptos_types::state_store::{state_key::StateKey, state_value::StateValueMetadata};
9+
use bytes::Bytes;
10+
use move_binary_format::{
11+
errors::{PartialVMResult, VMResult},
12+
file_format::CompiledScript,
13+
CompiledModule,
14+
};
15+
use move_core_types::{
16+
account_address::AccountAddress, identifier::IdentStr, language_storage::ModuleId,
17+
};
18+
use move_vm_runtime::{
19+
LayoutCache, LayoutCacheEntry, Module, ModuleStorage, RuntimeEnvironment, Script, StructKey,
20+
WithRuntimeEnvironment,
21+
};
22+
use move_vm_types::code::{Code, ScriptCache};
23+
use std::{cell::RefCell, collections::HashSet, sync::Arc};
24+
25+
/// Wraps a code storage and records the state key of every module the VM fetches through it,
26+
/// so that module accesses become part of the transaction's observed read set (the basis for
27+
/// hot state promotion). Recording happens here, at the VM boundary, so the derived read set
28+
/// is the same no matter which executor (sequential, BlockSTM, ...) or cache tier (global
29+
/// cross-block cache vs per-block cache vs storage) served the module.
30+
///
31+
/// Scripts are not state items, so script cache accesses are not recorded.
32+
pub struct ReadRecordingCodeStorage<'a, C> {
33+
code_storage: &'a C,
34+
module_reads: RefCell<HashSet<StateKey>>,
35+
}
36+
37+
impl<'a, C> ReadRecordingCodeStorage<'a, C> {
38+
pub fn new(code_storage: &'a C) -> Self {
39+
Self {
40+
code_storage,
41+
module_reads: RefCell::new(HashSet::new()),
42+
}
43+
}
44+
45+
/// Returns the state keys of modules fetched so far, clearing the recorded set.
46+
pub fn take_recorded_reads(&self) -> HashSet<StateKey> {
47+
self.module_reads.take()
48+
}
49+
50+
fn record(&self, address: &AccountAddress, module_name: &IdentStr) {
51+
self.module_reads
52+
.borrow_mut()
53+
.insert(StateKey::module(address, module_name));
54+
}
55+
}
56+
57+
impl<C: WithRuntimeEnvironment> WithRuntimeEnvironment for ReadRecordingCodeStorage<'_, C> {
58+
fn runtime_environment(&self) -> &RuntimeEnvironment {
59+
self.code_storage.runtime_environment()
60+
}
61+
}
62+
63+
impl<C: LayoutCache> LayoutCache for ReadRecordingCodeStorage<'_, C> {
64+
fn get_struct_layout(&self, key: &StructKey) -> Option<LayoutCacheEntry> {
65+
self.code_storage.get_struct_layout(key)
66+
}
67+
68+
fn store_struct_layout(&self, key: &StructKey, entry: LayoutCacheEntry) -> PartialVMResult<()> {
69+
self.code_storage.store_struct_layout(key, entry)
70+
}
71+
72+
fn remove_struct_layout(&self, key: &StructKey) {
73+
self.code_storage.remove_struct_layout(key)
74+
}
75+
}
76+
77+
impl<C: ModuleStorage> ModuleStorage for ReadRecordingCodeStorage<'_, C> {
78+
fn unmetered_check_module_exists(
79+
&self,
80+
address: &AccountAddress,
81+
module_name: &IdentStr,
82+
) -> VMResult<bool> {
83+
self.record(address, module_name);
84+
self.code_storage
85+
.unmetered_check_module_exists(address, module_name)
86+
}
87+
88+
fn unmetered_get_module_bytes(
89+
&self,
90+
address: &AccountAddress,
91+
module_name: &IdentStr,
92+
) -> VMResult<Option<Bytes>> {
93+
self.record(address, module_name);
94+
self.code_storage
95+
.unmetered_get_module_bytes(address, module_name)
96+
}
97+
98+
fn unmetered_get_module_hash_and_size(
99+
&self,
100+
address: &AccountAddress,
101+
module_name: &IdentStr,
102+
) -> VMResult<Option<([u8; 32], usize)>> {
103+
self.record(address, module_name);
104+
self.code_storage
105+
.unmetered_get_module_hash_and_size(address, module_name)
106+
}
107+
108+
fn unmetered_get_module_size(
109+
&self,
110+
address: &AccountAddress,
111+
module_name: &IdentStr,
112+
) -> VMResult<Option<usize>> {
113+
self.record(address, module_name);
114+
self.code_storage
115+
.unmetered_get_module_size(address, module_name)
116+
}
117+
118+
fn unmetered_get_deserialized_module(
119+
&self,
120+
address: &AccountAddress,
121+
module_name: &IdentStr,
122+
) -> VMResult<Option<Arc<CompiledModule>>> {
123+
self.record(address, module_name);
124+
self.code_storage
125+
.unmetered_get_deserialized_module(address, module_name)
126+
}
127+
128+
fn unmetered_get_eagerly_verified_module(
129+
&self,
130+
address: &AccountAddress,
131+
module_name: &IdentStr,
132+
) -> VMResult<Option<Arc<Module>>> {
133+
self.record(address, module_name);
134+
self.code_storage
135+
.unmetered_get_eagerly_verified_module(address, module_name)
136+
}
137+
138+
fn unmetered_get_lazily_verified_module(
139+
&self,
140+
module_id: &ModuleId,
141+
) -> VMResult<Option<Arc<Module>>> {
142+
self.record(module_id.address(), module_id.name());
143+
self.code_storage
144+
.unmetered_get_lazily_verified_module(module_id)
145+
}
146+
147+
#[cfg(fuzzing)]
148+
fn unmetered_get_module_skip_verification(
149+
&self,
150+
address: &AccountAddress,
151+
module_name: &IdentStr,
152+
) -> VMResult<Option<Arc<Module>>> {
153+
self.record(address, module_name);
154+
self.code_storage
155+
.unmetered_get_module_skip_verification(address, module_name)
156+
}
157+
}
158+
159+
impl<C: AptosModuleStorage> AptosModuleStorage for ReadRecordingCodeStorage<'_, C> {
160+
fn unmetered_get_module_state_value_metadata(
161+
&self,
162+
address: &AccountAddress,
163+
module_name: &IdentStr,
164+
) -> PartialVMResult<Option<StateValueMetadata>> {
165+
self.record(address, module_name);
166+
self.code_storage
167+
.unmetered_get_module_state_value_metadata(address, module_name)
168+
}
169+
}
170+
171+
impl<C> ScriptCache for ReadRecordingCodeStorage<'_, C>
172+
where
173+
C: ScriptCache<Key = [u8; 32], Deserialized = CompiledScript, Verified = Script>,
174+
{
175+
type Deserialized = CompiledScript;
176+
type Key = [u8; 32];
177+
type Verified = Script;
178+
179+
fn insert_deserialized_script(
180+
&self,
181+
key: Self::Key,
182+
deserialized_script: Self::Deserialized,
183+
) -> Arc<Self::Deserialized> {
184+
self.code_storage
185+
.insert_deserialized_script(key, deserialized_script)
186+
}
187+
188+
fn insert_verified_script(
189+
&self,
190+
key: Self::Key,
191+
verified_script: Self::Verified,
192+
) -> Arc<Self::Verified> {
193+
self.code_storage
194+
.insert_verified_script(key, verified_script)
195+
}
196+
197+
fn get_script(&self, key: &Self::Key) -> Option<Code<Self::Deserialized, Self::Verified>> {
198+
self.code_storage.get_script(key)
199+
}
200+
201+
fn num_scripts(&self) -> usize {
202+
self.code_storage.num_scripts()
203+
}
204+
}
205+
206+
impl<C: BlockSynchronizationKillSwitch> BlockSynchronizationKillSwitch
207+
for ReadRecordingCodeStorage<'_, C>
208+
{
209+
fn interrupt_requested(&self) -> bool {
210+
self.code_storage.interrupt_requested()
211+
}
212+
}

aptos-move/aptos-vm/src/block_executor/mod.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,25 @@ use vm_wrapper::AptosExecutorTask;
5959
pub struct AptosTransactionOutput {
6060
vm_output: Option<VMOutput>,
6161
committed_output: OnceCell<TransactionOutput>,
62+
/// State keys read by the VM during the execution (incarnation) that produced this
63+
/// output. Recorded at the resolver / code storage boundary, so the content does not
64+
/// depend on the executor mode (sequential vs BlockSTM) serving the reads. Consumed by
65+
/// the block executor to derive hot state promotions; never part of the committed
66+
/// output. TODO(HotState): also record the read kind (exists/metadata/value) and the
67+
/// observed slot hotness, so the promotion policy can filter on them.
68+
read_set: HashSet<StateKey>,
6269
}
6370

6471
impl AptosTransactionOutput {
6572
pub fn new(output: VMOutput) -> Self {
73+
Self::new_with_read_set(output, HashSet::new())
74+
}
75+
76+
pub fn new_with_read_set(output: VMOutput, read_set: HashSet<StateKey>) -> Self {
6677
Self {
6778
vm_output: Some(output),
6879
committed_output: OnceCell::new(),
80+
read_set,
6981
}
7082
}
7183

@@ -107,6 +119,7 @@ impl<'a> AfterMaterializationOutput<SignatureVerifiedTransaction>
107119
/// Before materialization guard wrapper that holds a read lock.
108120
pub struct BeforeMaterializationGuard<'a> {
109121
guard: &'a VMOutput,
122+
read_set: &'a HashSet<StateKey>,
110123
}
111124

112125
impl BeforeMaterializationOutput<SignatureVerifiedTransaction> for BeforeMaterializationGuard<'_> {
@@ -156,6 +169,22 @@ impl BeforeMaterializationOutput<SignatureVerifiedTransaction> for BeforeMateria
156169
writes
157170
}
158171

172+
fn hot_state_read_keys(&self) -> HashSet<StateKey> {
173+
self.read_set.clone()
174+
}
175+
176+
fn hot_state_write_keys(&self) -> HashSet<StateKey> {
177+
// Every key receiving a value write becomes hot via the write itself, so the hot
178+
// state accumulator must treat it as written and not promote it separately. Unlike
179+
// get_write_summary (conflict detection), this includes in-place delayed field
180+
// rewrites, aggregator v1 writes/deltas and module writes.
181+
let mut keys: HashSet<StateKey> = self.guard.resource_write_set().keys().cloned().collect();
182+
keys.extend(self.guard.module_write_set().keys().cloned());
183+
keys.extend(self.guard.aggregator_v1_write_set().keys().cloned());
184+
keys.extend(self.guard.aggregator_v1_delta_set().keys().cloned());
185+
keys
186+
}
187+
159188
// TODO: get rid of the cloning data-structures in the following APIs.
160189
fn resource_group_write_set(
161190
&self,
@@ -381,6 +410,7 @@ impl BlockExecutorTransactionOutput for AptosTransactionOutput {
381410
.vm_output
382411
.as_ref()
383412
.ok_or_else(|| code_invariant_error("Output must be set but not materialized"))?,
413+
read_set: &self.read_set,
384414
})
385415
}
386416

aptos-move/aptos-vm/src/block_executor/vm_wrapper.rs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ use aptos_types::{
1515
use aptos_vm_environment::environment::AptosEnvironment;
1616
use aptos_vm_logging::{log_schema::AdapterLogSchema, prelude::*};
1717
use aptos_vm_types::{
18-
module_and_script_storage::code_storage::AptosCodeStorage,
18+
module_and_script_storage::{
19+
code_storage::AptosCodeStorage, read_recording::ReadRecordingCodeStorage,
20+
},
1921
resolver::{BlockSynchronizationKillSwitch, ExecutorView, ResourceGroupView},
2022
};
2123
use fail::fail_point;
@@ -61,11 +63,20 @@ impl ExecutorTask for AptosExecutorTask {
6163

6264
let log_context = AdapterLogSchema::new(self.id, txn_idx as usize);
6365
let resolver = self.vm.as_move_resolver_with_group_view(view);
64-
match self
65-
.vm
66-
.execute_single_transaction(txn, &resolver, view, &log_context, auxiliary_info)
67-
{
66+
let code_storage = ReadRecordingCodeStorage::new(view);
67+
match self.vm.execute_single_transaction(
68+
txn,
69+
&resolver,
70+
&code_storage,
71+
&log_context,
72+
auxiliary_info,
73+
) {
6874
Ok((vm_status, vm_output)) => {
75+
// The state keys the VM read during this execution (incarnation). For the
76+
// committed incarnation this is the transaction's logical read set, used to
77+
// derive hot state promotions.
78+
let mut read_set = resolver.take_recorded_reads();
79+
read_set.extend(code_storage.take_recorded_reads());
6980
if vm_output.status().is_discarded() {
7081
speculative_trace!(
7182
&log_context,
@@ -87,13 +98,17 @@ impl ExecutorTask for AptosExecutorTask {
8798
&log_context,
8899
"Reconfiguration occurred: restart required".into()
89100
);
90-
ExecutionStatus::SkipRest(AptosTransactionOutput::new(vm_output))
101+
ExecutionStatus::SkipRest(AptosTransactionOutput::new_with_read_set(
102+
vm_output, read_set,
103+
))
91104
} else {
92105
assert!(
93106
Self::is_transaction_dynamic_change_set_capable(txn),
94107
"DirectWriteSet should always create SkipRest transaction, validate_waypoint_change_set provides this guarantee"
95108
);
96-
ExecutionStatus::Success(AptosTransactionOutput::new(vm_output))
109+
ExecutionStatus::Success(AptosTransactionOutput::new_with_read_set(
110+
vm_output, read_set,
111+
))
97112
}
98113
},
99114
// execute_single_transaction only returns an error when transactions that should never fail

0 commit comments

Comments
 (0)