diff --git a/.gitignore b/.gitignore index 6f20a8ecdb..cbe15aa263 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ artifacts/ # Log files (e.g. memory profiling) *.log +dhat-heap.json diff --git a/Cargo.lock b/Cargo.lock index 79cf3599a5..c9b1d05ff3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -508,6 +508,7 @@ dependencies = [ "crc32fast", "criterion", "derivative", + "dhat", "glob", "hex", "hex-literal", @@ -521,6 +522,7 @@ dependencies = [ "target-lexicon", "tempfile", "thiserror", + "time", "wasmer", "wasmer-middlewares", "wat", @@ -808,6 +810,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", +] + [[package]] name = "derivative" version = "2.2.0" @@ -819,6 +830,22 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "dhat" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2aaf837aaf456f6706cb46386ba8dffd4013a757e36f4ea05c20dd46b209a3" +dependencies = [ + "backtrace", + "lazy_static", + "mintex", + "parking_lot", + "rustc-hash", + "serde", + "serde_json", + "thousands", +] + [[package]] name = "difflib" version = "0.4.0" @@ -1415,6 +1442,16 @@ dependencies = [ "adler", ] +[[package]] +name = "mintex" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7c5ba1c3b5a23418d7bbf98c71c3d4946a0125002129231da8d6b723d559cb" +dependencies = [ + "once_cell", + "sys-info", +] + [[package]] name = "more-asserts" version = "0.2.2" @@ -1483,6 +1520,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + [[package]] name = "parking_lot_core" version = "0.9.7" @@ -1546,6 +1593,12 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1828,6 +1881,12 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" version = "0.37.19" @@ -2127,6 +2186,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sys-info" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -2178,6 +2247,41 @@ dependencies = [ "syn 2.0.28", ] +[[package]] +name = "thousands" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" + +[[package]] +name = "time" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +dependencies = [ + "deranged", + "itoa", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + [[package]] name = "tinytemplate" version = "1.2.1" diff --git a/packages/vm/Cargo.toml b/packages/vm/Cargo.toml index 9a5a9794f8..3c7256f0e7 100644 --- a/packages/vm/Cargo.toml +++ b/packages/vm/Cargo.toml @@ -20,6 +20,8 @@ staking = ["cosmwasm-std/staking"] stargate = ["cosmwasm-std/stargate"] # Use cranelift backend instead of singlepass. This is required for development on Windows. cranelift = ["wasmer/cranelift"] +# For heap profiling. Only used in "memory" example. +dhat-heap = ["dep:dhat"] [lib] # See https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options @@ -43,6 +45,9 @@ thiserror = "1.0.26" wasmer = { version = "=4.2.5", default-features = false, features = ["cranelift", "singlepass"] } wasmer-middlewares = "=4.2.5" strum = { version = "0.25.0", default-features = false, features = ["derive"] } +# For heap profiling. Only used in "memory" example. This has to be a non-dev dependency +# because cargo currently does not support optional dev-dependencies. +dhat = { version = "0.3.2", optional = true } # Dependencies that we do not use ourself. We add those entries # to bump the min version of them. @@ -65,7 +70,15 @@ clap = "4" rand = "0.8" leb128 = "0.2" target-lexicon = "0.12" +time = { version = "0.3.28", features = ["formatting"] } [[bench]] name = "main" harness = false + +[[example]] +name = "memory" +path = "examples/memory.rs" + +[profile.release] +debug = 1 diff --git a/packages/vm/examples/memory.rs b/packages/vm/examples/memory.rs new file mode 100644 index 0000000000..0eb1ce921b --- /dev/null +++ b/packages/vm/examples/memory.rs @@ -0,0 +1,198 @@ +// Run with +// cargo run --features dhat-heap --example memory --release + +use std::time::SystemTime; +use tempfile::TempDir; +use time::{format_description::well_known::Rfc3339, OffsetDateTime}; + +use cosmwasm_std::{coins, Checksum, Empty}; +use cosmwasm_vm::testing::{mock_backend, mock_env, mock_info, MockApi, MockQuerier, MockStorage}; +use cosmwasm_vm::{ + call_execute, call_instantiate, capabilities_from_csv, Cache, CacheOptions, InstanceOptions, + Size, +}; + +#[cfg(feature = "dhat-heap")] +#[global_allocator] +static ALLOC: dhat::Alloc = dhat::Alloc; + +/// Number of seconds after which the test stops +const END_AFTER: u64 = 2 * 60; // seconds +const ROUNDS: usize = 1024; +const ROUND_LEN: usize = 16; + +// Instance +const DEFAULT_MEMORY_LIMIT: Size = Size::mebi(64); +const DEFAULT_GAS_LIMIT: u64 = u64::MAX; +const DEFAULT_INSTANCE_OPTIONS: InstanceOptions = InstanceOptions { + gas_limit: DEFAULT_GAS_LIMIT, +}; +// Cache +const MEMORY_CACHE_SIZE: Size = Size::mebi(5); + +struct Execute { + pub msg: &'static [u8], + pub expect_error: bool, +} + +struct Contract { + pub wasm: &'static [u8], + pub instantiate_msg: Option>, + pub execute_msgs: Vec, +} + +fn contracts() -> Vec { + let api = MockApi::default(); + let verifier = api.addr_make("verifies"); + let beneficiary = api.addr_make("benefits"); + vec![ + Contract { + wasm: include_bytes!("../testdata/cyberpunk.wasm"), + instantiate_msg: Some(b"{}".to_vec()), + execute_msgs: vec![ + Execute { + msg: br#"{"unreachable":{}}"#, + expect_error: true, + }, + Execute { + msg: br#"{"allocate_large_memory":{"pages":1000}}"#, + expect_error: false, + }, + Execute { + // mem_cost in KiB + msg: br#"{"argon2":{"mem_cost":256,"time_cost":1}}"#, + expect_error: false, + }, + Execute { + msg: br#"{"memory_loop":{}}"#, + expect_error: true, + }, + ], + }, + Contract { + wasm: include_bytes!("../testdata/hackatom.wasm"), + instantiate_msg: Some( + format!(r#"{{"verifier": "{verifier}", "beneficiary": "{beneficiary}"}}"#) + .into_bytes(), + ), + execute_msgs: vec![Execute { + msg: br#"{"release":{}}"#, + expect_error: false, + }], + }, + Contract { + wasm: include_bytes!("../testdata/hackatom_1.0.wasm"), + instantiate_msg: Some( + format!(r#"{{"verifier": "{verifier}", "beneficiary": "{beneficiary}"}}"#) + .into_bytes(), + ), + execute_msgs: vec![Execute { + msg: br#"{"release":{}}"#, + expect_error: false, + }], + }, + Contract { + wasm: include_bytes!("../testdata/ibc_reflect.wasm"), + instantiate_msg: None, + execute_msgs: vec![], + }, + ] +} + +#[allow(clippy::collapsible_else_if)] +fn app() { + let start_time = SystemTime::now(); + + let options = CacheOptions::new( + TempDir::new().unwrap().into_path(), + capabilities_from_csv("iterator,staking,stargate"), + MEMORY_CACHE_SIZE, + DEFAULT_MEMORY_LIMIT, + ); + + let contracts = contracts(); + + let checksums = { + let cache: Cache = + unsafe { Cache::new(options.clone()).unwrap() }; + + let mut checksums = Vec::::new(); + for contract in &contracts { + checksums.push(cache.save_wasm(contract.wasm).unwrap()); + } + checksums + }; + + let after = SystemTime::now().duration_since(start_time).unwrap(); + eprintln!("Done compiling after {after:?}"); + + let cache: Cache = unsafe { Cache::new(options).unwrap() }; + for round in 0..ROUNDS { + for _ in 0..ROUND_LEN { + if SystemTime::now() + .duration_since(start_time) + .unwrap() + .as_secs() + > END_AFTER + { + eprintln!("Round {round}. End time reached. Ending the process"); + + let metrics = cache.metrics(); + eprintln!("Cache metrics: {metrics:?}"); + + return; // ends app() + } + + for idx in 0..contracts.len() { + let mut instance = cache + .get_instance(&checksums[idx], mock_backend(&[]), DEFAULT_INSTANCE_OPTIONS) + .unwrap(); + + instance.set_debug_handler(|_msg, info| { + let _t = now_rfc3339(); + let _gas = info.gas_remaining; + //eprintln!("[{t}]: {msg} (gas remaining: {gas})"); + }); + + if let Some(msg) = &contracts[idx].instantiate_msg { + let info = mock_info("creator", &coins(1000, "earth")); + let contract_result = + call_instantiate::<_, _, _, Empty>(&mut instance, &mock_env(), &info, msg) + .unwrap(); + assert!(contract_result.into_result().is_ok()); + } + + for (execution_idx, execute) in contracts[idx].execute_msgs.iter().enumerate() { + let info = mock_info("verifies", &coins(15, "earth")); + let msg = execute.msg; + let res = + call_execute::<_, _, _, Empty>(&mut instance, &mock_env(), &info, msg); + + if execute.expect_error { + if res.is_ok() { + panic!( + "Round {round}, Execution {execution_idx}, Contract {idx}. Expected error but got {res:?}" + ); + } + } else { + if res.is_err() { + panic!("Round {round}, Execution {execution_idx}, Contract {idx}. Expected no error but got {res:?}"); + } + } + } + } + } + } +} + +fn now_rfc3339() -> String { + let dt = OffsetDateTime::from(SystemTime::now()); + dt.format(&Rfc3339).unwrap_or_default() +} + +pub fn main() { + #[cfg(feature = "dhat-heap")] + let _profiler = dhat::Profiler::new_heap(); + + app(); +}