From 5633f68ba918973e0b0eb96814119f9cdc2af6f1 Mon Sep 17 00:00:00 2001 From: jackzhhuang Date: Fri, 6 Mar 2026 15:19:03 +0800 Subject: [PATCH 01/11] fix test case rpc/client/tests/chain_get_block_txn_infos_in_seq_test.rs --- rpc/client/tests/chain_get_block_txn_infos_in_seq_test.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rpc/client/tests/chain_get_block_txn_infos_in_seq_test.rs b/rpc/client/tests/chain_get_block_txn_infos_in_seq_test.rs index 6fae000f46..0224d34900 100644 --- a/rpc/client/tests/chain_get_block_txn_infos_in_seq_test.rs +++ b/rpc/client/tests/chain_get_block_txn_infos_in_seq_test.rs @@ -204,7 +204,6 @@ fn wait_for_queryable_main_head_hash( std::thread::sleep(Duration::from_millis(200)); } } - fn wait_for_consistent_txn_infos( client: &RpcClient, block_hash: HashValue, From 2fff53a156cdb8e4eaaf3405e5065de7ef4845f6 Mon Sep 17 00:00:00 2001 From: jackzhhuang Date: Fri, 27 Mar 2026 10:56:57 +0800 Subject: [PATCH 02/11] add recursive check --- vm2/vm-runtime/src/data_cache.rs | 153 +++++++++++++++--- .../src/parallel_executor/storage_wrapper.rs | 81 +++++++--- 2 files changed, 193 insertions(+), 41 deletions(-) diff --git a/vm2/vm-runtime/src/data_cache.rs b/vm2/vm-runtime/src/data_cache.rs index 88a490a5e1..eaad83cf8f 100644 --- a/vm2/vm-runtime/src/data_cache.rs +++ b/vm2/vm-runtime/src/data_cache.rs @@ -56,7 +56,7 @@ use std::sync::{ Arc, }; use std::{ - cell::RefCell, + cell::{Cell, RefCell}, collections::btree_map::BTreeMap, collections::HashSet, ops::{Deref, DerefMut}, @@ -153,6 +153,57 @@ struct GroupReadInfo { layouts: BTreeMap>, } +fn compute_layout_has_identifier_mappings(layout: &MoveTypeLayout) -> bool { + match layout { + MoveTypeLayout::Native(..) => true, + MoveTypeLayout::Vector(inner) => compute_layout_has_identifier_mappings(inner), + MoveTypeLayout::Struct(struct_layout) => match struct_layout { + MoveStructLayout::Runtime(fields) => { + fields.iter().any(compute_layout_has_identifier_mappings) + } + MoveStructLayout::WithFields(fields) => fields + .iter() + .any(|field| compute_layout_has_identifier_mappings(&field.layout)), + MoveStructLayout::WithTypes { fields, .. } => fields + .iter() + .any(|field| compute_layout_has_identifier_mappings(&field.layout)), + }, + _ => false, + } +} + +#[derive(Default)] +struct LayoutIdentifierMappingCache { + last_key: Cell, + last_value: Cell, + has_last: Cell, + entries: RefCell>, +} + +impl LayoutIdentifierMappingCache { + fn has_identifier_mappings(&self, layout: &MoveTypeLayout) -> bool { + let key = layout as *const MoveTypeLayout as usize; + if self.has_last.get() && self.last_key.get() == key { + return self.last_value.get(); + } + + if let Some(cached) = self.entries.borrow().get(&key) { + let value = *cached; + self.last_key.set(key); + self.last_value.set(value); + self.has_last.set(true); + return value; + } + + let computed = compute_layout_has_identifier_mappings(layout); + self.entries.borrow_mut().insert(key, computed); + self.last_key.set(key); + self.last_value.set(computed); + self.has_last.set(true); + computed + } +} + /// Adapter to convert a `ExecutorView` into a `MoveResolver`. /// /// Resources in groups are handled either through dedicated interfaces of executor_view @@ -169,6 +220,7 @@ pub struct StorageAdapter<'e, E> { delayed_fields: VersionedDelayedFields, resource_reads: RefCell>, group_reads: RefCell>, + layout_identifier_mapping_cache: LayoutIdentifierMappingCache, delayed_field_id_start: u32, delayed_field_id_counter: AtomicU32, } @@ -252,23 +304,9 @@ impl TStateView for StateViewCache<'_, S> { } impl<'a, S: StateView> StorageAdapter<'a, S> { - fn layout_has_identifier_mappings(layout: &MoveTypeLayout) -> bool { - match layout { - MoveTypeLayout::Native(..) => true, - MoveTypeLayout::Vector(inner) => Self::layout_has_identifier_mappings(inner), - MoveTypeLayout::Struct(struct_layout) => match struct_layout { - MoveStructLayout::Runtime(fields) => { - fields.iter().any(Self::layout_has_identifier_mappings) - } - MoveStructLayout::WithFields(fields) => fields - .iter() - .any(|field| Self::layout_has_identifier_mappings(&field.layout)), - MoveStructLayout::WithTypes { fields, .. } => fields - .iter() - .any(|field| Self::layout_has_identifier_mappings(&field.layout)), - }, - _ => false, - } + fn layout_has_identifier_mappings(&self, layout: &MoveTypeLayout) -> bool { + self.layout_identifier_mapping_cache + .has_identifier_mappings(layout) } pub fn new( @@ -290,6 +328,7 @@ impl<'a, S: StateView> StorageAdapter<'a, S> { delayed_fields: VersionedDelayedFields::empty(), resource_reads: RefCell::new(HashMap::new()), group_reads: RefCell::new(HashMap::new()), + layout_identifier_mapping_cache: LayoutIdentifierMappingCache::default(), delayed_field_id_start, delayed_field_id_counter: AtomicU32::new(delayed_field_id_start), } @@ -331,7 +370,7 @@ impl<'a, S: StateView> StorageAdapter<'a, S> { bytes: &Bytes, layout: &MoveTypeLayout, ) -> Result, StateviewError> { - if !Self::layout_has_identifier_mappings(layout) { + if !self.layout_has_identifier_mappings(layout) { return Ok(HashSet::new()); } let value = ValueSerDeContext::::new(self.max_value_nest_depth) @@ -478,7 +517,7 @@ impl<'a, S: StateView> StorageAdapter<'a, S> { state_value: &StateValue, layout: &MoveTypeLayout, ) -> Result<(StateValue, HashSet, bool), StateviewError> { - if !self.delayed_fields_enabled || !Self::layout_has_identifier_mappings(layout) { + if !self.delayed_fields_enabled || !self.layout_has_identifier_mappings(layout) { return Ok((state_value.clone(), HashSet::new(), false)); } let (exchanged, delayed_ids) = self.exchange_state_value(state_value, layout)?; @@ -1140,7 +1179,10 @@ impl IntoMoveResolver for S { #[cfg(test)] pub(crate) mod tests { use super::*; + use move_core_types::value::IdentifierMappingKind; use starcoin_vm_runtime_types::resource_group_adapter::GroupSizeKind; + use std::hint::black_box; + use std::time::Instant; //use starcoin_vm_types::on_chain_config::{Features, OnChainConfig}; // Expose a method to create a storage adapter with a provided group size kind. @@ -1169,4 +1211,75 @@ pub(crate) mod tests { state_view.as_move_resolver() } + + fn build_complex_aggregator_like_layout() -> MoveTypeLayout { + // Move-like shape: + // struct Position { cash: Aggregator, snap: Snapshot, limit: u128 } + // struct Account { positions: vector, totals: vector> } + // struct Vault { accounts: vector, risk: Position, nonce: u64 } + let position = MoveTypeLayout::Struct(MoveStructLayout::Runtime(vec![ + MoveTypeLayout::Native( + IdentifierMappingKind::Aggregator, + Box::new(MoveTypeLayout::U128), + ), + MoveTypeLayout::Native( + IdentifierMappingKind::Snapshot, + Box::new(MoveTypeLayout::U128), + ), + MoveTypeLayout::U128, + ])); + let account = MoveTypeLayout::Struct(MoveStructLayout::Runtime(vec![ + MoveTypeLayout::Vector(Box::new(position.clone())), + MoveTypeLayout::Vector(Box::new(MoveTypeLayout::Native( + IdentifierMappingKind::Aggregator, + Box::new(MoveTypeLayout::U128), + ))), + ])); + MoveTypeLayout::Struct(MoveStructLayout::Runtime(vec![ + MoveTypeLayout::Vector(Box::new(account)), + position, + MoveTypeLayout::U64, + ])) + } + + #[test] + #[ignore = "benchmark-style regression probe; run manually with --ignored --nocapture"] + fn test_bench_layout_identifier_mapping_cache_complex_aggregator() { + let layout = build_complex_aggregator_like_layout(); + let iterations = 2_000_000usize; + + let start_plain = Instant::now(); + let mut plain_acc = 0usize; + for _ in 0..iterations { + let hit = compute_layout_has_identifier_mappings(&layout); + plain_acc ^= usize::from(black_box(hit)); + } + let plain_elapsed = start_plain.elapsed(); + + let cache = LayoutIdentifierMappingCache::default(); + let start_cached = Instant::now(); + let mut cached_acc = 0usize; + for _ in 0..iterations { + let hit = cache.has_identifier_mappings(&layout); + cached_acc ^= usize::from(black_box(hit)); + } + let cached_elapsed = start_cached.elapsed(); + + assert_eq!(plain_acc, cached_acc, "cache must preserve semantics"); + assert_eq!(plain_acc, 0, "xor accumulator keeps optimizer honest"); + + let plain_ns_per_iter = plain_elapsed.as_nanos() as f64 / iterations as f64; + let cached_ns_per_iter = cached_elapsed.as_nanos() as f64 / iterations as f64; + let speedup = plain_ns_per_iter / cached_ns_per_iter; + + println!( + "[layout-check-bench] iters={} plain={:?} cached={:?} plain_ns/iter={:.2} cached_ns/iter={:.2} speedup={:.2}x", + iterations, + plain_elapsed, + cached_elapsed, + plain_ns_per_iter, + cached_ns_per_iter, + speedup + ); + } } diff --git a/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs b/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs index 7c18430def..1396ab9502 100644 --- a/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs +++ b/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs @@ -36,7 +36,7 @@ use starcoin_vm_types::state_store::{ state_value::StateValueMetadata, StateView, TStateView, }; use starcoin_vm_types::write_set::{TransactionWrite, WriteOp}; -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::collections::{BTreeMap, HashMap, HashSet}; use std::sync::Arc; @@ -164,6 +164,57 @@ struct GroupReadInfo { layouts: BTreeMap>, } +fn compute_layout_has_identifier_mappings(layout: &MoveTypeLayout) -> bool { + match layout { + MoveTypeLayout::Native(..) => true, + MoveTypeLayout::Vector(inner) => compute_layout_has_identifier_mappings(inner), + MoveTypeLayout::Struct(struct_layout) => match struct_layout { + MoveStructLayout::Runtime(fields) => { + fields.iter().any(compute_layout_has_identifier_mappings) + } + MoveStructLayout::WithFields(fields) => fields + .iter() + .any(|field| compute_layout_has_identifier_mappings(&field.layout)), + MoveStructLayout::WithTypes { fields, .. } => fields + .iter() + .any(|field| compute_layout_has_identifier_mappings(&field.layout)), + }, + _ => false, + } +} + +#[derive(Default)] +struct LayoutIdentifierMappingCache { + last_key: Cell, + last_value: Cell, + has_last: Cell, + entries: RefCell>, +} + +impl LayoutIdentifierMappingCache { + fn has_identifier_mappings(&self, layout: &MoveTypeLayout) -> bool { + let key = layout as *const MoveTypeLayout as usize; + if self.has_last.get() && self.last_key.get() == key { + return self.last_value.get(); + } + + if let Some(cached) = self.entries.borrow().get(&key) { + let value = *cached; + self.last_key.set(key); + self.last_value.set(value); + self.has_last.set(true); + return value; + } + + let computed = compute_layout_has_identifier_mappings(layout); + self.entries.borrow_mut().insert(key, computed); + self.last_key.set(key); + self.last_value.set(computed); + self.has_last.set(true); + computed + } +} + pub(crate) struct VersionedView<'a, S: StateView> { base_view: &'a S, hashmap_view: &'a MVHashMapView<'a, ParallelStateKey, ParallelStateValue>, @@ -173,26 +224,13 @@ pub(crate) struct VersionedView<'a, S: StateView> { accessed_groups: RefCell>, resource_reads: RefCell>, group_reads: RefCell>, + layout_identifier_mapping_cache: LayoutIdentifierMappingCache, } impl<'a, S: StateView> VersionedView<'a, S> { - fn layout_has_identifier_mappings(layout: &MoveTypeLayout) -> bool { - match layout { - MoveTypeLayout::Native(..) => true, - MoveTypeLayout::Vector(inner) => Self::layout_has_identifier_mappings(inner), - MoveTypeLayout::Struct(struct_layout) => match struct_layout { - MoveStructLayout::Runtime(fields) => { - fields.iter().any(Self::layout_has_identifier_mappings) - } - MoveStructLayout::WithFields(fields) => fields - .iter() - .any(|field| Self::layout_has_identifier_mappings(&field.layout)), - MoveStructLayout::WithTypes { fields, .. } => fields - .iter() - .any(|field| Self::layout_has_identifier_mappings(&field.layout)), - }, - _ => false, - } + fn layout_has_identifier_mappings(&self, layout: &MoveTypeLayout) -> bool { + self.layout_identifier_mapping_cache + .has_identifier_mappings(layout) } pub fn new( @@ -211,6 +249,7 @@ impl<'a, S: StateView> VersionedView<'a, S> { accessed_groups: RefCell::new(HashSet::new()), resource_reads: RefCell::new(HashMap::new()), group_reads: RefCell::new(HashMap::new()), + layout_identifier_mapping_cache: LayoutIdentifierMappingCache::default(), } } @@ -315,7 +354,7 @@ impl<'a, S: StateView> VersionedView<'a, S> { bytes: &Bytes, layout: &MoveTypeLayout, ) -> Result, StateviewError> { - if !Self::layout_has_identifier_mappings(layout) { + if !self.layout_has_identifier_mappings(layout) { return Ok(HashSet::new()); } let value = ValueSerDeContext::::new(self.max_value_nest_depth) @@ -406,7 +445,7 @@ impl<'a, S: StateView> VersionedView<'a, S> { state_value: &StateValue, layout: &MoveTypeLayout, ) -> Result<(StateValue, HashSet, bool), StateviewError> { - if !self.delayed_fields_enabled || !Self::layout_has_identifier_mappings(layout) { + if !self.delayed_fields_enabled || !self.layout_has_identifier_mappings(layout) { return Ok((state_value.clone(), HashSet::new(), false)); } let (exchanged, delayed_ids) = self.exchange_state_value(state_value, layout)?; @@ -451,7 +490,7 @@ impl<'a, S: StateView> VersionedView<'a, S> { if let (Some(layout), Some(state_value)) = (maybe_layout, maybe_state_value.as_ref()) { let exchanged = self.delayed_field_cache.get_or_insert_base_value( state_key.clone(), - Self::layout_has_identifier_mappings(layout), + self.layout_has_identifier_mappings(layout), || { let (value_with_ids, ids, _) = self.maybe_exchange_state_value(state_value, layout)?; From 20bf9fd67c2a66f20b1116987bf44649637bb4ca Mon Sep 17 00:00:00 2001 From: jackzhhuang Date: Tue, 31 Mar 2026 10:53:21 +0800 Subject: [PATCH 03/11] fix review comments for delayed materialization and flaky rpc test --- commons/parallel-executor/src/executor.rs | 6 +++--- .../chain_get_block_txn_infos_in_seq_test.rs | 21 +++++++++++-------- sync/starcoin-execute-bench/src/main.rs | 2 +- vm2/vm-runtime/src/parallel_executor/mod.rs | 9 ++++++-- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/commons/parallel-executor/src/executor.rs b/commons/parallel-executor/src/executor.rs index bece7ca19e..a4afbeebc8 100644 --- a/commons/parallel-executor/src/executor.rs +++ b/commons/parallel-executor/src/executor.rs @@ -2278,13 +2278,13 @@ mod tests { _view: &MVHashMapView, txn: &Self::T, ) -> ExecutionStatus { - let change = match txn.op { + let change = match &txn.op { CreateThenAddOp::Create { value } => { - DelayedChange::Create(DelayedFieldValue::Aggregator(value)) + DelayedChange::Create(DelayedFieldValue::Aggregator(*value)) } CreateThenAddOp::Add { delta, max_value } => { DelayedChange::Apply(DelayedApplyChange::AggregatorDelta { - delta: DeltaWithMax::new(SignedU128::Positive(delta), max_value), + delta: DeltaWithMax::new(SignedU128::Positive(*delta), *max_value), }) } }; diff --git a/rpc/client/tests/chain_get_block_txn_infos_in_seq_test.rs b/rpc/client/tests/chain_get_block_txn_infos_in_seq_test.rs index 0224d34900..f65ec0241d 100644 --- a/rpc/client/tests/chain_get_block_txn_infos_in_seq_test.rs +++ b/rpc/client/tests/chain_get_block_txn_infos_in_seq_test.rs @@ -52,8 +52,10 @@ fn test_chain_get_block_txn_infos_in_seq() -> Result<()> { std::thread::sleep(Duration::from_secs(5)); let client = RpcClient::connect_ipc(ipc_file)?; - let block_hash = wait_for_queryable_main_head_hash( + let block_hash = block.id(); + wait_for_block_queryable( &client, + block_hash, block.header().number(), Duration::from_secs(60), )?; @@ -176,28 +178,29 @@ fn test_chain_get_block_txn_infos_in_seq() -> Result<()> { Ok(()) } -fn wait_for_queryable_main_head_hash( +fn wait_for_block_queryable( client: &RpcClient, + block_hash: HashValue, min_number: u64, timeout: Duration, -) -> Result { +) -> Result<()> { let deadline = Instant::now() + timeout; loop { if Instant::now() >= deadline { return Err(format_err!( - "timeout waiting queryable main head, min number {}", + "timeout waiting queryable block {:?}, min number {}", + block_hash, min_number )); } if let Ok(chain_info) = client.chain_info() { let head_number = chain_info.head.number.0; - let head_hash = chain_info.head.block_hash; if head_number >= min_number { - let seq_res = client.chain_get_block_txn_infos_in_seq(head_hash); - let vm1_res = client.chain_get_block_txn_infos(head_hash); - let vm2_res = client.chain_get_block_txn_infos2(head_hash); + let seq_res = client.chain_get_block_txn_infos_in_seq(block_hash); + let vm1_res = client.chain_get_block_txn_infos(block_hash); + let vm2_res = client.chain_get_block_txn_infos2(block_hash); if seq_res.is_ok() && vm1_res.is_ok() && vm2_res.is_ok() { - return Ok(head_hash); + return Ok(()); } } } diff --git a/sync/starcoin-execute-bench/src/main.rs b/sync/starcoin-execute-bench/src/main.rs index 88c472d8c4..b9df2cf981 100644 --- a/sync/starcoin-execute-bench/src/main.rs +++ b/sync/starcoin-execute-bench/src/main.rs @@ -2256,7 +2256,7 @@ impl ObserverService { } }; - let batch_index = state.batch_index.load(Ordering::SeqCst); + let batch_index = state.batch_index.load(Ordering::SeqCst).saturating_sub(1); let txn_hashes = sign_and_import_transactions_sync(&batch, account_service, txpool)?; state.add_txn_hashes(&txn_hashes); info!( diff --git a/vm2/vm-runtime/src/parallel_executor/mod.rs b/vm2/vm-runtime/src/parallel_executor/mod.rs index 54b104d9c3..5d8a00f9a5 100644 --- a/vm2/vm-runtime/src/parallel_executor/mod.rs +++ b/vm2/vm-runtime/src/parallel_executor/mod.rs @@ -456,10 +456,14 @@ fn materialize_parallel_outputs( } let mut outputs = outputs; + let mut has_delayed = false; let mut has_agg_v1 = false; let mut group_touches = 0u64; let mut group_touch_counts: HashMap = HashMap::new(); for (_, output) in outputs.iter() { + if output.output.contains_delayed_fields() { + has_delayed = true; + } if !output.output.aggregator_v1_delta_set().is_empty() { has_agg_v1 = true; } @@ -471,7 +475,7 @@ fn materialize_parallel_outputs( } } let has_group_dup = group_touch_counts.values().any(|count| *count > 1); - let needs_sequential = has_agg_v1 || has_group_dup; + let needs_sequential = has_delayed || has_agg_v1 || has_group_dup; outputs.sort_by_key(|(idx, _)| *idx); if !needs_sequential { @@ -495,7 +499,8 @@ fn materialize_parallel_outputs( info!( target: "vm-bench", - "materialize sequential: agg_v1={} group_dup={} group_touches={}", + "materialize sequential: delayed={} agg_v1={} group_dup={} group_touches={}", + has_delayed, has_agg_v1, has_group_dup, group_touches From 098d02b14a0d1cae0bd2029465039e9f45d513ae Mon Sep 17 00:00:00 2001 From: jackzhhuang Date: Thu, 9 Apr 2026 22:39:54 +0800 Subject: [PATCH 04/11] refactor(vm2): unify layout identifier mapping cache with shared manager --- Cargo.lock | 1 + vm2/vm-runtime/Cargo.toml | 1 + vm2/vm-runtime/src/data_cache.rs | 60 +---- .../src/layout_identifier_mapping_cache.rs | 210 ++++++++++++++++++ vm2/vm-runtime/src/lib.rs | 1 + .../src/parallel_executor/storage_wrapper.rs | 56 +---- 6 files changed, 223 insertions(+), 106 deletions(-) create mode 100644 vm2/vm-runtime/src/layout_identifier_mapping_cache.rs diff --git a/Cargo.lock b/Cargo.lock index e481c822b6..7d23ce72dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13548,6 +13548,7 @@ dependencies = [ "rand 0.10.1", "rand_core 0.6.4", "rayon", + "rustc-hash 2.1.2", "serde 1.0.228", "serde_bytes", "starcoin-aggregator", diff --git a/vm2/vm-runtime/Cargo.toml b/vm2/vm-runtime/Cargo.toml index da5dbe73b2..e4bcf05d89 100644 --- a/vm2/vm-runtime/Cargo.toml +++ b/vm2/vm-runtime/Cargo.toml @@ -46,6 +46,7 @@ hex = "0.4.3" bytes = { workspace = true } bcs = { workspace = true } dashmap = { workspace = true } +rustc-hash = { workspace = true } starcoin-mvhashmap = { workspace = true } move-bytecode-verifier = { git = "https://github.com/starcoinorg/move", rev = "ed9d919d05fedeae9cf433d4f44f6aba526580c3" } starcoin-aggregator = { path = "../aggregator" } diff --git a/vm2/vm-runtime/src/data_cache.rs b/vm2/vm-runtime/src/data_cache.rs index eaad83cf8f..a835f13b24 100644 --- a/vm2/vm-runtime/src/data_cache.rs +++ b/vm2/vm-runtime/src/data_cache.rs @@ -10,7 +10,9 @@ use move_binary_format::CompiledModule; use move_bytecode_utils::compiled_module_viewer::CompiledModuleView; use move_core_types::metadata::Metadata; use move_core_types::resolver::{resource_size, ModuleResolver, ResourceResolver}; -use move_core_types::value::{MoveStructLayout, MoveTypeLayout}; +#[cfg(test)] +use move_core_types::value::MoveStructLayout; +use move_core_types::value::MoveTypeLayout; use move_table_extension::{TableHandle, TableResolver}; use move_vm_runtime::config::DEFAULT_MAX_VALUE_NEST_DEPTH; use move_vm_types::delayed_values::delayed_field_id::{ @@ -56,12 +58,15 @@ use std::sync::{ Arc, }; use std::{ - cell::{Cell, RefCell}, + cell::RefCell, collections::btree_map::BTreeMap, collections::HashSet, ops::{Deref, DerefMut}, }; +#[cfg(test)] +use crate::layout_identifier_mapping_cache::compute_layout_has_identifier_mappings; +use crate::layout_identifier_mapping_cache::LayoutIdentifierMappingCache; use crate::parallel_executor::{ materialize_events, materialize_resource_write_set, storage_wrapper::DelayedFieldCache, }; @@ -153,57 +158,6 @@ struct GroupReadInfo { layouts: BTreeMap>, } -fn compute_layout_has_identifier_mappings(layout: &MoveTypeLayout) -> bool { - match layout { - MoveTypeLayout::Native(..) => true, - MoveTypeLayout::Vector(inner) => compute_layout_has_identifier_mappings(inner), - MoveTypeLayout::Struct(struct_layout) => match struct_layout { - MoveStructLayout::Runtime(fields) => { - fields.iter().any(compute_layout_has_identifier_mappings) - } - MoveStructLayout::WithFields(fields) => fields - .iter() - .any(|field| compute_layout_has_identifier_mappings(&field.layout)), - MoveStructLayout::WithTypes { fields, .. } => fields - .iter() - .any(|field| compute_layout_has_identifier_mappings(&field.layout)), - }, - _ => false, - } -} - -#[derive(Default)] -struct LayoutIdentifierMappingCache { - last_key: Cell, - last_value: Cell, - has_last: Cell, - entries: RefCell>, -} - -impl LayoutIdentifierMappingCache { - fn has_identifier_mappings(&self, layout: &MoveTypeLayout) -> bool { - let key = layout as *const MoveTypeLayout as usize; - if self.has_last.get() && self.last_key.get() == key { - return self.last_value.get(); - } - - if let Some(cached) = self.entries.borrow().get(&key) { - let value = *cached; - self.last_key.set(key); - self.last_value.set(value); - self.has_last.set(true); - return value; - } - - let computed = compute_layout_has_identifier_mappings(layout); - self.entries.borrow_mut().insert(key, computed); - self.last_key.set(key); - self.last_value.set(computed); - self.has_last.set(true); - computed - } -} - /// Adapter to convert a `ExecutorView` into a `MoveResolver`. /// /// Resources in groups are handled either through dedicated interfaces of executor_view diff --git a/vm2/vm-runtime/src/layout_identifier_mapping_cache.rs b/vm2/vm-runtime/src/layout_identifier_mapping_cache.rs new file mode 100644 index 0000000000..bb6ae651ab --- /dev/null +++ b/vm2/vm-runtime/src/layout_identifier_mapping_cache.rs @@ -0,0 +1,210 @@ +// Copyright (c) The Starcoin Core Contributors +// SPDX-License-Identifier: Apache-2.0 + +use dashmap::DashMap; +use move_core_types::value::{MoveStructLayout, MoveTypeLayout}; +use once_cell::sync::Lazy; +use rustc_hash::FxHasher; +use std::{ + cell::{Cell, RefCell}, + collections::HashMap, + hash::{Hash, Hasher}, +}; + +type LayoutBucket = Vec<(MoveTypeLayout, bool)>; + +// Bucket count soft limit for the global cache. If exceeded, clear to avoid unbounded growth. +const GLOBAL_CACHE_SOFT_LIMIT: usize = 100_000; + +static GLOBAL_LAYOUT_IDENTIFIER_MAPPING_CACHE: Lazy> = + Lazy::new(DashMap::new); + +#[inline] +fn hash_layout(layout: &MoveTypeLayout) -> u64 { + let mut hasher = FxHasher::default(); + layout.hash(&mut hasher); + hasher.finish() +} + +#[inline] +fn lookup_bucket(bucket: &LayoutBucket, layout: &MoveTypeLayout) -> Option { + bucket + .iter() + .find(|(cached_layout, _)| cached_layout == layout) + .map(|(_, value)| *value) +} + +#[inline] +fn insert_bucket(bucket: &mut LayoutBucket, layout: &MoveTypeLayout, value: bool) { + if lookup_bucket(bucket, layout).is_none() { + bucket.push((layout.clone(), value)); + } +} + +#[inline] +fn maybe_trim_global_cache() { + if GLOBAL_LAYOUT_IDENTIFIER_MAPPING_CACHE.len() > GLOBAL_CACHE_SOFT_LIMIT { + GLOBAL_LAYOUT_IDENTIFIER_MAPPING_CACHE.clear(); + } +} + +pub(crate) fn compute_layout_has_identifier_mappings(layout: &MoveTypeLayout) -> bool { + match layout { + MoveTypeLayout::Native(..) => true, + MoveTypeLayout::Vector(inner) => compute_layout_has_identifier_mappings(inner), + MoveTypeLayout::Struct(struct_layout) => match struct_layout { + MoveStructLayout::Runtime(fields) => { + fields.iter().any(compute_layout_has_identifier_mappings) + } + MoveStructLayout::WithFields(fields) => fields + .iter() + .any(|field| compute_layout_has_identifier_mappings(&field.layout)), + MoveStructLayout::WithTypes { fields, .. } => fields + .iter() + .any(|field| compute_layout_has_identifier_mappings(&field.layout)), + }, + _ => false, + } +} + +#[derive(Default)] +pub(crate) struct LayoutIdentifierMappingCache { + // Fast-path for repeated checks on the same long-lived layout reference. + // In vm-runtime callsites, layout references come from resolver/loader and are stable. + last_layout_ptr: Cell, + last_value: Cell, + has_last: Cell, + local_entries: RefCell>, +} + +impl LayoutIdentifierMappingCache { + pub(crate) fn has_identifier_mappings(&self, layout: &MoveTypeLayout) -> bool { + let ptr = layout as *const MoveTypeLayout as usize; + if self.has_last.get() && self.last_layout_ptr.get() == ptr { + return self.last_value.get(); + } + + let key = hash_layout(layout); + + if let Some(cached) = self + .local_entries + .borrow() + .get(&key) + .and_then(|bucket| lookup_bucket(bucket, layout)) + { + self.last_layout_ptr.set(ptr); + self.last_value.set(cached); + self.has_last.set(true); + return cached; + } + + if let Some(cached) = GLOBAL_LAYOUT_IDENTIFIER_MAPPING_CACHE + .get(&key) + .and_then(|bucket| lookup_bucket(bucket.value(), layout)) + { + self.local_entries + .borrow_mut() + .entry(key) + .and_modify(|bucket| insert_bucket(bucket, layout, cached)) + .or_insert_with(|| vec![(layout.clone(), cached)]); + self.last_layout_ptr.set(ptr); + self.last_value.set(cached); + self.has_last.set(true); + return cached; + } + + let computed = compute_layout_has_identifier_mappings(layout); + self.local_entries + .borrow_mut() + .entry(key) + .and_modify(|bucket| insert_bucket(bucket, layout, computed)) + .or_insert_with(|| vec![(layout.clone(), computed)]); + GLOBAL_LAYOUT_IDENTIFIER_MAPPING_CACHE + .entry(key) + .and_modify(|bucket| insert_bucket(bucket, layout, computed)) + .or_insert_with(|| vec![(layout.clone(), computed)]); + maybe_trim_global_cache(); + self.last_layout_ptr.set(ptr); + self.last_value.set(computed); + self.has_last.set(true); + computed + } +} + +#[cfg(test)] +mod tests { + use super::*; + use move_core_types::{ + identifier::Identifier, + language_storage::StructTag, + value::{IdentifierMappingKind, MoveFieldLayout}, + }; + + fn id(name: &str) -> Identifier { + Identifier::new(name).unwrap() + } + + fn runtime_native_layout() -> MoveTypeLayout { + MoveTypeLayout::Struct(MoveStructLayout::Runtime(vec![ + MoveTypeLayout::U64, + MoveTypeLayout::Native( + IdentifierMappingKind::Aggregator, + Box::new(MoveTypeLayout::U128), + ), + ])) + } + + fn with_fields_native_layout() -> MoveTypeLayout { + MoveTypeLayout::Struct(MoveStructLayout::WithFields(vec![ + MoveFieldLayout::new(id("a"), MoveTypeLayout::U64), + MoveFieldLayout::new( + id("b"), + MoveTypeLayout::Native( + IdentifierMappingKind::Snapshot, + Box::new(MoveTypeLayout::U128), + ), + ), + ])) + } + + fn with_types_native_layout() -> MoveTypeLayout { + MoveTypeLayout::Struct(MoveStructLayout::WithTypes { + type_: StructTag { + address: move_core_types::account_address::AccountAddress::ONE, + module: id("M"), + name: id("S"), + type_args: vec![], + }, + fields: vec![ + MoveFieldLayout::new(id("x"), MoveTypeLayout::U8), + MoveFieldLayout::new( + id("y"), + MoveTypeLayout::Native( + IdentifierMappingKind::DerivedString, + Box::new(MoveTypeLayout::U64), + ), + ), + ], + }) + } + + #[test] + fn test_layout_identifier_mapping_cache_matches_compute() { + let cache = LayoutIdentifierMappingCache::default(); + let layouts = vec![ + MoveTypeLayout::U64, + runtime_native_layout(), + with_fields_native_layout(), + with_types_native_layout(), + MoveTypeLayout::Vector(Box::new(MoveTypeLayout::Bool)), + ]; + + for layout in &layouts { + let expected = compute_layout_has_identifier_mappings(layout); + let cached_first = cache.has_identifier_mappings(layout); + let cached_second = cache.has_identifier_mappings(layout); + assert_eq!(expected, cached_first); + assert_eq!(cached_first, cached_second); + } + } +} diff --git a/vm2/vm-runtime/src/lib.rs b/vm2/vm-runtime/src/lib.rs index 8bcebedbe7..9203666732 100644 --- a/vm2/vm-runtime/src/lib.rs +++ b/vm2/vm-runtime/src/lib.rs @@ -16,6 +16,7 @@ use starcoin_gas_schedule::{ mod access_path_cache; mod errors; +mod layout_identifier_mapping_cache; pub mod move_vm_ext; pub mod parallel_executor; mod verifier; diff --git a/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs b/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs index 1396ab9502..a2b2c9bab0 100644 --- a/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs +++ b/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs @@ -9,7 +9,7 @@ use move_core_types::account_address::AccountAddress; use move_core_types::language_storage::{ModuleId, StructTag}; use move_core_types::metadata::Metadata; use move_core_types::resolver::{resource_size, ModuleResolver, ResourceResolver}; -use move_core_types::value::{MoveStructLayout, MoveTypeLayout}; +use move_core_types::value::MoveTypeLayout; use move_table_extension::{TableHandle, TableResolver}; use move_vm_types::delayed_values::delayed_field_id::{ DelayedFieldID, ExtractUniqueIndex, TryFromMoveValue, @@ -36,11 +36,12 @@ use starcoin_vm_types::state_store::{ state_value::StateValueMetadata, StateView, TStateView, }; use starcoin_vm_types::write_set::{TransactionWrite, WriteOp}; -use std::cell::{Cell, RefCell}; +use std::cell::RefCell; use std::collections::{BTreeMap, HashMap, HashSet}; use std::sync::Arc; use crate::data_cache::get_resource_group_member_from_metadata; +use crate::layout_identifier_mapping_cache::LayoutIdentifierMappingCache; use crate::move_vm_ext::{resource_state_key, AsExecutorView, ResourceGroupResolver}; use crate::parallel_executor::{ParallelStateKey, ParallelStateValue}; @@ -164,57 +165,6 @@ struct GroupReadInfo { layouts: BTreeMap>, } -fn compute_layout_has_identifier_mappings(layout: &MoveTypeLayout) -> bool { - match layout { - MoveTypeLayout::Native(..) => true, - MoveTypeLayout::Vector(inner) => compute_layout_has_identifier_mappings(inner), - MoveTypeLayout::Struct(struct_layout) => match struct_layout { - MoveStructLayout::Runtime(fields) => { - fields.iter().any(compute_layout_has_identifier_mappings) - } - MoveStructLayout::WithFields(fields) => fields - .iter() - .any(|field| compute_layout_has_identifier_mappings(&field.layout)), - MoveStructLayout::WithTypes { fields, .. } => fields - .iter() - .any(|field| compute_layout_has_identifier_mappings(&field.layout)), - }, - _ => false, - } -} - -#[derive(Default)] -struct LayoutIdentifierMappingCache { - last_key: Cell, - last_value: Cell, - has_last: Cell, - entries: RefCell>, -} - -impl LayoutIdentifierMappingCache { - fn has_identifier_mappings(&self, layout: &MoveTypeLayout) -> bool { - let key = layout as *const MoveTypeLayout as usize; - if self.has_last.get() && self.last_key.get() == key { - return self.last_value.get(); - } - - if let Some(cached) = self.entries.borrow().get(&key) { - let value = *cached; - self.last_key.set(key); - self.last_value.set(value); - self.has_last.set(true); - return value; - } - - let computed = compute_layout_has_identifier_mappings(layout); - self.entries.borrow_mut().insert(key, computed); - self.last_key.set(key); - self.last_value.set(computed); - self.has_last.set(true); - computed - } -} - pub(crate) struct VersionedView<'a, S: StateView> { base_view: &'a S, hashmap_view: &'a MVHashMapView<'a, ParallelStateKey, ParallelStateValue>, From 3c6255c71e0b9d4c08155552a412f04d69a33ecf Mon Sep 17 00:00:00 2001 From: jackzhhuang Date: Fri, 10 Apr 2026 01:23:19 +0800 Subject: [PATCH 05/11] feat(vm2): align move rev and switch layout mapping cache to move-vm-types --- vm2/vm-runtime/src/data_cache.rs | 10 +- .../src/layout_identifier_mapping_cache.rs | 210 ------------------ vm2/vm-runtime/src/lib.rs | 1 - .../src/parallel_executor/storage_wrapper.rs | 4 +- 4 files changed, 7 insertions(+), 218 deletions(-) delete mode 100644 vm2/vm-runtime/src/layout_identifier_mapping_cache.rs diff --git a/vm2/vm-runtime/src/data_cache.rs b/vm2/vm-runtime/src/data_cache.rs index a835f13b24..f05f9c3391 100644 --- a/vm2/vm-runtime/src/data_cache.rs +++ b/vm2/vm-runtime/src/data_cache.rs @@ -18,6 +18,9 @@ use move_vm_runtime::config::DEFAULT_MAX_VALUE_NEST_DEPTH; use move_vm_types::delayed_values::delayed_field_id::{ DelayedFieldID, ExtractUniqueIndex, ExtractWidth, TryFromMoveValue, }; +#[cfg(test)] +use move_vm_types::layout_identifier_mapping::compute_layout_has_identifier_mappings; +use move_vm_types::layout_identifier_mapping::LayoutIdentifierMappingCache; use move_vm_types::loaded_data::runtime_types::TypeBuilder; use move_vm_types::value_serde::{ValueSerDeContext, ValueToIdentifierMapping}; use move_vm_types::value_traversal::find_identifiers_in_value; @@ -64,9 +67,6 @@ use std::{ ops::{Deref, DerefMut}, }; -#[cfg(test)] -use crate::layout_identifier_mapping_cache::compute_layout_has_identifier_mappings; -use crate::layout_identifier_mapping_cache::LayoutIdentifierMappingCache; use crate::parallel_executor::{ materialize_events, materialize_resource_write_set, storage_wrapper::DelayedFieldCache, }; @@ -260,7 +260,7 @@ impl TStateView for StateViewCache<'_, S> { impl<'a, S: StateView> StorageAdapter<'a, S> { fn layout_has_identifier_mappings(&self, layout: &MoveTypeLayout) -> bool { self.layout_identifier_mapping_cache - .has_identifier_mappings(layout) + .has_identifier_mappings_stable_ref(layout) } pub fn new( @@ -1214,7 +1214,7 @@ pub(crate) mod tests { let start_cached = Instant::now(); let mut cached_acc = 0usize; for _ in 0..iterations { - let hit = cache.has_identifier_mappings(&layout); + let hit = cache.has_identifier_mappings_stable_ref(&layout); cached_acc ^= usize::from(black_box(hit)); } let cached_elapsed = start_cached.elapsed(); diff --git a/vm2/vm-runtime/src/layout_identifier_mapping_cache.rs b/vm2/vm-runtime/src/layout_identifier_mapping_cache.rs deleted file mode 100644 index bb6ae651ab..0000000000 --- a/vm2/vm-runtime/src/layout_identifier_mapping_cache.rs +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (c) The Starcoin Core Contributors -// SPDX-License-Identifier: Apache-2.0 - -use dashmap::DashMap; -use move_core_types::value::{MoveStructLayout, MoveTypeLayout}; -use once_cell::sync::Lazy; -use rustc_hash::FxHasher; -use std::{ - cell::{Cell, RefCell}, - collections::HashMap, - hash::{Hash, Hasher}, -}; - -type LayoutBucket = Vec<(MoveTypeLayout, bool)>; - -// Bucket count soft limit for the global cache. If exceeded, clear to avoid unbounded growth. -const GLOBAL_CACHE_SOFT_LIMIT: usize = 100_000; - -static GLOBAL_LAYOUT_IDENTIFIER_MAPPING_CACHE: Lazy> = - Lazy::new(DashMap::new); - -#[inline] -fn hash_layout(layout: &MoveTypeLayout) -> u64 { - let mut hasher = FxHasher::default(); - layout.hash(&mut hasher); - hasher.finish() -} - -#[inline] -fn lookup_bucket(bucket: &LayoutBucket, layout: &MoveTypeLayout) -> Option { - bucket - .iter() - .find(|(cached_layout, _)| cached_layout == layout) - .map(|(_, value)| *value) -} - -#[inline] -fn insert_bucket(bucket: &mut LayoutBucket, layout: &MoveTypeLayout, value: bool) { - if lookup_bucket(bucket, layout).is_none() { - bucket.push((layout.clone(), value)); - } -} - -#[inline] -fn maybe_trim_global_cache() { - if GLOBAL_LAYOUT_IDENTIFIER_MAPPING_CACHE.len() > GLOBAL_CACHE_SOFT_LIMIT { - GLOBAL_LAYOUT_IDENTIFIER_MAPPING_CACHE.clear(); - } -} - -pub(crate) fn compute_layout_has_identifier_mappings(layout: &MoveTypeLayout) -> bool { - match layout { - MoveTypeLayout::Native(..) => true, - MoveTypeLayout::Vector(inner) => compute_layout_has_identifier_mappings(inner), - MoveTypeLayout::Struct(struct_layout) => match struct_layout { - MoveStructLayout::Runtime(fields) => { - fields.iter().any(compute_layout_has_identifier_mappings) - } - MoveStructLayout::WithFields(fields) => fields - .iter() - .any(|field| compute_layout_has_identifier_mappings(&field.layout)), - MoveStructLayout::WithTypes { fields, .. } => fields - .iter() - .any(|field| compute_layout_has_identifier_mappings(&field.layout)), - }, - _ => false, - } -} - -#[derive(Default)] -pub(crate) struct LayoutIdentifierMappingCache { - // Fast-path for repeated checks on the same long-lived layout reference. - // In vm-runtime callsites, layout references come from resolver/loader and are stable. - last_layout_ptr: Cell, - last_value: Cell, - has_last: Cell, - local_entries: RefCell>, -} - -impl LayoutIdentifierMappingCache { - pub(crate) fn has_identifier_mappings(&self, layout: &MoveTypeLayout) -> bool { - let ptr = layout as *const MoveTypeLayout as usize; - if self.has_last.get() && self.last_layout_ptr.get() == ptr { - return self.last_value.get(); - } - - let key = hash_layout(layout); - - if let Some(cached) = self - .local_entries - .borrow() - .get(&key) - .and_then(|bucket| lookup_bucket(bucket, layout)) - { - self.last_layout_ptr.set(ptr); - self.last_value.set(cached); - self.has_last.set(true); - return cached; - } - - if let Some(cached) = GLOBAL_LAYOUT_IDENTIFIER_MAPPING_CACHE - .get(&key) - .and_then(|bucket| lookup_bucket(bucket.value(), layout)) - { - self.local_entries - .borrow_mut() - .entry(key) - .and_modify(|bucket| insert_bucket(bucket, layout, cached)) - .or_insert_with(|| vec![(layout.clone(), cached)]); - self.last_layout_ptr.set(ptr); - self.last_value.set(cached); - self.has_last.set(true); - return cached; - } - - let computed = compute_layout_has_identifier_mappings(layout); - self.local_entries - .borrow_mut() - .entry(key) - .and_modify(|bucket| insert_bucket(bucket, layout, computed)) - .or_insert_with(|| vec![(layout.clone(), computed)]); - GLOBAL_LAYOUT_IDENTIFIER_MAPPING_CACHE - .entry(key) - .and_modify(|bucket| insert_bucket(bucket, layout, computed)) - .or_insert_with(|| vec![(layout.clone(), computed)]); - maybe_trim_global_cache(); - self.last_layout_ptr.set(ptr); - self.last_value.set(computed); - self.has_last.set(true); - computed - } -} - -#[cfg(test)] -mod tests { - use super::*; - use move_core_types::{ - identifier::Identifier, - language_storage::StructTag, - value::{IdentifierMappingKind, MoveFieldLayout}, - }; - - fn id(name: &str) -> Identifier { - Identifier::new(name).unwrap() - } - - fn runtime_native_layout() -> MoveTypeLayout { - MoveTypeLayout::Struct(MoveStructLayout::Runtime(vec![ - MoveTypeLayout::U64, - MoveTypeLayout::Native( - IdentifierMappingKind::Aggregator, - Box::new(MoveTypeLayout::U128), - ), - ])) - } - - fn with_fields_native_layout() -> MoveTypeLayout { - MoveTypeLayout::Struct(MoveStructLayout::WithFields(vec![ - MoveFieldLayout::new(id("a"), MoveTypeLayout::U64), - MoveFieldLayout::new( - id("b"), - MoveTypeLayout::Native( - IdentifierMappingKind::Snapshot, - Box::new(MoveTypeLayout::U128), - ), - ), - ])) - } - - fn with_types_native_layout() -> MoveTypeLayout { - MoveTypeLayout::Struct(MoveStructLayout::WithTypes { - type_: StructTag { - address: move_core_types::account_address::AccountAddress::ONE, - module: id("M"), - name: id("S"), - type_args: vec![], - }, - fields: vec![ - MoveFieldLayout::new(id("x"), MoveTypeLayout::U8), - MoveFieldLayout::new( - id("y"), - MoveTypeLayout::Native( - IdentifierMappingKind::DerivedString, - Box::new(MoveTypeLayout::U64), - ), - ), - ], - }) - } - - #[test] - fn test_layout_identifier_mapping_cache_matches_compute() { - let cache = LayoutIdentifierMappingCache::default(); - let layouts = vec![ - MoveTypeLayout::U64, - runtime_native_layout(), - with_fields_native_layout(), - with_types_native_layout(), - MoveTypeLayout::Vector(Box::new(MoveTypeLayout::Bool)), - ]; - - for layout in &layouts { - let expected = compute_layout_has_identifier_mappings(layout); - let cached_first = cache.has_identifier_mappings(layout); - let cached_second = cache.has_identifier_mappings(layout); - assert_eq!(expected, cached_first); - assert_eq!(cached_first, cached_second); - } - } -} diff --git a/vm2/vm-runtime/src/lib.rs b/vm2/vm-runtime/src/lib.rs index 9203666732..8bcebedbe7 100644 --- a/vm2/vm-runtime/src/lib.rs +++ b/vm2/vm-runtime/src/lib.rs @@ -16,7 +16,6 @@ use starcoin_gas_schedule::{ mod access_path_cache; mod errors; -mod layout_identifier_mapping_cache; pub mod move_vm_ext; pub mod parallel_executor; mod verifier; diff --git a/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs b/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs index a2b2c9bab0..3168ed12ea 100644 --- a/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs +++ b/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs @@ -14,6 +14,7 @@ use move_table_extension::{TableHandle, TableResolver}; use move_vm_types::delayed_values::delayed_field_id::{ DelayedFieldID, ExtractUniqueIndex, TryFromMoveValue, }; +use move_vm_types::layout_identifier_mapping::LayoutIdentifierMappingCache; use move_vm_types::value_serde::{ValueSerDeContext, ValueToIdentifierMapping}; use move_vm_types::value_traversal::find_identifiers_in_value; use starcoin_aggregator::bounded_math::{BoundedMath, SignedU128}; @@ -41,7 +42,6 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::sync::Arc; use crate::data_cache::get_resource_group_member_from_metadata; -use crate::layout_identifier_mapping_cache::LayoutIdentifierMappingCache; use crate::move_vm_ext::{resource_state_key, AsExecutorView, ResourceGroupResolver}; use crate::parallel_executor::{ParallelStateKey, ParallelStateValue}; @@ -180,7 +180,7 @@ pub(crate) struct VersionedView<'a, S: StateView> { impl<'a, S: StateView> VersionedView<'a, S> { fn layout_has_identifier_mappings(&self, layout: &MoveTypeLayout) -> bool { self.layout_identifier_mapping_cache - .has_identifier_mappings(layout) + .has_identifier_mappings_stable_ref(layout) } pub fn new( From 653e758bd5e31eacaf61325c121979be13e6f062 Mon Sep 17 00:00:00 2001 From: jackzhhuang Date: Mon, 13 Apr 2026 08:42:55 +0800 Subject: [PATCH 06/11] vm2: migrate group layout cache path and bump move rev --- vm2/vm-runtime/src/data_cache.rs | 34 ++---- vm2/vm-runtime/src/parallel_executor/mod.rs | 101 ++++++++---------- .../src/parallel_executor/storage_wrapper.rs | 86 +++++++++++---- .../src/parallel_executor/vm_wrapper.rs | 11 +- 4 files changed, 117 insertions(+), 115 deletions(-) diff --git a/vm2/vm-runtime/src/data_cache.rs b/vm2/vm-runtime/src/data_cache.rs index f05f9c3391..41c7ceb223 100644 --- a/vm2/vm-runtime/src/data_cache.rs +++ b/vm2/vm-runtime/src/data_cache.rs @@ -155,7 +155,6 @@ struct GroupReadInfo { metadata: StateValueMetadata, size: u64, delayed_ids: HashSet, - layouts: BTreeMap>, } /// Adapter to convert a `ExecutorView` into a `MoveResolver`. @@ -303,16 +302,6 @@ impl<'a, S: StateView> StorageAdapter<'a, S> { &self.delayed_fields } - pub fn take_group_read_layouts( - &self, - ) -> HashMap>> { - self.group_reads - .borrow_mut() - .drain() - .map(|(k, v)| (k, v.layouts)) - .collect() - } - fn generate_delayed_field_id(&self, width: u32) -> DelayedFieldID { let index = self.delayed_field_id_counter.fetch_add(1, Ordering::SeqCst); DelayedFieldID::new_with_width(index, width) @@ -381,24 +370,21 @@ impl<'a, S: StateView> StorageAdapter<'a, S> { if delayed_ids.is_empty() { return; } + self.delayed_field_cache.insert_group_member_layout( + group_key.clone(), + tag.clone(), + Arc::new(layout.clone()), + ); self.group_reads .borrow_mut() .entry(group_key.clone()) .and_modify(|existing| { existing.delayed_ids.extend(delayed_ids.iter().cloned()); - existing - .layouts - .insert(tag.clone(), Arc::new(layout.clone())); }) - .or_insert_with(|| { - let mut layouts = BTreeMap::new(); - layouts.insert(tag.clone(), Arc::new(layout.clone())); - GroupReadInfo { - metadata, - size, - delayed_ids, - layouts, - } + .or_insert_with(|| GroupReadInfo { + metadata, + size, + delayed_ids, }); } @@ -528,13 +514,11 @@ impl<'a, S: StateView> StorageAdapter<'a, S> { delayed_fields: &self.delayed_fields, txn_idx: 0, }; - let group_read_layouts = self.take_group_read_layouts(); let mut group_cache: HashMap> = HashMap::new(); let patched_resource_write_set = materialize_resource_write_set( &output, &mapping, &self.delayed_field_cache, - &group_read_layouts, self.executor_view, &mut group_cache, has_delayed, diff --git a/vm2/vm-runtime/src/parallel_executor/mod.rs b/vm2/vm-runtime/src/parallel_executor/mod.rs index 5d8a00f9a5..5805fe1a84 100644 --- a/vm2/vm-runtime/src/parallel_executor/mod.rs +++ b/vm2/vm-runtime/src/parallel_executor/mod.rs @@ -45,7 +45,7 @@ use starcoin_vm_types::{ state_store::state_key::StateKey, state_store::StateView, transaction::{Transaction, TransactionOutput, TransactionStatus}, - write_set::WriteOp, + write_set::{TransactionWrite, WriteOp}, }; use std::collections::{BTreeMap, HashMap, HashSet}; use std::sync::Arc; @@ -102,27 +102,15 @@ impl PTransaction for PreprocessedTransaction { // Wrapper to avoid orphan rule pub(crate) struct StarcoinTransactionOutput { output: VMOutput, - group_read_layouts: HashMap>>, } impl StarcoinTransactionOutput { - pub fn new( - output: VMOutput, - group_read_layouts: HashMap>>, - ) -> Self { - Self { - output, - group_read_layouts, - } + pub fn new(output: VMOutput) -> Self { + Self { output } } - pub fn into_inner( - self, - ) -> ( - VMOutput, - HashMap>>, - ) { - (self.output, self.group_read_layouts) + pub fn into_inner(self) -> VMOutput { + self.output } #[allow(dead_code)] @@ -214,10 +202,7 @@ impl PTransactionOutput for StarcoinTransactionOutput { /// Execution output for transactions that comes after SkipRest signal. fn skip_output() -> Self { - Self::new( - VMOutput::empty_with_status(TransactionStatus::Retry), - HashMap::new(), - ) + Self::new(VMOutput::empty_with_status(TransactionStatus::Retry)) } fn delayed_field_change_set( @@ -389,7 +374,7 @@ fn materialize_parallel_outputs( state_view: &S, max_value_nest_depth: Option, ) -> Result<(usize, TransactionOutput), VMStatus> { - let (vm_output, group_read_layouts) = output.into_inner(); + let vm_output = output.into_inner(); let has_delayed = vm_output.contains_delayed_fields(); let has_group_ops = vm_output .resource_write_set() @@ -416,7 +401,6 @@ fn materialize_parallel_outputs( &vm_output, &mapping, delayed_field_cache, - &group_read_layouts, state_view, &mut group_cache, has_delayed, @@ -545,7 +529,7 @@ fn materialize_parallel_outputs( let mut results = Vec::with_capacity(ordered_indices.len()); for txn_idx in ordered_indices { let txn_output = if let Some(output) = sequential_outputs.remove(&txn_idx) { - let (mut vm_output, group_read_layouts) = output.into_inner(); + let mut vm_output = output.into_inner(); let has_delayed = vm_output.contains_delayed_fields(); let has_agg_v1 = !vm_output.aggregator_v1_delta_set().is_empty(); let has_group_ops = vm_output @@ -572,7 +556,6 @@ fn materialize_parallel_outputs( &vm_output, &mapping, &delayed_field_cache, - &group_read_layouts, &state_cache, &mut group_cache, has_delayed, @@ -692,7 +675,6 @@ pub(crate) fn materialize_resource_write_set( output: &VMOutput, mapping: &impl ValueToIdentifierMapping, delayed_field_cache: &DelayedFieldCache, - group_read_layouts: &HashMap>>, state_view: &S, group_cache: &mut HashMap>, materialize_delayed: bool, @@ -747,22 +729,32 @@ pub(crate) fn materialize_resource_write_set( )? } AbstractResourceWriteOp::WriteResourceGroup(group_write) => { - if materialize_delayed { - for (tag, (inner_op, _)) in group_write.inner_ops() { - match inner_op { - WriteOp::Creation { data, .. } | WriteOp::Modification { data, .. } => { - delayed_field_cache.insert_group_member_value( + for (tag, (inner_op, layout)) in group_write.inner_ops() { + match inner_op { + WriteOp::Creation { data, .. } | WriteOp::Modification { data, .. } => { + delayed_field_cache.insert_group_member_value( + key.clone(), + tag.clone(), + data.clone(), + ); + if let Some(layout) = layout { + delayed_field_cache.insert_group_member_layout( key.clone(), tag.clone(), - data.clone(), + layout.clone(), ); } - WriteOp::Deletion { .. } => { - delayed_field_cache.remove_group_member_value(key, tag); - } + } + WriteOp::Deletion { .. } => { + delayed_field_cache.remove_group_member_value(key, tag); + delayed_field_cache.remove_group_member_layout(key, tag); } } } + if group_write.metadata_op().is_deletion() { + delayed_field_cache.clear_group_member_values(key); + delayed_field_cache.clear_group_member_layouts(key); + } materialize_group_write( key, group_write, @@ -790,7 +782,6 @@ pub(crate) fn materialize_resource_write_set( metadata, mapping, delayed_field_cache, - group_read_layouts, state_view, group_cache, max_value_nest_depth, @@ -925,25 +916,26 @@ fn materialize_group_in_place( metadata: &starcoin_vm_types::state_store::state_value::StateValueMetadata, mapping: &impl ValueToIdentifierMapping, delayed_field_cache: &DelayedFieldCache, - group_read_layouts: &HashMap>>, state_view: &S, group_cache: &mut HashMap>, max_value_nest_depth: Option, ) -> Result { let group_map = load_group_map_cached(state_view, group_cache, key)?; - let layouts = group_read_layouts.get(key).ok_or_else(|| { - VMStatus::error( - StatusCode::DELAYED_MATERIALIZATION_CODE_INVARIANT_ERROR, - Some(format!( - "Missing group read layouts for delayed field exchange: {:?}", - key - )), - ) - })?; + let layouts = delayed_field_cache + .get_group_member_layouts(key) + .ok_or_else(|| { + VMStatus::error( + StatusCode::DELAYED_MATERIALIZATION_CODE_INVARIANT_ERROR, + Some(format!( + "Missing cached group member layouts for delayed field exchange: {:?}", + key + )), + ) + })?; for (tag, layout) in layouts { let cached = delayed_field_cache - .get_group_member_value(key, tag) + .get_group_member_value(key, &tag) .ok_or_else(|| { VMStatus::error( StatusCode::DELAYED_MATERIALIZATION_CODE_INVARIANT_ERROR, @@ -955,7 +947,7 @@ fn materialize_group_in_place( })?; let bytes = materialize_bytes_force(&cached, layout.as_ref(), mapping, max_value_nest_depth)?; - group_map.insert(tag.clone(), bytes); + group_map.insert(tag, bytes); } Ok(WriteOp::Modification { @@ -1287,10 +1279,7 @@ mod tests { TransactionStatus::Keep(KeptVMStatus::Executed), TransactionAuxiliaryData::None, ); - outputs.push(( - txn_idx, - StarcoinTransactionOutput::new(vm_output, HashMap::new()), - )); + outputs.push((txn_idx, StarcoinTransactionOutput::new(vm_output))); continue; } @@ -1337,10 +1326,7 @@ mod tests { TransactionStatus::Keep(KeptVMStatus::Executed), TransactionAuxiliaryData::None, ); - outputs.push(( - txn_idx, - StarcoinTransactionOutput::new(vm_output, HashMap::new()), - )); + outputs.push((txn_idx, StarcoinTransactionOutput::new(vm_output))); } let mut state_data = HashMap::new(); @@ -1365,7 +1351,7 @@ mod tests { let mut results = Vec::with_capacity(outputs.len()); for (txn_idx, output) in outputs.into_iter() { - let (mut vm_output, group_read_layouts) = output.into_inner(); + let mut vm_output = output.into_inner(); let has_delayed = vm_output.contains_delayed_fields(); let has_agg_v1 = !vm_output.aggregator_v1_delta_set().is_empty(); let has_group_ops = vm_output.resource_write_set().values().any(|op| { @@ -1395,7 +1381,6 @@ mod tests { &vm_output, &mapping, &delayed_field_cache, - &group_read_layouts, &state_cache, &mut group_cache, has_delayed, diff --git a/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs b/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs index 3168ed12ea..5ef0a1ea31 100644 --- a/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs +++ b/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs @@ -64,6 +64,7 @@ impl CachedWriteOp { pub(crate) struct DelayedFieldCache { base_values: DashMap, group_member_values: DashMap<(StateKey, StructTag), Bytes>, + group_member_layouts: DashMap>>, } impl DelayedFieldCache { @@ -147,6 +148,59 @@ impl DelayedFieldCache { .group_member_values .remove(&(group_key.clone(), tag.clone())); } + + pub fn clear_group_member_values(&self, group_key: &StateKey) { + let keys: Vec<_> = self + .group_member_values + .iter() + .filter(|entry| &entry.key().0 == group_key) + .map(|entry| entry.key().clone()) + .collect(); + for key in keys { + let _ = self.group_member_values.remove(&key); + } + } + + pub fn insert_group_member_layout( + &self, + group_key: StateKey, + tag: StructTag, + layout: Arc, + ) { + match self.group_member_layouts.entry(group_key) { + dashmap::mapref::entry::Entry::Occupied(mut entry) => { + entry.get_mut().insert(tag, layout); + } + dashmap::mapref::entry::Entry::Vacant(entry) => { + let mut layouts = BTreeMap::new(); + layouts.insert(tag, layout); + entry.insert(layouts); + } + } + } + + pub fn get_group_member_layouts( + &self, + group_key: &StateKey, + ) -> Option>> { + self.group_member_layouts + .get(group_key) + .map(|entry| entry.clone()) + } + + pub fn remove_group_member_layout(&self, group_key: &StateKey, tag: &StructTag) { + if let Some(mut layouts) = self.group_member_layouts.get_mut(group_key) { + layouts.remove(tag); + if layouts.is_empty() { + drop(layouts); + let _ = self.group_member_layouts.remove(group_key); + } + } + } + + pub fn clear_group_member_layouts(&self, group_key: &StateKey) { + let _ = self.group_member_layouts.remove(group_key); + } } #[derive(Clone)] @@ -162,7 +216,6 @@ struct GroupReadInfo { metadata: StateValueMetadata, size: u64, delayed_ids: HashSet, - layouts: BTreeMap>, } pub(crate) struct VersionedView<'a, S: StateView> { @@ -229,16 +282,6 @@ impl<'a, S: StateView> VersionedView<'a, S> { .and_then(|value| value.as_group_size()) } - pub fn take_group_read_layouts( - &self, - ) -> HashMap>> { - self.group_reads - .borrow_mut() - .drain() - .map(|(k, v)| (k, v.layouts)) - .collect() - } - fn record_resource_read( &self, key: &StateKey, @@ -278,24 +321,21 @@ impl<'a, S: StateView> VersionedView<'a, S> { if delayed_ids.is_empty() { return; } + self.delayed_field_cache.insert_group_member_layout( + group_key.clone(), + tag.clone(), + Arc::new(layout.clone()), + ); self.group_reads .borrow_mut() .entry(group_key.clone()) .and_modify(|existing| { existing.delayed_ids.extend(delayed_ids.iter().cloned()); - existing - .layouts - .insert(tag.clone(), Arc::new(layout.clone())); }) - .or_insert_with(|| { - let mut layouts = BTreeMap::new(); - layouts.insert(tag.clone(), Arc::new(layout.clone())); - GroupReadInfo { - metadata, - size, - delayed_ids, - layouts, - } + .or_insert_with(|| GroupReadInfo { + metadata, + size, + delayed_ids, }); } diff --git a/vm2/vm-runtime/src/parallel_executor/vm_wrapper.rs b/vm2/vm-runtime/src/parallel_executor/vm_wrapper.rs index f71aa5c03f..a1444b57d0 100644 --- a/vm2/vm-runtime/src/parallel_executor/vm_wrapper.rs +++ b/vm2/vm-runtime/src/parallel_executor/vm_wrapper.rs @@ -82,17 +82,10 @@ impl<'a, S: 'a + StateView + Sync> ExecutorTask for StarcoinVMWrapper<'a, S> { } }; } - let group_read_layouts = versioned_view.take_group_read_layouts(); if StarcoinVM::should_restart_execution(&output) { - ExecutionStatus::SkipRest(StarcoinTransactionOutput::new( - output, - group_read_layouts, - )) + ExecutionStatus::SkipRest(StarcoinTransactionOutput::new(output)) } else { - ExecutionStatus::Success(StarcoinTransactionOutput::new( - output, - group_read_layouts, - )) + ExecutionStatus::Success(StarcoinTransactionOutput::new(output)) } } Err(err) => ExecutionStatus::Abort(err), From 0aaf3d379b1d81f1d3004bc629d82fe9f7584069 Mon Sep 17 00:00:00 2001 From: jackzhhuang Date: Fri, 17 Apr 2026 16:14:17 +0800 Subject: [PATCH 07/11] fix(vm2): restore local layout mapping cache compatibility --- vm2/vm-runtime/src/data_cache.rs | 4 +- .../src/layout_identifier_mapping_cache.rs | 215 ++++++++++++++++++ vm2/vm-runtime/src/lib.rs | 1 + .../src/parallel_executor/storage_wrapper.rs | 2 +- 4 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 vm2/vm-runtime/src/layout_identifier_mapping_cache.rs diff --git a/vm2/vm-runtime/src/data_cache.rs b/vm2/vm-runtime/src/data_cache.rs index 41c7ceb223..01b7c886f1 100644 --- a/vm2/vm-runtime/src/data_cache.rs +++ b/vm2/vm-runtime/src/data_cache.rs @@ -19,8 +19,8 @@ use move_vm_types::delayed_values::delayed_field_id::{ DelayedFieldID, ExtractUniqueIndex, ExtractWidth, TryFromMoveValue, }; #[cfg(test)] -use move_vm_types::layout_identifier_mapping::compute_layout_has_identifier_mappings; -use move_vm_types::layout_identifier_mapping::LayoutIdentifierMappingCache; +use crate::layout_identifier_mapping_cache::compute_layout_has_identifier_mappings; +use crate::layout_identifier_mapping_cache::LayoutIdentifierMappingCache; use move_vm_types::loaded_data::runtime_types::TypeBuilder; use move_vm_types::value_serde::{ValueSerDeContext, ValueToIdentifierMapping}; use move_vm_types::value_traversal::find_identifiers_in_value; diff --git a/vm2/vm-runtime/src/layout_identifier_mapping_cache.rs b/vm2/vm-runtime/src/layout_identifier_mapping_cache.rs new file mode 100644 index 0000000000..2814d27032 --- /dev/null +++ b/vm2/vm-runtime/src/layout_identifier_mapping_cache.rs @@ -0,0 +1,215 @@ +// Copyright (c) The Starcoin Core Contributors +// SPDX-License-Identifier: Apache-2.0 + +use dashmap::DashMap; +use move_core_types::value::{MoveStructLayout, MoveTypeLayout}; +use once_cell::sync::Lazy; +use rustc_hash::FxHasher; +use std::{ + cell::{Cell, RefCell}, + collections::HashMap, + hash::{Hash, Hasher}, +}; + +type LayoutBucket = Vec<(MoveTypeLayout, bool)>; + +// Bucket count soft limit for the global cache. If exceeded, clear to avoid unbounded growth. +const GLOBAL_CACHE_SOFT_LIMIT: usize = 100_000; + +static GLOBAL_LAYOUT_IDENTIFIER_MAPPING_CACHE: Lazy> = + Lazy::new(DashMap::new); + +#[inline] +fn hash_layout(layout: &MoveTypeLayout) -> u64 { + let mut hasher = FxHasher::default(); + layout.hash(&mut hasher); + hasher.finish() +} + +#[inline] +fn lookup_bucket(bucket: &LayoutBucket, layout: &MoveTypeLayout) -> Option { + bucket + .iter() + .find(|(cached_layout, _)| cached_layout == layout) + .map(|(_, value)| *value) +} + +#[inline] +fn insert_bucket(bucket: &mut LayoutBucket, layout: &MoveTypeLayout, value: bool) { + if lookup_bucket(bucket, layout).is_none() { + bucket.push((layout.clone(), value)); + } +} + +#[inline] +fn maybe_trim_global_cache() { + if GLOBAL_LAYOUT_IDENTIFIER_MAPPING_CACHE.len() > GLOBAL_CACHE_SOFT_LIMIT { + GLOBAL_LAYOUT_IDENTIFIER_MAPPING_CACHE.clear(); + } +} + +pub(crate) fn compute_layout_has_identifier_mappings(layout: &MoveTypeLayout) -> bool { + match layout { + MoveTypeLayout::Native(..) => true, + MoveTypeLayout::Vector(inner) => compute_layout_has_identifier_mappings(inner), + MoveTypeLayout::Struct(struct_layout) => match struct_layout { + MoveStructLayout::Runtime(fields) => { + fields.iter().any(compute_layout_has_identifier_mappings) + } + MoveStructLayout::WithFields(fields) => fields + .iter() + .any(|field| compute_layout_has_identifier_mappings(&field.layout)), + MoveStructLayout::WithTypes { fields, .. } => fields + .iter() + .any(|field| compute_layout_has_identifier_mappings(&field.layout)), + }, + _ => false, + } +} + +#[derive(Default)] +pub(crate) struct LayoutIdentifierMappingCache { + // Fast-path for repeated checks on the same long-lived layout reference. + // In vm-runtime callsites, layout references come from resolver/loader and are stable. + last_layout_ptr: Cell, + last_value: Cell, + has_last: Cell, + local_entries: RefCell>, +} + +impl LayoutIdentifierMappingCache { + pub(crate) fn has_identifier_mappings(&self, layout: &MoveTypeLayout) -> bool { + let ptr = layout as *const MoveTypeLayout as usize; + if self.has_last.get() && self.last_layout_ptr.get() == ptr { + return self.last_value.get(); + } + + let key = hash_layout(layout); + + if let Some(cached) = self + .local_entries + .borrow() + .get(&key) + .and_then(|bucket| lookup_bucket(bucket, layout)) + { + self.last_layout_ptr.set(ptr); + self.last_value.set(cached); + self.has_last.set(true); + return cached; + } + + if let Some(cached) = GLOBAL_LAYOUT_IDENTIFIER_MAPPING_CACHE + .get(&key) + .and_then(|bucket| lookup_bucket(bucket.value(), layout)) + { + self.local_entries + .borrow_mut() + .entry(key) + .and_modify(|bucket| insert_bucket(bucket, layout, cached)) + .or_insert_with(|| vec![(layout.clone(), cached)]); + self.last_layout_ptr.set(ptr); + self.last_value.set(cached); + self.has_last.set(true); + return cached; + } + + let computed = compute_layout_has_identifier_mappings(layout); + self.local_entries + .borrow_mut() + .entry(key) + .and_modify(|bucket| insert_bucket(bucket, layout, computed)) + .or_insert_with(|| vec![(layout.clone(), computed)]); + GLOBAL_LAYOUT_IDENTIFIER_MAPPING_CACHE + .entry(key) + .and_modify(|bucket| insert_bucket(bucket, layout, computed)) + .or_insert_with(|| vec![(layout.clone(), computed)]); + maybe_trim_global_cache(); + self.last_layout_ptr.set(ptr); + self.last_value.set(computed); + self.has_last.set(true); + computed + } + + #[inline] + pub(crate) fn has_identifier_mappings_stable_ref(&self, layout: &MoveTypeLayout) -> bool { + self.has_identifier_mappings(layout) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use move_core_types::{ + identifier::Identifier, + language_storage::StructTag, + value::{IdentifierMappingKind, MoveFieldLayout}, + }; + + fn id(name: &str) -> Identifier { + Identifier::new(name).unwrap() + } + + fn runtime_native_layout() -> MoveTypeLayout { + MoveTypeLayout::Struct(MoveStructLayout::Runtime(vec![ + MoveTypeLayout::U64, + MoveTypeLayout::Native( + IdentifierMappingKind::Aggregator, + Box::new(MoveTypeLayout::U128), + ), + ])) + } + + fn with_fields_native_layout() -> MoveTypeLayout { + MoveTypeLayout::Struct(MoveStructLayout::WithFields(vec![ + MoveFieldLayout::new(id("a"), MoveTypeLayout::U64), + MoveFieldLayout::new( + id("b"), + MoveTypeLayout::Native( + IdentifierMappingKind::Snapshot, + Box::new(MoveTypeLayout::U128), + ), + ), + ])) + } + + fn with_types_native_layout() -> MoveTypeLayout { + MoveTypeLayout::Struct(MoveStructLayout::WithTypes { + type_: StructTag { + address: move_core_types::account_address::AccountAddress::ONE, + module: id("M"), + name: id("S"), + type_args: vec![], + }, + fields: vec![ + MoveFieldLayout::new(id("x"), MoveTypeLayout::U8), + MoveFieldLayout::new( + id("y"), + MoveTypeLayout::Native( + IdentifierMappingKind::DerivedString, + Box::new(MoveTypeLayout::U64), + ), + ), + ], + }) + } + + #[test] + fn test_layout_identifier_mapping_cache_matches_compute() { + let cache = LayoutIdentifierMappingCache::default(); + let layouts = vec![ + MoveTypeLayout::U64, + runtime_native_layout(), + with_fields_native_layout(), + with_types_native_layout(), + MoveTypeLayout::Vector(Box::new(MoveTypeLayout::Bool)), + ]; + + for layout in &layouts { + let expected = compute_layout_has_identifier_mappings(layout); + let cached_first = cache.has_identifier_mappings(layout); + let cached_second = cache.has_identifier_mappings(layout); + assert_eq!(expected, cached_first); + assert_eq!(cached_first, cached_second); + } + } +} diff --git a/vm2/vm-runtime/src/lib.rs b/vm2/vm-runtime/src/lib.rs index 8bcebedbe7..9203666732 100644 --- a/vm2/vm-runtime/src/lib.rs +++ b/vm2/vm-runtime/src/lib.rs @@ -16,6 +16,7 @@ use starcoin_gas_schedule::{ mod access_path_cache; mod errors; +mod layout_identifier_mapping_cache; pub mod move_vm_ext; pub mod parallel_executor; mod verifier; diff --git a/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs b/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs index 5ef0a1ea31..be0728bbf1 100644 --- a/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs +++ b/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs @@ -14,7 +14,7 @@ use move_table_extension::{TableHandle, TableResolver}; use move_vm_types::delayed_values::delayed_field_id::{ DelayedFieldID, ExtractUniqueIndex, TryFromMoveValue, }; -use move_vm_types::layout_identifier_mapping::LayoutIdentifierMappingCache; +use crate::layout_identifier_mapping_cache::LayoutIdentifierMappingCache; use move_vm_types::value_serde::{ValueSerDeContext, ValueToIdentifierMapping}; use move_vm_types::value_traversal::find_identifiers_in_value; use starcoin_aggregator::bounded_math::{BoundedMath, SignedU128}; From 714999ec875f2bd73c19539d5fcc211d3de9560a Mon Sep 17 00:00:00 2001 From: jackzhhuang Date: Fri, 17 Apr 2026 17:09:23 +0800 Subject: [PATCH 08/11] rebase and fix clippy --- vm2/vm-runtime/src/data_cache.rs | 6 +++--- vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vm2/vm-runtime/src/data_cache.rs b/vm2/vm-runtime/src/data_cache.rs index 01b7c886f1..f5724028ad 100644 --- a/vm2/vm-runtime/src/data_cache.rs +++ b/vm2/vm-runtime/src/data_cache.rs @@ -3,6 +3,9 @@ //! Scratchpad for on chain values during the execution. use crate::default_gas_schedule; +#[cfg(test)] +use crate::layout_identifier_mapping_cache::compute_layout_has_identifier_mappings; +use crate::layout_identifier_mapping_cache::LayoutIdentifierMappingCache; use crate::move_vm_ext::{resource_state_key, AsExecutorView, ResourceGroupResolver}; use bytes::Bytes; use move_binary_format::deserializer::DeserializerConfig; @@ -18,9 +21,6 @@ use move_vm_runtime::config::DEFAULT_MAX_VALUE_NEST_DEPTH; use move_vm_types::delayed_values::delayed_field_id::{ DelayedFieldID, ExtractUniqueIndex, ExtractWidth, TryFromMoveValue, }; -#[cfg(test)] -use crate::layout_identifier_mapping_cache::compute_layout_has_identifier_mappings; -use crate::layout_identifier_mapping_cache::LayoutIdentifierMappingCache; use move_vm_types::loaded_data::runtime_types::TypeBuilder; use move_vm_types::value_serde::{ValueSerDeContext, ValueToIdentifierMapping}; use move_vm_types::value_traversal::find_identifiers_in_value; diff --git a/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs b/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs index be0728bbf1..10029e61df 100644 --- a/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs +++ b/vm2/vm-runtime/src/parallel_executor/storage_wrapper.rs @@ -1,6 +1,7 @@ // Copyright (c) The Starcoin Core Contributors // SPDX-License-Identifier: Apache-2.0 +use crate::layout_identifier_mapping_cache::LayoutIdentifierMappingCache; use bytes::Bytes; use dashmap::DashMap; use move_binary_format::errors::PartialVMError; @@ -14,7 +15,6 @@ use move_table_extension::{TableHandle, TableResolver}; use move_vm_types::delayed_values::delayed_field_id::{ DelayedFieldID, ExtractUniqueIndex, TryFromMoveValue, }; -use crate::layout_identifier_mapping_cache::LayoutIdentifierMappingCache; use move_vm_types::value_serde::{ValueSerDeContext, ValueToIdentifierMapping}; use move_vm_types::value_traversal::find_identifiers_in_value; use starcoin_aggregator::bounded_math::{BoundedMath, SignedU128}; From 07b60dda2f8cb8aeac21f02dcd5db8111e6dc997 Mon Sep 17 00:00:00 2001 From: jackzhhuang Date: Fri, 17 Apr 2026 17:32:57 +0800 Subject: [PATCH 09/11] fix(vm2): address remaining PR 4839 review threads --- sync/starcoin-execute-bench/src/main.rs | 141 ++++++++++++++---- sync/starcoin-execute-bench/src/results.rs | 4 +- .../src/layout_identifier_mapping_cache.rs | 21 +-- 3 files changed, 113 insertions(+), 53 deletions(-) diff --git a/sync/starcoin-execute-bench/src/main.rs b/sync/starcoin-execute-bench/src/main.rs index b9df2cf981..2c6d516a9a 100644 --- a/sync/starcoin-execute-bench/src/main.rs +++ b/sync/starcoin-execute-bench/src/main.rs @@ -31,7 +31,7 @@ use starcoin_pipeline_timing::{clear_timing, disable_timing, enable_timing, glob use starcoin_service_registry::{ ActorService, EventHandler, RegistryAsyncService, ServiceContext, ServiceFactory, ServiceRef, }; -use starcoin_storage::{BlockStore, Storage, Storage2, Store}; +use starcoin_storage::{BlockStore, BlockTransactionInfoStore, Storage, Storage2, Store}; use starcoin_transaction_builder::vm2::{ build_batch_transfer_txn as build_batch_transfer_txn2, raw_peer_to_peer_txn, }; @@ -43,6 +43,7 @@ use starcoin_types::{ multi_transaction::MultiSignedUserTransaction, system_events::{MinedBlock, NewHeadBlock}, transaction::StcTransactionInfo, + vm_error::KeptVMStatus as Vm1KeptVMStatus, }; use starcoin_vm2_account_api::{ message::{AccountRequest, AccountResponse}, @@ -56,7 +57,10 @@ use starcoin_vm2_types::{ transaction::{RawUserTransaction as RawUserTransaction2, SignedUserTransaction}, }; use starcoin_vm2_vm_runtime::starcoin_vm::StarcoinVM; -use starcoin_vm2_vm_types::{account_address::AccountAddress, state_view::StateReaderExt}; +use starcoin_vm2_vm_types::{ + account_address::AccountAddress, state_view::StateReaderExt, + vm_status::KeptVMStatus as Vm2KeptVMStatus, +}; use tempfile::TempDir; use test_helper::run_node_with_all_service; @@ -73,6 +77,7 @@ struct PreparedBenchMeta { const PREPARED_SIGNED_TXNS_FILE: &str = "signed_txns.json"; const PREPARED_META_FILE: &str = "bench_meta.json"; const PREPARED_CHAIN_DATA_DIR: &str = "chain_data"; +const FUNDING_BATCH_SIZE: usize = 10; #[derive(Debug, Parser)] #[command(about = "Execute the full build-and-execute benchmark outside of tests.")] @@ -961,6 +966,7 @@ async fn wait_for_sufficient_balance( initial_gas_fee: u128, gas_price: u64, max_gas: u64, + funding_batch_size: usize, chain_reader_service: ServiceRef, storage1: Arc, storage2: Arc, @@ -993,9 +999,12 @@ async fn wait_for_sufficient_balance( continue; } }; + let receiver_count = account_count as usize; + let funding_txn_count = receiver_count.div_ceil(funding_batch_size.max(1)) as u128; let per_tx_fee = max_gas as u128 * gas_price as u128; - let needed_balance = - account_count as u128 * (initial_balance + per_tx_fee) + initial_gas_fee; + let total_transfer = account_count as u128 * initial_balance; + let total_gas_fee = funding_txn_count * per_tx_fee; + let needed_balance = total_transfer + total_gas_fee + initial_gas_fee; if association_balance >= needed_balance { info!( "Association account has sufficient balance: {} >= {}", @@ -1317,16 +1326,16 @@ impl BenchmarkState { /// Each batch uses batch_user_count users, where first half sends to second half. /// Supports multiple rounds where the same accounts are reused with incrementing sequence numbers. /// Returns None if all batches have been sent. - fn build_next_batch(&self, expire_time: u64) -> Option> { + fn build_next_batch(&self, expire_time: u64) -> Result)>> { if self.batch_user_count == 0 || self.total_batches == 0 { - return None; + return Ok(None); } let batch_index = self.batch_index.fetch_add(1, Ordering::SeqCst); // Check if we've completed all rounds let total_batch_count = self.total_batches * self.rounds; if batch_index >= total_batch_count { - return None; + return Ok(None); } // Calculate which batch within a round and which round we're in @@ -1356,13 +1365,15 @@ impl BenchmarkState { self.chain_id, self.simple_transfer, ) { - Ok(txns) => Some(txns), + Ok(txns) => Ok(Some((batch_index, txns))), Err(e) => { + // Revert the cursor on build error so this batch can be retried. + self.batch_index.fetch_sub(1, Ordering::SeqCst); error!( "Failed to build batch {} (round {}, batch_in_round {}): {:?}", batch_index, round, batch_in_round, e ); - None + Err(e) } } } @@ -1431,6 +1442,7 @@ async fn execute_benchmark( initial_gas_fee, gas_price, max_gas, + FUNDING_BATCH_SIZE, chain_reader_service.clone(), storage1.clone(), storage2.clone(), @@ -1453,7 +1465,7 @@ async fn execute_benchmark( ); // Funding batch size: 10 receivers per transaction (~13.8M gas) - let batch_size = 10usize; + let batch_size = FUNDING_BATCH_SIZE; let estimated_funding_txns = receivers.len().div_ceil(batch_size); info!( "Funding batch plan: receivers={}, batch_size={}, estimated_funding_txns={}, funding_max_gas={}, txpool_max_per_sender={}", @@ -1537,7 +1549,7 @@ async fn execute_benchmark( let phase_start = std::time::Instant::now(); for _ in 0..total_batches { - if let Some(batch) = benchmark_state.build_next_batch(expire_time) { + if let Some((_batch_index, batch)) = benchmark_state.build_next_batch(expire_time)? { all_transactions.extend(batch); } } @@ -1682,6 +1694,7 @@ async fn prepare_benchmark_state( initial_gas_fee, gas_price, max_gas, + FUNDING_BATCH_SIZE, chain_reader_service.clone(), storage1.clone(), storage2.clone(), @@ -1701,7 +1714,7 @@ async fn prepare_benchmark_state( account_creation_ms, account_count ); - let batch_size = 10usize; + let batch_size = FUNDING_BATCH_SIZE; let phase_start = std::time::Instant::now(); transfer_to_accounts( &receivers, @@ -1750,7 +1763,7 @@ async fn prepare_benchmark_state( let phase_start = std::time::Instant::now(); for _ in 0..total_batches { - if let Some(batch) = benchmark_state.build_next_batch(expire_time) { + if let Some((_batch_index, batch)) = benchmark_state.build_next_batch(expire_time)? { all_transactions.extend(batch); } } @@ -2035,7 +2048,7 @@ async fn sign_and_import_transactions( signed_transactions.push(signed_transaction); } - txpool.add_txns_multi_signed( + let import_results = txpool.add_txns_multi_signed( signed_transactions .into_iter() .map(MultiSignedUserTransaction::VM2) @@ -2043,6 +2056,21 @@ async fn sign_and_import_transactions( false, None, )?; + let imported_count = import_results.iter().filter(|r| r.is_ok()).count(); + if imported_count != import_results.len() { + let sample_errors: Vec = import_results + .iter() + .filter_map(|r| r.as_ref().err()) + .take(5) + .map(|e| format!("{:?}", e)) + .collect(); + bail!( + "user tx import incomplete: imported {}/{}. sample errors: {}", + imported_count, + import_results.len(), + sample_errors.join(", ") + ); + } Ok(txn_hashes) } @@ -2186,7 +2214,7 @@ fn sign_and_import_transactions_sync( signed_transactions.push(signed_transaction); } - txpool.add_txns_multi_signed( + let import_results = txpool.add_txns_multi_signed( signed_transactions .into_iter() .map(MultiSignedUserTransaction::VM2) @@ -2194,6 +2222,21 @@ fn sign_and_import_transactions_sync( false, None, )?; + let imported_count = import_results.iter().filter(|r| r.is_ok()).count(); + if imported_count != import_results.len() { + let sample_errors: Vec = import_results + .iter() + .filter_map(|r| r.as_ref().err()) + .take(5) + .map(|e| format!("{:?}", e)) + .collect(); + bail!( + "user tx import incomplete: imported {}/{}. sample errors: {}", + imported_count, + import_results.len(), + sample_errors.join(", ") + ); + } Ok::, anyhow::Error>(txn_hashes) }) @@ -2223,6 +2266,23 @@ impl ObserverService { }) } + fn is_txn_executed_in_block(&self, txn_hash: HashValue, block_id: HashValue) -> Result { + let infos = self.storage1.get_transaction_info_by_txn_hash(txn_hash)?; + Ok(infos.into_iter().any(|info| { + if info.block_id != block_id { + return false; + } + match info.transaction_info { + StcTransactionInfo::V1(txn_info) => { + matches!(txn_info.status(), Vm1KeptVMStatus::Executed) + } + StcTransactionInfo::V2(txn_info) => { + matches!(txn_info.status(), Vm2KeptVMStatus::Executed) + } + } + })) + } + fn try_submit_next_batch(&self) -> Result<()> { let state = match &self.benchmark_state { Some(s) => s, @@ -2248,7 +2308,7 @@ impl ObserverService { let expire_time = config.net().time_service().now_secs() + 3600; // Build next batch - each batch uses different users with seq=0 - let batch = match state.build_next_batch(expire_time) { + let (batch_index, batch) = match state.build_next_batch(expire_time)? { Some(b) => b, None => { info!("All batches have been sent"); @@ -2256,8 +2316,14 @@ impl ObserverService { } }; - let batch_index = state.batch_index.load(Ordering::SeqCst).saturating_sub(1); - let txn_hashes = sign_and_import_transactions_sync(&batch, account_service, txpool)?; + let txn_hashes = match sign_and_import_transactions_sync(&batch, account_service, txpool) { + Ok(hashes) => hashes, + Err(e) => { + // Revert cursor if submit failed so this batch can be retried. + state.batch_index.fetch_sub(1, Ordering::SeqCst); + return Err(e); + } + }; state.add_txn_hashes(&txn_hashes); info!( "Submitted batch {}/{}: {} transactions", @@ -2291,15 +2357,17 @@ impl ObserverService { // Only record benchmark transactions for TPS calculation if let Some(ref state) = self.benchmark_state { if state.is_benchmark_txn(&txn_hash) { - self.transaction_data.entry(txn_hash).or_default().push( - TransactionExecutionResult::Executed( - connected_time_ms, - block_number, - block_id, - block_timestamp_ms, - ), - ); - benchmark_txn_count += 1; + if self.is_txn_executed_in_block(txn_hash, block_id)? { + self.transaction_data.entry(txn_hash).or_default().push( + TransactionExecutionResult::Executed( + connected_time_ms, + block_number, + block_id, + block_timestamp_ms, + ), + ); + benchmark_txn_count += 1; + } } } } @@ -2339,10 +2407,12 @@ impl ObserverService { // Only record benchmark transactions if let Some(ref state) = self.benchmark_state { if state.is_benchmark_txn(&txn_hash) { - self.transaction_data.entry(txn_hash).or_default().push( - TransactionExecutionResult::Mined(mined_time_ms, block_number, block_id), - ); - benchmark_txn_count += 1; + if self.is_txn_executed_in_block(txn_hash, block_id)? { + self.transaction_data.entry(txn_hash).or_default().push( + TransactionExecutionResult::Mined(mined_time_ms, block_number, block_id), + ); + benchmark_txn_count += 1; + } } } } @@ -2494,6 +2564,15 @@ impl EventHandler> for ObserverService { .as_millis() as u64; for transaction_event in msg.as_ref() { + let should_record = self + .benchmark_state + .as_ref() + .map(|state| state.is_benchmark_txn(&transaction_event.0)) + .unwrap_or(false); + if !should_record { + continue; + } + match &transaction_event.1 { starcoin_types::transaction::TxStatus::Added => self .transaction_data diff --git a/sync/starcoin-execute-bench/src/results.rs b/sync/starcoin-execute-bench/src/results.rs index 3e0d1aa2f3..2287bc809c 100644 --- a/sync/starcoin-execute-bench/src/results.rs +++ b/sync/starcoin-execute-bench/src/results.rs @@ -757,7 +757,7 @@ impl<'a> ResultsDumper<'a> { fn draw_latency_chart( &self, - area: &DrawingArea, + area: &DrawingArea, plotters::coord::Shift>, executions: &[(HashValue, u64, f64)], unique_txn_count: usize, duplicate_exec_count: usize, @@ -919,7 +919,7 @@ impl<'a> ResultsDumper<'a> { fn draw_block_txn_chart( &self, - area: &DrawingArea, + area: &DrawingArea, plotters::coord::Shift>, block_stats: &[(u64, usize)], ) -> Result<(), Box> { if block_stats.is_empty() { diff --git a/vm2/vm-runtime/src/layout_identifier_mapping_cache.rs b/vm2/vm-runtime/src/layout_identifier_mapping_cache.rs index 2814d27032..cbaf6d9ab7 100644 --- a/vm2/vm-runtime/src/layout_identifier_mapping_cache.rs +++ b/vm2/vm-runtime/src/layout_identifier_mapping_cache.rs @@ -6,7 +6,7 @@ use move_core_types::value::{MoveStructLayout, MoveTypeLayout}; use once_cell::sync::Lazy; use rustc_hash::FxHasher; use std::{ - cell::{Cell, RefCell}, + cell::RefCell, collections::HashMap, hash::{Hash, Hasher}, }; @@ -69,21 +69,11 @@ pub(crate) fn compute_layout_has_identifier_mappings(layout: &MoveTypeLayout) -> #[derive(Default)] pub(crate) struct LayoutIdentifierMappingCache { - // Fast-path for repeated checks on the same long-lived layout reference. - // In vm-runtime callsites, layout references come from resolver/loader and are stable. - last_layout_ptr: Cell, - last_value: Cell, - has_last: Cell, local_entries: RefCell>, } impl LayoutIdentifierMappingCache { pub(crate) fn has_identifier_mappings(&self, layout: &MoveTypeLayout) -> bool { - let ptr = layout as *const MoveTypeLayout as usize; - if self.has_last.get() && self.last_layout_ptr.get() == ptr { - return self.last_value.get(); - } - let key = hash_layout(layout); if let Some(cached) = self @@ -92,9 +82,6 @@ impl LayoutIdentifierMappingCache { .get(&key) .and_then(|bucket| lookup_bucket(bucket, layout)) { - self.last_layout_ptr.set(ptr); - self.last_value.set(cached); - self.has_last.set(true); return cached; } @@ -107,9 +94,6 @@ impl LayoutIdentifierMappingCache { .entry(key) .and_modify(|bucket| insert_bucket(bucket, layout, cached)) .or_insert_with(|| vec![(layout.clone(), cached)]); - self.last_layout_ptr.set(ptr); - self.last_value.set(cached); - self.has_last.set(true); return cached; } @@ -124,9 +108,6 @@ impl LayoutIdentifierMappingCache { .and_modify(|bucket| insert_bucket(bucket, layout, computed)) .or_insert_with(|| vec![(layout.clone(), computed)]); maybe_trim_global_cache(); - self.last_layout_ptr.set(ptr); - self.last_value.set(computed); - self.has_last.set(true); computed } From 9fb4d5ba7b89661769dc70da7f9ec1984a568ad9 Mon Sep 17 00:00:00 2001 From: jackzhhuang Date: Fri, 17 Apr 2026 20:40:13 +0800 Subject: [PATCH 10/11] fix clippy --- sync/starcoin-execute-bench/src/main.rs | 43 +++++++++++++------------ 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/sync/starcoin-execute-bench/src/main.rs b/sync/starcoin-execute-bench/src/main.rs index 2c6d516a9a..4c01a5bdbb 100644 --- a/sync/starcoin-execute-bench/src/main.rs +++ b/sync/starcoin-execute-bench/src/main.rs @@ -1326,7 +1326,10 @@ impl BenchmarkState { /// Each batch uses batch_user_count users, where first half sends to second half. /// Supports multiple rounds where the same accounts are reused with incrementing sequence numbers. /// Returns None if all batches have been sent. - fn build_next_batch(&self, expire_time: u64) -> Result)>> { + fn build_next_batch( + &self, + expire_time: u64, + ) -> Result)>> { if self.batch_user_count == 0 || self.total_batches == 0 { return Ok(None); } @@ -2356,18 +2359,18 @@ impl ObserverService { // Only record benchmark transactions for TPS calculation if let Some(ref state) = self.benchmark_state { - if state.is_benchmark_txn(&txn_hash) { - if self.is_txn_executed_in_block(txn_hash, block_id)? { - self.transaction_data.entry(txn_hash).or_default().push( - TransactionExecutionResult::Executed( - connected_time_ms, - block_number, - block_id, - block_timestamp_ms, - ), - ); - benchmark_txn_count += 1; - } + if state.is_benchmark_txn(&txn_hash) + && self.is_txn_executed_in_block(txn_hash, block_id)? + { + self.transaction_data.entry(txn_hash).or_default().push( + TransactionExecutionResult::Executed( + connected_time_ms, + block_number, + block_id, + block_timestamp_ms, + ), + ); + benchmark_txn_count += 1; } } } @@ -2406,13 +2409,13 @@ impl ObserverService { // Only record benchmark transactions if let Some(ref state) = self.benchmark_state { - if state.is_benchmark_txn(&txn_hash) { - if self.is_txn_executed_in_block(txn_hash, block_id)? { - self.transaction_data.entry(txn_hash).or_default().push( - TransactionExecutionResult::Mined(mined_time_ms, block_number, block_id), - ); - benchmark_txn_count += 1; - } + if state.is_benchmark_txn(&txn_hash) + && self.is_txn_executed_in_block(txn_hash, block_id)? + { + self.transaction_data.entry(txn_hash).or_default().push( + TransactionExecutionResult::Mined(mined_time_ms, block_number, block_id), + ); + benchmark_txn_count += 1; } } } From b59e8633938613bfbd72be6e41224e8373ec67b4 Mon Sep 17 00:00:00 2001 From: jackzhhuang Date: Sat, 18 Apr 2026 04:02:57 +0800 Subject: [PATCH 11/11] fix test_equal_difficulty_branch_still_produce_block --- chain/src/chain.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/chain/src/chain.rs b/chain/src/chain.rs index 939864fda0..0f2f5a661a 100644 --- a/chain/src/chain.rs +++ b/chain/src/chain.rs @@ -411,13 +411,16 @@ impl BlockChain { author_array.copy_from_slice(&author_bytes[..16]); let author_v2 = starcoin_vm2_types::account_address::AccountAddress::new(author_array); + // Build execution state from the selected parent, not from current main head. + // In equal-difficulty DAG branches, selected parent may differ from self.statedb head. + let parent_multi_state = self.storage.0.get_vm_multi_state(parent_header.id())?; let chain_state = ChainStateDB::new( self.storage.0.clone().into_super_arc(), - Some(self.statedb.0.state_root()), + Some(parent_multi_state.state_root1()), ); let chain_state2 = ChainStateDB2::new( self.storage.1.clone().into_super_arc(), - Some(self.statedb.1.state_root()), + Some(parent_multi_state.state_root2()), ); let mut opened_block = OpenedBlock::new(