diff --git a/Cargo.lock b/Cargo.lock index 40031dd..80113d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "anstream" -version = "0.5.0" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" dependencies = [ "anstyle", "anstyle-parse", @@ -18,15 +18,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" dependencies = [ "utf8parse", ] @@ -42,9 +42,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "2.1.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", "windows-sys", @@ -140,15 +140,15 @@ dependencies = [ [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "candid" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "749938f355699c7dd0975e505d54541cd1d84db239374470bdf500a5d885530a" +checksum = "aa0f00717c71b8e9ee4c090b4880ec2418c8506bb6828a2c72df72d3896e905d" dependencies = [ "anyhow", "binread", @@ -180,7 +180,7 @@ dependencies = [ "lazy_static", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -201,9 +201,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.4.5" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824956d0dca8334758a5b7f7e50518d66ea319330cbceedcf76905c2f6ab30e3" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" dependencies = [ "clap_builder", "clap_derive", @@ -211,9 +211,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.5" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122ec64120a49b4563ccaedcbea7818d069ed8e9aa6d829b82d8a4128936b2ab" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" dependencies = [ "anstream", "anstyle", @@ -230,7 +230,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -307,7 +307,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -324,7 +324,7 @@ checksum = "2fa16a70dd58129e4dfffdff535fb1bce66673f7bbeec4a5a1765a504e1ccd84" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -369,9 +369,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" dependencies = [ "errno-dragonfly", "libc", @@ -429,9 +429,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" [[package]] name = "heck" @@ -456,7 +456,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "ic-wasm" -version = "0.5.1" +version = "0.6.0" dependencies = [ "anyhow", "assert_cmd", @@ -487,12 +487,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad227c3af19d4914570ad36d30409928b75967c298feb9ea1969db3a610bb14e" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.1", ] [[package]] @@ -527,9 +527,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.148" +version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "link-cplusplus" @@ -542,9 +542,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" +checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" [[package]] name = "log" @@ -554,9 +554,9 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" -version = "2.6.3" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "num-bigint" @@ -607,7 +607,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -673,9 +673,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.67" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +checksum = "5b1106fec09662ec6dd98ccac0f81cef56984d0b49f75c92d8cbad76e20c005c" dependencies = [ "unicode-ident", ] @@ -709,9 +709,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" [[package]] name = "rustc-demangle" @@ -721,9 +721,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.14" +version = "0.38.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" +checksum = "f25469e9ae0f3d0047ca8b93fc56843f38e6774f0914a107ff8b41be8be8e0b7" dependencies = [ "bitflags 2.4.0", "errno", @@ -770,7 +770,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -841,9 +841,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.37" +version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ "proc-macro2", "quote", @@ -895,7 +895,7 @@ checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -910,7 +910,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.0.1", + "indexmap 2.0.2", "toml_datetime", "winnow", ] @@ -1148,9 +1148,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" +checksum = "037711d82167854aff2018dfd193aa0fef5370f456732f0d5a0c59b0f1b4b907" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 5df674b..ad355cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ic-wasm" -version = "0.5.1" +version = "0.6.0" authors = ["DFINITY Stiftung"] edition = "2021" description = "A library for performing Wasm transformations specific to canisters running on the Internet Computer" @@ -19,7 +19,7 @@ required-features = ["exe"] [dependencies] walrus = "0.20.1" -candid = "0.9.0" +candid = "0.9.9" rustc-demangle = "0.1" thiserror = "1.0.35" diff --git a/README.md b/README.md index 163da37..c550265 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Usage: `ic-wasm -o instrument --trace-only func1 --tr Instrumented canister has the following additional endpoints: * `__get_cycles: () -> (int64) query`. Get the current cycle counter. -* `__get_profiling: () -> (vec { record { int32; int64 }}) query`. Get the execution trace log. +* `__get_profiling: (idx:int32) -> (vec { record { int32; int64 }}, opt int32) query`. Get the execution trace log, starting with `idx` 0. If the log is larger than 2M, it returns the first 2M of trace, and the next `idx` for the next 2M chunk. * `__toggle_tracing: () -> ()`. Disable/enable logging the execution trace. * `__toggle_entry: () -> ()`. Disable/enable clearing exection trace for each update call. * `icp:public name` metadata. Used to map func_id from execution trace to function name. @@ -110,7 +110,7 @@ When `--trace-only` flag is provided, the counter and trace logging will only ha By default, execution trace is stored in the first few pages (up to 32 pages) of stable memory. Without any user side support, we cannot profile upgrade or code which accesses stable memory. If the canister can pre-allocate a fixed region of stable memory at `canister_init`, we can then pass this address to `ic-wasm` via the `--start-page` flag, so that the trace is written to this pre-allocated space without corrupting the rest of the stable memory access. -Another optional flag `--page-limit` specifies the number of pre-allocated pages in stable memory. By default, it's set to 30 pages. We only store trace up to `page-limit` pages, the remaining trace is dropped. Currently, due to the message size limit, we can only store 2M of trace data, which equates to roughly 30 pages. This limitation can be lifted in the future by supporting streamed output of the trace. +Another optional flag `--page-limit` specifies the number of pre-allocated pages in stable memory. By default, it's set to 4096 pages (256MB). We only store trace up to `page-limit` pages, the remaining trace is dropped. The recommended way of pre-allocating stable memory is via the `Region` library in Motoko, and `ic-stable-structures` in Rust. But developers are free to use any other libraries or even the raw stable memory system API to pre-allocate space, as long as the developer can guarantee that the pre-allocated space is not touched by the rest of the code. @@ -121,7 +121,7 @@ import Region "mo:base/Region"; actor { stable let profiling = do { let r = Region.new(); - ignore Region.grow(r, 32); + ignore Region.grow(r, 4096); // Increase the page number if you need larger log space r; }; ... @@ -146,7 +146,7 @@ const UPGRADES: MemoryId = MemoryId::new(1); #[ic_cdk::init] fn init() { let memory = MEMORY_MANAGER.with(|m| m.borrow().get(PROFILING)); - memory.grow(32); + memory.grow(4096); // Increase the page number if you need larger log space ... } #[ic_cdk::pre_upgrade] @@ -163,10 +163,9 @@ fn post_upgrade() { #### Current limitations -* Without pre-allocating stable memory from user code, we cannot profile upgrade or code that accesses stable memory. +* Without pre-allocating stable memory from user code, we cannot profile upgrade or code that accesses stable memory. You can profile traces larger than 256M, if you pre-allocate large pages of stable memory and specify the `page-limit` flag. Larger traces can be fetched in a streamming fashion via `__get_profiling(idx)`. * Since the pre-allocation happens in `canister_init`, we cannot profile `canister_init`. * If heartbeat is present, it's hard to measure any other method calls. It's also hard to measure a specific heartbeat event. -* We only store the first 2M of profiling data. * We cannot measure query calls. * No concurrent calls. diff --git a/src/bin/main.rs b/src/bin/main.rs index 3b93e26..189b99d 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -76,6 +76,9 @@ enum SubCommand { /// The number of pages of the preallocated stable memory #[clap(short, long, requires("start_page"))] page_limit: Option, + /// Use the new metering cost, default to false. This flag will eventually be removed and set to true after the mainnet has adapted the new metering. + #[clap(short, long)] + use_new_metering: bool, }, } @@ -113,12 +116,14 @@ fn main() -> anyhow::Result<()> { trace_only, start_page, page_limit, + use_new_metering, } => { use ic_wasm::instrumentation::{instrument, Config}; let config = Config { trace_only_funcs: trace_only.clone().unwrap_or(vec![]), start_address: start_page.map(|page| page * 65536), page_limit: *page_limit, + use_new_metering: *use_new_metering, }; instrument(&mut m, config).map_err(|e| anyhow::anyhow!("{e}"))?; } diff --git a/src/instrumentation.rs b/src/instrumentation.rs index e728f97..02f50aa 100644 --- a/src/instrumentation.rs +++ b/src/instrumentation.rs @@ -5,8 +5,9 @@ use crate::utils::*; use std::collections::HashSet; const METADATA_SIZE: i32 = 24; -const DEFAULT_PAGE_LIMIT: i32 = 30; +const DEFAULT_PAGE_LIMIT: i32 = 16 * 256; // 256M const LOG_ITEM_SIZE: i32 = 12; +const MAX_ITEMS_PER_QUERY: i32 = 174758; // (2M - 40) / LOG_ITEM_SIZE; struct InjectionPoint { position: usize, @@ -37,6 +38,7 @@ pub struct Config { pub trace_only_funcs: Vec, pub start_address: Option, pub page_limit: Option, + pub use_new_metering: bool, } impl Config { pub fn is_preallocated(&self) -> bool { @@ -49,7 +51,9 @@ impl Config { self.start_address.unwrap_or(0) } pub fn page_limit(&self) -> i32 { - self.page_limit.unwrap_or(DEFAULT_PAGE_LIMIT) + self.page_limit + .map(|x| x - 1) + .unwrap_or(DEFAULT_PAGE_LIMIT - 1) // minus 1 because of metadata } } @@ -65,7 +69,7 @@ pub fn instrument(m: &mut Module, config: Config) -> Result<(), String> { trace_only_ids.insert(id); } let is_partial_tracing = !trace_only_ids.is_empty(); - let func_cost = FunctionCost::new(m); + let func_cost = FunctionCost::new(m, config.use_new_metering); let total_counter = m .globals .add_local(ValType::I64, true, InitExpr::Value(Value::I64(0))); @@ -106,6 +110,7 @@ pub fn instrument(m: &mut Module, config: Config) -> Result<(), String> { &vars, &func_cost, is_partial_tracing, + config.use_new_metering, ); } } @@ -146,6 +151,7 @@ fn inject_metering( vars: &Variables, func_cost: &FunctionCost, is_partial_tracing: bool, + use_new_metering: bool, ) { use InjectionKind::*; let mut stack = vec![start]; @@ -159,7 +165,9 @@ fn inject_metering( match instr { Instr::Block(Block { seq }) | Instr::Loop(Loop { seq }) => { match func.block(*seq).ty { - InstrSeqType::Simple(Some(_)) => curr.cost += 1, + InstrSeqType::Simple(Some(_)) => { + curr.cost += instr_cost(instr, use_new_metering) + } InstrSeqType::Simple(None) => (), InstrSeqType::MultiValue(_) => unreachable!("Multivalue not supported"), } @@ -171,7 +179,7 @@ fn inject_metering( consequent, alternative, }) => { - curr.cost += 1; + curr.cost += instr_cost(instr, use_new_metering); stack.push(*consequent); stack.push(*alternative); injection_points.push(curr); @@ -179,34 +187,38 @@ fn inject_metering( } Instr::Br(_) | Instr::BrIf(_) | Instr::BrTable(_) => { // br always points to a block, so we don't need to push the br block to stack for traversal - curr.cost += 1; + curr.cost += instr_cost(instr, use_new_metering); injection_points.push(curr); curr = InjectionPoint::new(); } Instr::Return(_) | Instr::Unreachable(_) => { - curr.cost += 1; + curr.cost += instr_cost(instr, use_new_metering); injection_points.push(curr); curr = InjectionPoint::new(); } - Instr::Call(Call { func }) => match func_cost.get_cost(*func) { - (cost, InjectionKind::Static) => curr.cost += cost, - (cost, kind @ InjectionKind::Dynamic) - | (cost, kind @ InjectionKind::Dynamic64) => { - curr.cost += cost; - let dynamic = InjectionPoint { - position: pos, - cost: 0, - kind, - }; - injection_points.push(dynamic); + Instr::Call(Call { func }) => { + curr.cost += instr_cost(instr, use_new_metering); + match func_cost.get_cost(*func) { + Some((cost, InjectionKind::Static)) => curr.cost += cost, + Some((cost, kind @ InjectionKind::Dynamic)) + | Some((cost, kind @ InjectionKind::Dynamic64)) => { + curr.cost += cost; + let dynamic = InjectionPoint { + position: pos, + cost: 0, + kind, + }; + injection_points.push(dynamic); + } + None => {} } - }, + } Instr::MemoryFill(_) | Instr::MemoryCopy(_) | Instr::MemoryInit(_) | Instr::TableCopy(_) | Instr::TableInit(_) => { - curr.cost += 1; + curr.cost += instr_cost(instr, use_new_metering); let dynamic = InjectionPoint { position: pos, cost: 0, @@ -215,7 +227,7 @@ fn inject_metering( injection_points.push(dynamic); } _ => { - curr.cost += 1; + curr.cost += instr_cost(instr, use_new_metering); } } } @@ -469,8 +481,8 @@ fn make_stable_writer(m: &mut Module, vars: &Variables, config: &Config) -> Func } else { // This assumes user code doesn't use stable memory then.global_get(vars.page_size) - .i32_const(30) // cannot use the full 2M, because Candid header takes some extra bytes - .binop(BinaryOp::I32GtS) // trace >= 2M + .i32_const(DEFAULT_PAGE_LIMIT) + .binop(BinaryOp::I32GtS) // trace > default_page_limit .if_else( None, |then| { @@ -608,10 +620,22 @@ fn inject_init(m: &mut Module, is_init: GlobalId) { fn inject_pre_upgrade(m: &mut Module, vars: &Variables, config: &Config) { let writer = get_ic_func_id(m, "stable_write"); let memory = get_memory_id(m); + let a = m.locals.add(ValType::I64); + let b = m.locals.add(ValType::I64); + let c = m.locals.add(ValType::I64); let mut builder = get_or_create_export_func(m, "canister_pre_upgrade"); #[rustfmt::skip] builder - // no need to backup memory, since this is the end of the pre-upgrade. + // backup memory. This is not strictly needed, since it's at the end of pre-upgrade. + .i32_const(0) + .load(memory, LoadKind::I64 { atomic: false }, MemArg { offset: 0, align: 8}) + .local_set(a) + .i32_const(8) + .load(memory, LoadKind::I64 { atomic: false }, MemArg { offset: 0, align: 8}) + .local_set(b) + .i32_const(16) + .load(memory, LoadKind::I64 { atomic: false }, MemArg { offset: 0, align: 8}) + .local_set(c) // persist globals .i32_const(0) .global_get(vars.total_counter) @@ -631,7 +655,17 @@ fn inject_pre_upgrade(m: &mut Module, vars: &Variables, config: &Config) { .i32_const(config.metadata_start_address()) .i32_const(0) .i32_const(METADATA_SIZE) - .call(writer); + .call(writer) + // restore memory + .i32_const(0) + .local_get(a) + .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 }) + .i32_const(8) + .local_get(b) + .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 }) + .i32_const(16) + .local_get(c) + .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 }); } fn inject_post_upgrade(m: &mut Module, vars: &Variables, config: &Config) { let reader = get_ic_func_id(m, "stable_read"); @@ -687,39 +721,134 @@ fn inject_post_upgrade(m: &mut Module, vars: &Variables, config: &Config) { fn make_stable_getter(m: &mut Module, vars: &Variables, leb: FunctionId, config: &Config) { let memory = get_memory_id(m); + let arg_size = get_ic_func_id(m, "msg_arg_data_size"); + let arg_copy = get_ic_func_id(m, "msg_arg_data_copy"); let reply_data = get_ic_func_id(m, "msg_reply_data_append"); let reply = get_ic_func_id(m, "msg_reply"); + let trap = get_ic_func_id(m, "trap"); let reader = get_ic_func_id(m, "stable_read"); + let idx = m.locals.add(ValType::I32); + let len = m.locals.add(ValType::I32); + let next_idx = m.locals.add(ValType::I32); let mut builder = FunctionBuilder::new(&mut m.types, &[], &[]); builder.name("__get_profiling".to_string()); #[rustfmt::skip] builder.func_body() + // allocate 2M of heap memory, it's a query call, the system will give back the memory. + .memory_size(memory) + .i32_const(32) + .binop(BinaryOp::I32LtU) + .if_else( + None, + |then| { + then + .i32_const(32) + .memory_grow(memory) + .drop(); + }, + |_| {} + ) + // parse input idx + .call(arg_size) + .i32_const(11) + .binop(BinaryOp::I32Ne) + .if_else( + None, + |then| { + then.i32_const(0) + .i32_const(0) + .call(trap); + }, + |_| {}, + ) + .i32_const(0) + .i32_const(7) + .i32_const(4) + .call(arg_copy) + .i32_const(0) + .load(memory, LoadKind::I32 { atomic: false }, MemArg { offset: 0, align: 4}) + .local_set(idx) + // write header (vec { record { int32; int64 } }, opt int32) .i32_const(0) - // vec { record { int32; int64 } } - .i64_const(0x6c016d024c444944) // "DIDL026d016c" + .i64_const(0x6c016d034c444944) // "DIDL036d016c" .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 }) .i32_const(8) - .i64_const(0x0000017401750002) // "02007501740100xx" + .i64_const(0x02756e7401750002) // "02007501746e7502" .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 }) + .i32_const(16) + .i32_const(0x0200) // "0002" + .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4}) .i32_const(0) - .i32_const(15) + .i32_const(18) .call(reply_data) + // if log_size - idx > MAX_ITEMS_PER_QUERY .global_get(vars.log_size) + .local_get(idx) + .binop(BinaryOp::I32Sub) + .local_tee(len) + .i32_const(MAX_ITEMS_PER_QUERY) + .binop(BinaryOp::I32GtU) + .if_else( + None, + |then| { + then.i32_const(MAX_ITEMS_PER_QUERY) + .local_set(len) + .local_get(idx) + .i32_const(MAX_ITEMS_PER_QUERY) + .binop(BinaryOp::I32Add) + .local_set(next_idx); + }, + |else_| { + else_.i32_const(0) + .local_set(next_idx); + }, + ) + .local_get(len) .call(leb) .i32_const(0) .i32_const(5) .call(reply_data) + // read stable logs .i32_const(0) .i32_const(config.log_start_address()) - .global_get(vars.log_size) + .local_get(idx) + .i32_const(LOG_ITEM_SIZE) + .binop(BinaryOp::I32Mul) + .binop(BinaryOp::I32Add) + .local_get(len) .i32_const(LOG_ITEM_SIZE) .binop(BinaryOp::I32Mul) .call(reader) .i32_const(0) - .global_get(vars.log_size) + .local_get(len) .i32_const(LOG_ITEM_SIZE) .binop(BinaryOp::I32Mul) .call(reply_data) + // opt next idx + .local_get(next_idx) + .unop(UnaryOp::I32Eqz) + .if_else( + None, + |then| { + then.i32_const(0) + .i32_const(0) + .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4}) + .i32_const(0) + .i32_const(1) + .call(reply_data); + }, + |else_| { + else_.i32_const(0) + .i32_const(1) + .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 1}) + .i32_const(1) + .local_get(next_idx) + .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4}) + .i32_const(0) + .i32_const(5) + .call(reply_data); + }, + ) .call(reply); let getter = builder.finish(vec![], &mut m.funcs); m.exports.add("canister_query __get_profiling", getter); diff --git a/src/limit_resource.rs b/src/limit_resource.rs index 0ade0f3..e8090c2 100644 --- a/src/limit_resource.rs +++ b/src/limit_resource.rs @@ -14,6 +14,8 @@ struct CyclesAdd { cycles_add: FunctionId, old_cycles_add128: FunctionId, new_cycles_add128: FunctionId, + //old_cycles_burn128: FunctionId, + //new_cycles_burn128: FunctionId, } #[derive(Copy, Clone)] struct StableGrow { @@ -47,7 +49,13 @@ impl VisitorMut for Replacer { } .into(); return; - } + } /* else if *func == ids.old_cycles_burn128 { + *instr = Call { + func: ids.new_cycles_burn128, + } + .into(); + return; + }*/ } if let Some(ids) = &self.stable_grow { if *func == ids.old_grow { @@ -78,15 +86,20 @@ pub fn limit_resource(m: &mut Module, config: &Config) { .imports .find("ic0", "call_cycles_add") .or_else(|| m.imports.find("ic0", "call_cycles_add128")) + .or_else(|| m.imports.find("ic0", "cycles_burn128")) .is_some(); let cycles_add = if has_cycles_add && config.remove_cycles_add { let cycles_add = get_ic_func_id(m, "call_cycles_add"); let old_cycles_add128 = get_ic_func_id(m, "call_cycles_add128"); + //let old_cycles_burn128 = get_ic_func_id(m, "cycles_burn128"); let new_cycles_add128 = make_cycles_add128(m); + //let new_cycles_burn128 = make_cycles_burn128(m); Some(CyclesAdd { cycles_add, old_cycles_add128, new_cycles_add128, + //old_cycles_burn128, + //new_cycles_burn128, }) } else { None @@ -126,7 +139,9 @@ pub fn limit_resource(m: &mut Module, config: &Config) { m.funcs.iter_local_mut().for_each(|(id, func)| { if let Some(ids) = &cycles_add { - if id == ids.new_cycles_add128 { + if id == ids.new_cycles_add128 + /*|| id == ids.new_cycles_burn128*/ + { return; } } @@ -164,6 +179,26 @@ fn make_cycles_add128(m: &mut Module) -> FunctionId { .drop(); builder.finish(vec![high, low], &mut m.funcs) } +/* +fn make_cycles_burn128(m: &mut Module) -> FunctionId { + let mut builder = FunctionBuilder::new( + &mut m.types, + &[ValType::I64, ValType::I64, ValType::I32], + &[], + ); + let high = m.locals.add(ValType::I64); + let low = m.locals.add(ValType::I64); + let dst = m.locals.add(ValType::I32); + builder + .func_body() + .local_get(high) + .local_get(low) + .local_get(dst) + .drop() + .drop() + .drop(); + builder.finish(vec![high, low, dst], &mut m.funcs) +}*/ fn make_grow_func(m: &mut Module, limit: i32) -> FunctionId { let size = get_ic_func_id(m, "stable_size"); let grow = get_ic_func_id(m, "stable_grow"); diff --git a/src/utils.rs b/src/utils.rs index 7770573..db88e3f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -32,7 +32,7 @@ pub(crate) enum InjectionKind { pub(crate) struct FunctionCost(HashMap); impl FunctionCost { - pub fn new(m: &Module) -> Self { + pub fn new(m: &Module, use_new_metering: bool) -> Self { let mut res = HashMap::new(); for (method, func) in m.imports.iter().filter_map(|i| { if let ImportKind::Function(func) = i.kind { @@ -47,30 +47,124 @@ impl FunctionCost { }) { use InjectionKind::*; // System API cost taken from https://github.com/dfinity/ic/blob/master/rs/embedders/src/wasmtime_embedder/system_api_complexity.rs - let cost = match method { - "msg_arg_data_copy" => (21, Dynamic), - "msg_method_name_copy" => (21, Dynamic), - "msg_reply_data_append" => (21, Dynamic), - "msg_reject" => (21, Dynamic), - "msg_reject_msg_copy" => (21, Dynamic), - "debug_print" => (101, Dynamic), - "trap" => (21, Dynamic), - "call_new" => (1, Static), - "call_data_append" => (21, Dynamic), - "call_perform" => (1, Static), - "stable_read" => (21, Dynamic), - "stable_write" => (21, Dynamic), - "stable64_read" => (21, Dynamic64), - "stable64_write" => (21, Dynamic64), - "performance_counter" => (201, Static), - _ => (1, Static), + let cost = if use_new_metering { + match method { + "accept_message" => (500, Static), + "call_cycles_add" | "call_cycles_add128" => (500, Static), + "call_data_append" => (500, Dynamic), + "call_new" => (1500, Static), + "call_on_cleanup" => (500, Static), + "call_perform" => (5000, Static), + "canister_cycle_balance" | "canister_cycle_balance128" => (500, Static), + "canister_self_copy" => (500, Dynamic), + "canister_self_size" => (500, Static), + "canister_status" | "canister_version" => (500, Static), + "certified_data_set" => (500, Dynamic), + "data_certificate_copy" => (500, Dynamic), + "data_certificate_present" | "data_certificate_size" => (500, Static), + "debug_print" => (100, Dynamic), + "global_timer_set" => (500, Static), + "is_controller" => (1000, Dynamic), + "msg_arg_data_copy" => (500, Dynamic), + "msg_arg_data_size" => (500, Static), + "msg_caller_copy" => (500, Dynamic), + "msg_caller_size" => (500, Static), + "msg_cycles_accept" | "msg_cycles_accept128" => (500, Static), + "msg_cycles_available" | "msg_cycles_available128" => (500, Static), + "msg_cycles_refunded" | "msg_cycles_refunded128" => (500, Static), + "cycles_burn128" => (100, Static), + "msg_method_name_copy" => (500, Dynamic), + "msg_method_name_size" => (500, Static), + "msg_reject_code" | "msg_reject_msg_size" => (500, Static), + "msg_reject_msg_copy" => (500, Dynamic), + "msg_reject" => (500, Dynamic), + "msg_reply_data_append" => (500, Dynamic), + "msg_reply" => (500, Static), + "performance_counter" => (200, Static), + "stable_grow" | "stable64_grow" => (100, Static), + "stable_size" | "stable64_size" => (20, Static), + "stable_read" => (20, Dynamic), + "stable_write" => (20, Dynamic), + "stable64_read" => (20, Dynamic64), + "stable64_write" => (20, Dynamic64), + "trap" => (500, Dynamic), + "time" => (500, Static), + _ => (20, Static), + } + } else { + match method { + "msg_arg_data_copy" => (20, Dynamic), + "msg_method_name_copy" => (20, Dynamic), + "msg_reply_data_append" => (20, Dynamic), + "msg_reject" => (20, Dynamic), + "msg_reject_msg_copy" => (20, Dynamic), + "debug_print" => (100, Dynamic), + "is_controller" => (1000, Dynamic), + "trap" => (20, Dynamic), + "call_new" => (0, Static), + "call_data_append" => (20, Dynamic), + "call_perform" => (0, Static), + "stable_read" => (20, Dynamic), + "stable_write" => (20, Dynamic), + "stable64_read" => (20, Dynamic64), + "stable64_write" => (20, Dynamic64), + "performance_counter" => (200, Static), + _ => (0, Static), + } }; res.insert(func, cost); } Self(res) } - pub fn get_cost(&self, id: FunctionId) -> (i64, InjectionKind) { - *self.0.get(&id).unwrap_or(&(1, InjectionKind::Static)) + pub fn get_cost(&self, id: FunctionId) -> Option<(i64, InjectionKind)> { + self.0.get(&id).copied() + } +} +pub(crate) fn instr_cost(i: &ir::Instr, use_new_metering: bool) -> i64 { + use ir::*; + use BinaryOp::*; + use UnaryOp::*; + if !use_new_metering { + return 1; + } + // Cost taken from https://github.com/dfinity/ic/blob/master/rs/embedders/src/wasm_utils/instrumentation.rs + match i { + Instr::Block(..) | Instr::Loop(..) => 0, + Instr::Const(..) | Instr::Load(..) | Instr::Store(..) => 1, + Instr::GlobalGet(..) | Instr::GlobalSet(..) => 2, + Instr::TableGet(..) | Instr::TableSet(..) => 5, + Instr::TableGrow(..) | Instr::MemoryGrow(..) => 300, + Instr::MemorySize(..) => 20, + Instr::TableSize(..) => 100, + Instr::MemoryFill(..) | Instr::MemoryCopy(..) | Instr::MemoryInit(..) => 100, + Instr::TableFill(..) | Instr::TableCopy(..) | Instr::TableInit(..) => 100, + Instr::DataDrop(..) | Instr::ElemDrop(..) => 300, + Instr::Call(..) => 5, + Instr::CallIndirect(..) => 10, // missing ReturnCall/Indirect + Instr::IfElse(..) | Instr::Br(..) | Instr::BrIf(..) | Instr::BrTable(..) => 2, + Instr::RefIsNull(..) => 5, + Instr::RefFunc(..) => 130, + Instr::Unop(Unop { op }) => match op { + F32Ceil | F32Floor | F32Trunc | F32Nearest | F32Sqrt => 20, + F64Ceil | F64Floor | F64Trunc | F64Nearest | F64Sqrt => 20, + F32Abs | F32Neg | F64Abs | F64Neg => 2, + F32ConvertSI32 | F64ConvertSI64 | F32ConvertSI64 | F64ConvertSI32 => 3, + F64ConvertUI32 | F32ConvertUI64 | F32ConvertUI32 | F64ConvertUI64 => 16, + I64TruncSF32 | I64TruncUF32 | I64TruncSF64 | I64TruncUF64 => 20, + I32TruncSF32 | I32TruncUF32 | I32TruncSF64 | I32TruncUF64 => 20, // missing TruncSat? + _ => 1, + }, + Instr::Binop(Binop { op }) => match op { + I32DivS | I32DivU | I32RemS | I32RemU => 10, + I64DivS | I64DivU | I64RemS | I64RemU => 10, + F32Add | F32Sub | F32Mul | F32Div | F32Min | F32Max => 20, + F64Add | F64Sub | F64Mul | F64Div | F64Min | F64Max => 20, + F32Copysign | F64Copysign => 2, + F32Eq | F32Ne | F32Lt | F32Gt | F32Le | F32Ge => 3, + F64Eq | F64Ne | F64Lt | F64Gt | F64Le | F64Ge => 3, + _ => 1, + }, + _ => 1, } } @@ -100,6 +194,9 @@ pub(crate) fn get_ic_func_id(m: &mut Module, method: &str) -> FunctionId { "stable64_size" => m.types.add(&[], &[ValType::I64]), "call_cycles_add" => m.types.add(&[ValType::I64], &[]), "call_cycles_add128" => m.types.add(&[ValType::I64, ValType::I64], &[]), + "cycles_burn128" => m + .types + .add(&[ValType::I64, ValType::I64, ValType::I32], &[]), "call_new" => m.types.add( &[ ValType::I32, @@ -115,6 +212,10 @@ pub(crate) fn get_ic_func_id(m: &mut Module, method: &str) -> FunctionId { ), "debug_print" => m.types.add(&[ValType::I32, ValType::I32], &[]), "trap" => m.types.add(&[ValType::I32, ValType::I32], &[]), + "msg_arg_data_size" => m.types.add(&[], &[ValType::I32]), + "msg_arg_data_copy" => m + .types + .add(&[ValType::I32, ValType::I32, ValType::I32], &[]), "msg_reply_data_append" => m.types.add(&[ValType::I32, ValType::I32], &[]), "msg_reply" => m.types.add(&[], &[]), _ => unreachable!(), diff --git a/tests/deployable.ic-repl.sh b/tests/deployable.ic-repl.sh index 7cf583e..6c6dab8 100644 --- a/tests/deployable.ic-repl.sh +++ b/tests/deployable.ic-repl.sh @@ -89,8 +89,11 @@ function classes_redirect(wasm) { function check_profiling(S, cycles, len) { call S.__get_cycles(); assert _ == (cycles : int64); - call S.__get_profiling(); - assert _.size() == (len : nat); + call S.__get_profiling((0:nat32)); + assert _[0].size() == (len : nat); + assert _[1] == (null : opt empty); + call S.__get_profiling((1:nat32)); + assert _[0].size() == (sub(len,1) : nat); null }; diff --git a/tests/ok/motoko-gc-instrument.wasm b/tests/ok/motoko-gc-instrument.wasm index 2c3c853..6c1f349 100644 Binary files a/tests/ok/motoko-gc-instrument.wasm and b/tests/ok/motoko-gc-instrument.wasm differ diff --git a/tests/ok/motoko-instrument.wasm b/tests/ok/motoko-instrument.wasm index e59e7a1..e0cec5a 100644 Binary files a/tests/ok/motoko-instrument.wasm and b/tests/ok/motoko-instrument.wasm differ diff --git a/tests/ok/motoko-region-instrument.wasm b/tests/ok/motoko-region-instrument.wasm index 0d62a15..18f61ce 100644 Binary files a/tests/ok/motoko-region-instrument.wasm and b/tests/ok/motoko-region-instrument.wasm differ diff --git a/tests/ok/rust-instrument.wasm b/tests/ok/rust-instrument.wasm index 4e29853..a6a9319 100644 Binary files a/tests/ok/rust-instrument.wasm and b/tests/ok/rust-instrument.wasm differ diff --git a/tests/ok/rust-region-instrument.wasm b/tests/ok/rust-region-instrument.wasm index 30fb265..7ab7b60 100644 Binary files a/tests/ok/rust-region-instrument.wasm and b/tests/ok/rust-region-instrument.wasm differ diff --git a/tests/ok/wat-instrument.wasm b/tests/ok/wat-instrument.wasm index 9ab72fb..5212e23 100644 Binary files a/tests/ok/wat-instrument.wasm and b/tests/ok/wat-instrument.wasm differ