diff --git a/third_party/move/move-vm/runtime/src/data_cache.rs b/third_party/move/move-vm/runtime/src/data_cache.rs index 8c314eb980d4f..9d0775dfc723d 100644 --- a/third_party/move/move-vm/runtime/src/data_cache.rs +++ b/third_party/move/move-vm/runtime/src/data_cache.rs @@ -18,7 +18,7 @@ use move_core_types::{ effects::{AccountChanges, ChangeSet, Changes}, gas_algebra::NumBytes, language_storage::{StructTag, TypeTag}, - value::MoveTypeLayout, + value::{MoveStructLayout, MoveTypeLayout}, vm_status::StatusCode, }; use move_vm_types::{ @@ -173,25 +173,48 @@ impl TransactionDataCache { )? }; - let function_value_extension = FunctionValueExtensionAdapter { module_storage }; let (layout, contains_delayed_fields) = layout_with_delayed_fields.unpack(); let value = match data { Some(blob) => { + let function_value_extension = FunctionValueExtensionAdapter { module_storage }; let max_value_nest_depth = function_value_extension.max_value_nest_depth(); - let val = ValueSerDeContext::new(max_value_nest_depth) - .with_func_args_deserialization(&function_value_extension) - .with_delayed_fields_serde() - .deserialize(&blob, &layout) - .ok_or_else(|| { - let msg = format!( - "Failed to deserialize resource {} at {}!", - struct_tag.to_canonical_string(), - addr - ); - PartialVMError::new(StatusCode::FAILED_TO_DESERIALIZE_RESOURCE) - .with_message(msg) - })?; - GlobalValue::cached(val)? + if contains_delayed_fields || Self::layout_contains_functions(&layout) { + // Need function value extension, directly parse here. + let val = ValueSerDeContext::new(max_value_nest_depth) + .with_func_args_deserialization(&function_value_extension) + .with_delayed_fields_serde() + .deserialize(&blob, &layout) + .ok_or_else(|| { + let msg = format!( + "Failed to deserialize resource {} at {}!", + struct_tag.to_canonical_string(), + addr + ); + PartialVMError::new(StatusCode::FAILED_TO_DESERIALIZE_RESOURCE) + .with_message(msg) + })?; + GlobalValue::cached(val)? + } else { + // Do not need function value extension, can delay deserialization. + let layout = layout.clone(); // too many clones for error case only + let blob = blob.to_vec(); + let addr = *addr; + let struct_tag = struct_tag.clone(); + let deserializer = move || { + ValueSerDeContext::new(max_value_nest_depth) + .deserialize(&blob, &layout) + .ok_or_else(|| { + let msg = format!( + "Failed to deserialize resource {} at {}!", + struct_tag.to_canonical_string(), + addr + ); + PartialVMError::new(StatusCode::FAILED_TO_DESERIALIZE_RESOURCE) + .with_message(msg) + }) + }; + GlobalValue::fetched(deserializer)? + } }, None => GlobalValue::none(), }; @@ -205,6 +228,41 @@ impl TransactionDataCache { Ok((entry, NumBytes::new(bytes_loaded as u64))) } + // HACK for benchmarking + fn layout_contains_functions(ly: &MoveTypeLayout) -> bool { + match ly { + MoveTypeLayout::Function => true, + MoveTypeLayout::Bool + | MoveTypeLayout::U8 + | MoveTypeLayout::U64 + | MoveTypeLayout::U128 + | MoveTypeLayout::Address + | MoveTypeLayout::Signer + | MoveTypeLayout::U16 + | MoveTypeLayout::U32 + | MoveTypeLayout::U256 + | MoveTypeLayout::Native(_, _) => false, + MoveTypeLayout::Vector(ly) => Self::layout_contains_functions(ly), + MoveTypeLayout::Struct(sly) => { + match sly { + MoveStructLayout::Runtime(fields) => { + fields.iter().any(Self::layout_contains_functions) + }, + MoveStructLayout::RuntimeVariants(variants) => variants + .iter() + .flatten() + .any(Self::layout_contains_functions), + MoveStructLayout::WithFields(_) + | MoveStructLayout::WithTypes { .. } + | MoveStructLayout::WithVariants(_) => { + // overapproximate + true + }, + } + }, + } + } + /// Returns true if resource has been inserted into the cache. Otherwise, returns false. The /// state of the cache does not chang when calling this function. pub(crate) fn contains_resource(&self, addr: &AccountAddress, ty: &Type) -> bool { diff --git a/third_party/move/move-vm/types/src/values/value_depth_tests.rs b/third_party/move/move-vm/types/src/values/value_depth_tests.rs index 879ae162a31df..66b7b96c95725 100644 --- a/third_party/move/move-vm/types/src/values/value_depth_tests.rs +++ b/third_party/move/move-vm/types/src/values/value_depth_tests.rs @@ -74,7 +74,7 @@ fn test_copy_value() { test_unop_with_max_depth(|v, max_depth| v.copy_value_with_depth(max_depth)); // Special-case: reference clone Rcs, so their depth can be larger. - let v = assert_ok!(GlobalValue::cached(Value::struct_(Struct::pack(vec![ + let mut v = assert_ok!(GlobalValue::cached(Value::struct_(Struct::pack(vec![ Value::u8(0) ])))); let v_ref = assert_ok!(v.borrow_global()); @@ -85,7 +85,7 @@ fn test_copy_value() { #[test] fn test_read_ref() { - let v = assert_ok!(GlobalValue::cached(Value::struct_(Struct::pack(vec![ + let mut v = assert_ok!(GlobalValue::cached(Value::struct_(Struct::pack(vec![ Value::u8(0) ])))); let v_ref = assert_ok!(assert_ok!(v.borrow_global()).value_as::()); @@ -206,7 +206,7 @@ where assert_eq!(err.major_status(), StatusCode::VM_MAX_VALUE_DEPTH_REACHED); // Create a reference to struct with 1 field (3 nodes). - let v = assert_ok!(GlobalValue::cached(Value::struct_(Struct::pack(vec![ + let mut v = assert_ok!(GlobalValue::cached(Value::struct_(Struct::pack(vec![ Value::u8(0) ])))); let v_ref = assert_ok!(v.borrow_global()); diff --git a/third_party/move/move-vm/types/src/values/values_impl.rs b/third_party/move/move-vm/types/src/values/values_impl.rs index d7d48f5b7da4d..af2f0c1f85bb5 100644 --- a/third_party/move/move-vm/types/src/values/values_impl.rs +++ b/third_party/move/move-vm/types/src/values/values_impl.rs @@ -11,6 +11,7 @@ use crate::{ values::function_values_impl::{AbstractFunction, Closure, ClosureVisitor}, views::{ValueView, ValueVisitor}, }; +use derivative::Derivative; use itertools::Itertools; use move_binary_format::{ errors::*, @@ -239,10 +240,17 @@ pub struct VectorRef(ContainerRef); /// A special "slot" in global storage that can hold a resource. It also keeps track of the status /// of the resource relative to the global state, which is necessary to compute the effects to emit /// at the end of transaction execution. -#[derive(Debug)] +#[derive(Derivative)] +#[derivative(Debug)] enum GlobalValueImpl { /// No resource resides in this slot or in storage. None, + /// A resource has been fetched from storage but is not yet deserialized and not in the + /// cache. + Fetched { + #[derivative(Debug = "ignore")] + deserializer: Box PartialVMResult>, + }, /// A resource has been published to this slot and it did not previously exist in storage. Fresh { fields: Rc>> }, /// A resource resides in this slot and also in storage. The status flag indicates whether @@ -3365,6 +3373,14 @@ impl Struct { **************************************************************************************/ #[allow(clippy::unnecessary_wraps)] impl GlobalValueImpl { + fn fetched( + deserializer: impl FnOnce() -> PartialVMResult + 'static, + ) -> PartialVMResult { + Ok(GlobalValueImpl::Fetched { + deserializer: Box::new(deserializer), + }) + } + fn cached( val: ValueImpl, status: GlobalDataStatus, @@ -3393,7 +3409,22 @@ impl GlobalValueImpl { } } + fn ensure_cached(&mut self) -> PartialVMResult<()> { + if matches!(self, Self::Fetched { .. }) { + // For Rust semantics restrictions, need to replace data before we can consume. + let data = mem::replace(self, GlobalValueImpl::None); + let Self::Fetched { deserializer } = data else { + unreachable!() + }; + *self = Self::cached(deserializer()?.0, GlobalDataStatus::Clean).map_err(|_| { + PartialVMError::new_invariant_violation("unexpected lazy deserialization error") + })?; + } + Ok(()) + } + fn move_from(&mut self) -> PartialVMResult { + self.ensure_cached()?; let fields = match self { Self::None | Self::Deleted => { return Err(PartialVMError::new(StatusCode::MISSING_DATA)) @@ -3406,6 +3437,7 @@ impl GlobalValueImpl { Self::Cached { fields, .. } => fields, _ => unreachable!(), }, + Self::Fetched { .. } => unreachable!(), }; if Rc::strong_count(&fields) != 1 { return Err( @@ -3419,7 +3451,7 @@ impl GlobalValueImpl { fn move_to(&mut self, val: ValueImpl) -> Result<(), (PartialVMError, ValueImpl)> { match self { - Self::Fresh { .. } | Self::Cached { .. } => { + Self::Fresh { .. } | Self::Cached { .. } | Self::Fetched { .. } => { return Err(( PartialVMError::new(StatusCode::RESOURCE_ALREADY_EXISTS), val, @@ -3433,12 +3465,13 @@ impl GlobalValueImpl { fn exists(&self) -> PartialVMResult { match self { - Self::Fresh { .. } | Self::Cached { .. } => Ok(true), + Self::Fresh { .. } | Self::Cached { .. } | Self::Fetched { .. } => Ok(true), Self::None | Self::Deleted => Ok(false), } } - fn borrow_global(&self) -> PartialVMResult { + fn borrow_global(&mut self) -> PartialVMResult { + self.ensure_cached()?; match self { Self::None | Self::Deleted => Err(PartialVMError::new(StatusCode::MISSING_DATA)), Self::Fresh { fields } => Ok(ValueImpl::ContainerRef(ContainerRef::Local( @@ -3448,6 +3481,7 @@ impl GlobalValueImpl { container: Container::Struct(Rc::clone(fields)), status: Rc::clone(status), })), + Self::Fetched { .. } => unreachable!(), } } @@ -3464,6 +3498,7 @@ impl GlobalValueImpl { }, GlobalDataStatus::Clean => None, }, + Self::Fetched { .. } => None, } } @@ -3476,6 +3511,7 @@ impl GlobalValueImpl { GlobalDataStatus::Dirty => true, GlobalDataStatus::Clean => false, }, + Self::Fetched { .. } => false, } } } @@ -3485,6 +3521,12 @@ impl GlobalValue { Self(GlobalValueImpl::None) } + pub fn fetched( + deserializer: impl FnOnce() -> PartialVMResult + 'static, + ) -> PartialVMResult { + Ok(Self(GlobalValueImpl::fetched(deserializer)?)) + } + pub fn cached(val: Value) -> PartialVMResult { Ok(Self( GlobalValueImpl::cached(val.0, GlobalDataStatus::Clean).map_err(|(err, _val)| err)?, @@ -3501,7 +3543,7 @@ impl GlobalValue { .map_err(|(err, val)| (err, Value(val))) } - pub fn borrow_global(&self) -> PartialVMResult { + pub fn borrow_global(&mut self) -> PartialVMResult { Ok(Value(self.0.borrow_global()?)) } @@ -4752,7 +4794,7 @@ impl GlobalValue { } match &self.0 { - G::None | G::Deleted => None, + G::None | G::Deleted | G::Fetched { .. } => None, G::Cached { fields, .. } | G::Fresh { fields } => Some(Wrapper(fields)), } }