diff --git a/rs/config/src/subnet_config.rs b/rs/config/src/subnet_config.rs index 14d51e72f421..4f10382ed53d 100644 --- a/rs/config/src/subnet_config.rs +++ b/rs/config/src/subnet_config.rs @@ -155,7 +155,7 @@ pub const DEFAULT_REFERENCE_SUBNET_SIZE: usize = 13; pub const SEV_REFERENCE_SUBNET_SIZE: usize = 7; /// Costs for each newly created dirty page in stable memory. -const DEFAULT_DIRTY_PAGE_OVERHEAD: NumInstructions = NumInstructions::new(1_000); +const DEFAULT_DIRTY_PAGE_OVERHEAD: NumInstructions = NumInstructions::new(5_000); /// Accumulated priority reset interval, rounds. /// diff --git a/rs/embedders/src/wasmtime_embedder.rs b/rs/embedders/src/wasmtime_embedder.rs index aa7fb3281ae4..d4579f971d2d 100644 --- a/rs/embedders/src/wasmtime_embedder.rs +++ b/rs/embedders/src/wasmtime_embedder.rs @@ -636,6 +636,7 @@ impl WasmtimeEmbedder { &mut *store, self.log.clone(), self.config.feature_flags.deterministic_memory_tracker, + self.config.dirty_page_overhead, subtract_instruction_counter, ); @@ -798,6 +799,7 @@ fn sigsegv_memory_tracker( store: &mut wasmtime::Store, log: ReplicaLogger, deterministic_memory_tracker: FlagStatus, + page_overhead: NumInstructions, subtract_instruction_counter: Arc>, ) -> HashMap>> { let maybe_missing_page_handler_kind = match deterministic_memory_tracker { @@ -843,6 +845,7 @@ fn sigsegv_memory_tracker( page_map, maybe_missing_page_handler_kind, memory_limits, + page_overhead.get(), subtract_instruction_counter.clone(), ) .expect("failed to instantiate SIGSEGV memory tracker"), diff --git a/rs/execution_environment/tests/dts.rs b/rs/execution_environment/tests/dts.rs index 3c53ee0d28e8..9b8968b416b2 100644 --- a/rs/execution_environment/tests/dts.rs +++ b/rs/execution_environment/tests/dts.rs @@ -40,13 +40,14 @@ const DTS_WAT: &str = r#" ) (import "ic0" "canister_version" (func $canister_version (result i64))) (func $work - (memory.fill (i32.const 0) (i32.const 12) (i32.const 10000)) - (memory.fill (i32.const 0) (i32.const 23) (i32.const 10000)) - (memory.fill (i32.const 0) (i32.const 34) (i32.const 10000)) - (memory.fill (i32.const 0) (i32.const 45) (i32.const 10000)) - (memory.fill (i32.const 0) (i32.const 56) (i32.const 10000)) - (memory.fill (i32.const 0) (i32.const 67) (i32.const 10000)) - (memory.fill (i32.const 0) (i32.const 78) (i32.const 10000)) + (memory.fill (i32.const 0) (i32.const 12) (i32.const 50000)) + (memory.fill (i32.const 0) (i32.const 23) (i32.const 50000)) + (memory.fill (i32.const 0) (i32.const 34) (i32.const 50000)) + (memory.fill (i32.const 0) (i32.const 45) (i32.const 50000)) + (memory.fill (i32.const 0) (i32.const 56) (i32.const 50000)) + (memory.fill (i32.const 0) (i32.const 67) (i32.const 50000)) + (memory.fill (i32.const 0) (i32.const 78) (i32.const 50000)) + ) (start $work) @@ -550,7 +551,7 @@ fn dts_install_code_with_concurrent_ingress_insufficient_cycles_and_freezing_thr fn dts_pending_upgrade_with_heartbeat() { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(30_000), + NumInstructions::from(250_000), ); let binary = wat2wasm(DTS_WAT); @@ -559,7 +560,7 @@ fn dts_pending_upgrade_with_heartbeat() { let controller = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -635,7 +636,7 @@ fn dts_pending_upgrade_with_heartbeat() { fn dts_scheduling_of_install_code() { let (env, _) = dts_install_code_env( NumInstructions::from(5_000_000_000), - NumInstructions::from(10_000), + NumInstructions::from(250_000), ); let binary = wat2wasm(DTS_WAT); @@ -644,7 +645,7 @@ fn dts_scheduling_of_install_code() { let controller = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -785,7 +786,7 @@ fn dts_scheduling_of_install_code() { fn dts_pending_install_code_does_not_block_subnet_messages_of_other_canisters() { let (env, _) = dts_install_code_env( NumInstructions::from(5_000_000_000), - NumInstructions::from(10_000), + NumInstructions::from(250_000), ); let binary = wat2wasm(DTS_WAT); @@ -798,7 +799,7 @@ fn dts_pending_install_code_does_not_block_subnet_messages_of_other_canisters() for _ in 0..n { let id = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -909,7 +910,7 @@ fn dts_pending_install_code_does_not_block_subnet_messages_of_other_canisters() fn dts_pending_execution_blocks_subnet_messages_to_the_same_canister() { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(10_000), + NumInstructions::from(250_000), ); let binary = wat2wasm(DTS_WAT); @@ -985,7 +986,7 @@ fn dts_aborted_execution_does_not_block_subnet_messages() { let user_id = PrincipalId::new_anonymous(); let other_canister_id = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -993,7 +994,7 @@ fn dts_aborted_execution_does_not_block_subnet_messages() { .unwrap(); let aborted_canister_id = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], Some( CanisterSettingsArgsBuilder::new() @@ -1090,7 +1091,7 @@ fn dts_paused_execution_blocks_deposit_cycles() { let user_id = PrincipalId::new_anonymous(); let long_canister_id = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -1098,7 +1099,7 @@ fn dts_paused_execution_blocks_deposit_cycles() { .unwrap(); let other_canister_id = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -1165,7 +1166,7 @@ fn dts_paused_execution_blocks_deposit_cycles() { fn dts_pending_install_code_blocks_update_messages_to_the_same_canister() { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(10_000), + NumInstructions::from(250_000), ); let binary = wat2wasm(DTS_WAT); @@ -1242,7 +1243,7 @@ fn dts_long_running_install_and_update() { for _ in 0..n { let id = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -1262,7 +1263,7 @@ fn dts_long_running_install_and_update() { let id = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], settings.clone(), INITIAL_CYCLES_BALANCE, @@ -1273,7 +1274,7 @@ fn dts_long_running_install_and_update() { let short = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -1285,7 +1286,7 @@ fn dts_long_running_install_and_update() { let args = InstallCodeArgs::new( CanisterInstallMode::Upgrade, canister[i], - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], ); let payload = wasm() @@ -1368,7 +1369,7 @@ fn dts_long_running_calls() { for _ in 0..n { let id = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -1379,7 +1380,7 @@ fn dts_long_running_calls() { let short = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -1439,7 +1440,7 @@ fn dts_long_running_calls() { fn dts_unrelated_subnet_messages_make_progress() { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(10_000), + NumInstructions::from(250_000), ); let binary = wat2wasm(DTS_WAT); @@ -1501,7 +1502,7 @@ fn dts_unrelated_subnet_messages_make_progress() { fn dts_ingress_status_of_update_is_correct() { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(10_000), + NumInstructions::from(170_000), ); let binary = wat2wasm(DTS_WAT); @@ -1514,10 +1515,9 @@ fn dts_ingress_status_of_update_is_correct() { // advance time so that time does not grow implicitly when executing a round env.advance_time(Duration::from_secs(1)); - let original_time = env.time(); let update = env.send_ingress(user_id, canister, "update", vec![]); - env.tick(); + let original_time = env.time(); assert_eq!( ingress_state(env.ingress_status(&update)), @@ -1568,7 +1568,7 @@ fn dts_ingress_status_of_update_is_correct() { fn dts_ingress_status_of_install_is_correct() { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(10_000), + NumInstructions::from(170_000), ); let binary = wat2wasm(DTS_WAT); @@ -1588,8 +1588,6 @@ fn dts_ingress_status_of_install_is_correct() { env.send_ingress(user_id, IC_00, Method::InstallCode, args.encode()) }; - env.tick(); - assert_eq!( ingress_state(env.ingress_status(&install)), Some(IngressState::Processing) @@ -1639,7 +1637,7 @@ fn dts_ingress_status_of_install_is_correct() { fn dts_ingress_status_of_upgrade_is_correct() { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(10_000), + NumInstructions::from(250_000), ); let binary = wat2wasm(DTS_WAT); @@ -1708,12 +1706,13 @@ fn dts_ingress_status_of_upgrade_is_correct() { #[test] fn dts_ingress_status_of_update_with_call_is_correct() { - let env = dts_env( - NumInstructions::from(1_000_000_000), - NumInstructions::from(10_000), - ); + // start with a large slice limit so installations just work. + let env = ic_state_machine_tests::StateMachineBuilder::new() + .with_subnet_type(SubnetType::Application) + .with_checkpoints_enabled(true) + .build(); - let binary = UNIVERSAL_CANISTER_WASM.to_vec(); + let binary = UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(); let user_id = PrincipalId::new_anonymous(); @@ -1725,18 +1724,26 @@ fn dts_ingress_status_of_update_with_call_is_correct() { .install_canister_with_cycles(binary, vec![], None, INITIAL_CYCLES_BALANCE) .unwrap(); + // reduce slice limit to provoke test conditions + let env = env.restart_node_with_config(dts_state_machine_config(dts_subnet_config( + NumInstructions::from(1_000_000_000), + NumInstructions::from(250_000), + ))); + let b = wasm() - .stable64_grow(1) - .stable64_fill(0, 0, 10_000) - .stable64_fill(0, 0, 10_000) + .stable64_grow(1) // cost: 1 wasm page = 16 os pages -> 5k * 16 = 80k instructions + .stable64_fill(0, 0, 24_000) // 24_000 cost: 6 stable pages dirtied (6*5k) + 6 heap pages (6*5k) (because vec is allocated there) + 24k for the bytes = 84k instructions + .stable64_fill(0, 0, 24_000) // 84k * 3 = 252k -> this message needs at least two slices. + .stable64_fill(0, 0, 24_000) .message_payload() .append_and_reply() .build(); let a = wasm() .stable64_grow(1) - .stable64_fill(0, 0, 10_000) - .stable64_fill(0, 0, 10_000) + .stable64_fill(0, 0, 24_000) + .stable64_fill(0, 0, 24_000) + .stable64_fill(0, 0, 24_000) .inter_update(b_id, call_args().other_side(b)) .build(); @@ -1807,7 +1814,7 @@ fn impl_dts_canister_with_upgrade_is_uninstalled_due_to_resource_charges( ) -> Result { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(10_000), + NumInstructions::from(170000), ); let binary = wat2wasm(DTS_WAT); @@ -1823,7 +1830,6 @@ fn impl_dts_canister_with_upgrade_is_uninstalled_due_to_resource_charges( let canister = env .install_canister_with_cycles(binary.clone(), vec![], settings, INITIAL_CYCLES_BALANCE) .unwrap(); - // Checkpointing will abort the upgrade after each round. env.set_checkpoints_enabled(aborted); @@ -1841,7 +1847,7 @@ fn impl_dts_canister_with_upgrade_is_uninstalled_due_to_resource_charges( // Enable normal message execution. env.set_checkpoints_enabled(false); - env.await_ingress(upgrade, 30) + env.await_ingress(upgrade, 60) } #[test] @@ -1866,9 +1872,15 @@ fn dts_canister_with_paused_upgrade_is_not_uninstalled_due_to_resource_charges() fn impl_dts_canister_with_update_is_uninstalled_due_to_resource_charges( aborted: bool, ) -> Result { + // A single `memory.fill` in `$work` now costs ~170_000 instructions: the + // first write to a Wasm page is charged the full deterministic-memory-tracker + // page overhead (2x16 OS pages * 5_000) plus the ~10_000 byte copy cost. The + // slice limit must exceed that so the (un-splittable) operation fits in one + // slice, while staying below the total `$work` cost (~230_000) so execution + // still spans multiple rounds and can be aborted mid-flight. let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(10_000), + NumInstructions::from(200_000), ); let binary = wat2wasm(DTS_WAT); let user_id = PrincipalId::new_anonymous(); @@ -1923,7 +1935,7 @@ fn dts_serialized_and_runtime_states_are_equal() { fn run(restart_node: bool) -> CryptoHashOfState { let subnet_config = dts_subnet_config( NumInstructions::from(1_000_000_000), - NumInstructions::from(10_000), + NumInstructions::from(250_000), ); let num_canisters = subnet_config.scheduler_config.scheduler_cores * 2; let state_machine_config = dts_state_machine_config(subnet_config); @@ -1933,7 +1945,7 @@ fn dts_serialized_and_runtime_states_are_equal() { for _ in 0..num_canisters { let canister_id = env .install_canister_with_cycles( - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), vec![], None, INITIAL_CYCLES_BALANCE, @@ -1994,7 +2006,7 @@ fn get_canister_version(env: &StateMachine, canister_id: CanisterId) -> u64 { fn dts_heartbeat_works() { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(50_000), + NumInstructions::from(500_000), ); let binary = UNIVERSAL_CANISTER_WASM.to_vec(); @@ -2007,7 +2019,7 @@ fn dts_heartbeat_works() { assert_eq!(2, get_canister_version(&env, canister_id)); let heartbeat = wasm() - .instruction_counter_is_at_least(100_000) + .instruction_counter_is_at_least(1_100_000) .inc_global_counter() .build(); @@ -2042,7 +2054,7 @@ fn dts_heartbeat_works() { fn dts_heartbeat_resume_after_abort() { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(50_000), + NumInstructions::from(500_000), ); let binary = UNIVERSAL_CANISTER_WASM.to_vec(); @@ -2055,7 +2067,7 @@ fn dts_heartbeat_resume_after_abort() { assert_eq!(2, get_canister_version(&env, canister_id)); let heartbeat = wasm() - .instruction_counter_is_at_least(100_000) + .instruction_counter_is_at_least(1_100_000) .inc_global_counter() .build(); @@ -2103,7 +2115,7 @@ fn dts_heartbeat_resume_after_abort() { fn dts_heartbeat_with_trap() { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(50_000), + NumInstructions::from(500_000), ); let binary = UNIVERSAL_CANISTER_WASM.to_vec(); @@ -2116,7 +2128,7 @@ fn dts_heartbeat_with_trap() { assert_eq!(2, get_canister_version(&env, canister_id)); let heartbeat = wasm() - .instruction_counter_is_at_least(100_000) + .instruction_counter_is_at_least(1_100_000) .inc_global_counter() .trap() .build(); @@ -2151,7 +2163,7 @@ fn dts_heartbeat_with_trap() { fn dts_heartbeat_does_not_prevent_canister_from_stopping() { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(50_000), + NumInstructions::from(500_000), ); let binary = UNIVERSAL_CANISTER_WASM.to_vec(); @@ -2161,7 +2173,7 @@ fn dts_heartbeat_does_not_prevent_canister_from_stopping() { .unwrap(); let heartbeat = wasm() - .instruction_counter_is_at_least(100_000) + .instruction_counter_is_at_least(1_100_000) .inc_global_counter() .build(); @@ -2195,7 +2207,7 @@ fn dts_heartbeat_does_not_prevent_canister_from_stopping() { fn dts_heartbeat_does_not_prevent_upgrade() { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(50_000), + NumInstructions::from(500_000), ); let binary = UNIVERSAL_CANISTER_WASM.to_vec(); @@ -2205,7 +2217,7 @@ fn dts_heartbeat_does_not_prevent_upgrade() { .unwrap(); let heartbeat = wasm() - .instruction_counter_is_at_least(100_000) + .instruction_counter_is_at_least(1_100_000) .inc_global_counter() .build(); @@ -2238,29 +2250,26 @@ fn dts_heartbeat_does_not_prevent_upgrade() { fn dts_global_timer_one_shot_works() { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(50_000), + NumInstructions::from(500_000), ); - let binary = UNIVERSAL_CANISTER_WASM.to_vec(); + let binary = UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(); let canister_id = env .install_canister_with_cycles(binary, vec![], None, INITIAL_CYCLES_BALANCE) .unwrap(); - // 1) canister create and 2) install code. - assert_eq!(2, get_canister_version(&env, canister_id)); + // 1) canister installation. + assert_eq!(1, get_canister_version(&env, canister_id)); let now_nanos = env.time().duration_since(UNIX_EPOCH).unwrap().as_nanos() as u64; - let disable_heartbeats = wasm().trap().build(); - let timer = wasm() - .instruction_counter_is_at_least(100_000) + .instruction_counter_is_at_least(1_100_000) .inc_global_counter() .build(); - let set_heartbeat_and_global_timer = wasm() - .set_heartbeat(disable_heartbeats) + let set_global_timer = wasm() .set_global_timer_method(timer) .api_global_timer_set(now_nanos) .get_global_counter() @@ -2268,14 +2277,14 @@ fn dts_global_timer_one_shot_works() { .build(); let result = env - .execute_ingress(canister_id, "update", set_heartbeat_and_global_timer) + .execute_ingress(canister_id, "update", set_global_timer) .unwrap(); assert_eq!(result, WasmResult::Reply(0_u64.to_le_bytes().to_vec())); - // 3) the update. + // 2) the update. let base_canister_version = get_canister_version(&env, canister_id); - assert_eq!(3, base_canister_version); + assert_eq!(2, base_canister_version); for i in 1..10 { env.tick(); @@ -2302,7 +2311,7 @@ fn dts_global_timer_one_shot_works() { fn dts_heartbeat_does_not_starve_when_global_timer_is_long() { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(75_000), + NumInstructions::from(500_000), ); let binary = UNIVERSAL_CANISTER_WASM.to_vec(); @@ -2316,10 +2325,10 @@ fn dts_heartbeat_does_not_starve_when_global_timer_is_long() { let now_nanos = env.time().duration_since(UNIX_EPOCH).unwrap().as_nanos() as u64; - let heartbeat = wasm().instruction_counter_is_at_least(150_000).build(); + let heartbeat = wasm().instruction_counter_is_at_least(1_100_000).build(); let timer = wasm() - .instruction_counter_is_at_least(150_000) + .instruction_counter_is_at_least(1_100_000) .inc_global_counter() .api_global_timer_set(now_nanos) .build(); @@ -2385,10 +2394,10 @@ fn dts_heartbeat_does_not_starve_when_global_timer_is_long() { fn dts_global_timer_resume_after_abort() { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(60_000), + NumInstructions::from(500_000), ); - let binary = UNIVERSAL_CANISTER_WASM.to_vec(); + let binary = UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(); let canister_id = env .install_canister_with_cycles(binary, vec![], None, INITIAL_CYCLES_BALANCE) @@ -2397,7 +2406,7 @@ fn dts_global_timer_resume_after_abort() { let now_nanos = env.time().duration_since(UNIX_EPOCH).unwrap().as_nanos() as u64; let timer = wasm() - .instruction_counter_is_at_least(150_000) + .instruction_counter_is_at_least(1_100_000) .inc_global_counter() .api_global_timer_set(now_nanos) .build(); @@ -2435,10 +2444,10 @@ fn dts_global_timer_resume_after_abort() { fn dts_global_timer_does_not_prevent_canister_from_stopping() { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(60_000), + NumInstructions::from(500_000), ); - let binary = UNIVERSAL_CANISTER_WASM.to_vec(); + let binary = UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(); let canister_id = env .install_canister_with_cycles(binary, vec![], None, INITIAL_CYCLES_BALANCE) @@ -2447,7 +2456,7 @@ fn dts_global_timer_does_not_prevent_canister_from_stopping() { let now_nanos = env.time().duration_since(UNIX_EPOCH).unwrap().as_nanos() as u64; let timer = wasm() - .instruction_counter_is_at_least(150_000) + .instruction_counter_is_at_least(1_100_000) .inc_global_counter() .api_global_timer_set(now_nanos) .build(); @@ -2483,24 +2492,24 @@ fn dts_global_timer_does_not_prevent_canister_from_stopping() { fn dts_global_timer_with_trap() { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(50_000), + NumInstructions::from(500_000), ); - let binary = UNIVERSAL_CANISTER_WASM.to_vec(); + let binary = UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(); let canister_id = env .install_canister_with_cycles(binary, vec![], None, INITIAL_CYCLES_BALANCE) .unwrap(); - // 1) canister create and 2) install code. - assert_eq!(2, get_canister_version(&env, canister_id)); + // 1) install code. + assert_eq!(1, get_canister_version(&env, canister_id)); let now_nanos = env.time().duration_since(UNIX_EPOCH).unwrap().as_nanos() as u64; let disable_heartbeats = wasm().trap().build(); let timer = wasm() - .instruction_counter_is_at_least(100_000) + .instruction_counter_is_at_least(1_100_000) .inc_global_counter() .api_global_timer_set(now_nanos) .trap() @@ -2520,9 +2529,9 @@ fn dts_global_timer_with_trap() { assert_eq!(result, WasmResult::Reply(0_u64.to_le_bytes().to_vec())); - // 3) the update. + // 2) the update. let base_canister_version = get_canister_version(&env, canister_id); - assert_eq!(3, base_canister_version); + assert_eq!(2, base_canister_version); for _ in 1..10 { env.tick(); @@ -2538,10 +2547,10 @@ fn dts_global_timer_with_trap() { fn dts_global_timer_does_not_prevent_upgrade() { let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(60_000), + NumInstructions::from(500_000), ); - let binary = UNIVERSAL_CANISTER_WASM.to_vec(); + let binary = UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(); let canister_id = env .install_canister_with_cycles(binary, vec![], None, INITIAL_CYCLES_BALANCE) @@ -2550,7 +2559,7 @@ fn dts_global_timer_does_not_prevent_upgrade() { let now_nanos = env.time().duration_since(UNIX_EPOCH).unwrap().as_nanos() as u64; let timer = wasm() - .instruction_counter_is_at_least(150_000) + .instruction_counter_is_at_least(1_100_000) .inc_global_counter() .api_global_timer_set(now_nanos) .build(); @@ -2567,7 +2576,6 @@ fn dts_global_timer_does_not_prevent_upgrade() { .unwrap(); assert_eq!(result, WasmResult::Reply(0_u64.to_le_bytes().to_vec())); - for i in 1..10 { env.tick(); // Each timer takes three rounds to execute. @@ -2583,13 +2591,19 @@ fn dts_global_timer_does_not_prevent_upgrade() { #[test] fn dts_abort_paused_execution_on_state_switch() { + // Installing the universal canister copies its data segment into memory in a + // single (un-splittable) bulk memory operation that now costs ~240K + // instructions: the first write to each touched Wasm page is charged the full + // deterministic-memory-tracker page overhead (2 * OS pages * 5_000). The slice + // limit must therefore exceed that op, hence 300K (round = slice + slice / 2 = + // 450K). let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(50_000), + NumInstructions::from(300_000), ); let user_id = PrincipalId::new_anonymous(); - let binary = UNIVERSAL_CANISTER_WASM.to_vec(); + let binary = UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(); let canister_id = env .install_canister_with_cycles(binary, vec![], None, INITIAL_CYCLES_BALANCE) @@ -2631,9 +2645,21 @@ fn dts_abort_paused_execution_on_state_switch() { #[test] fn dts_abort_after_dropping_memory_on_state_switch() { + // A single `memory.fill` over the whole 1000-page heap (16_000 OS pages) now + // costs ~225M instructions: the first write to each Wasm page is charged the + // full deterministic-memory-tracker page overhead (2 * 16_000 OS pages * + // 5_000) plus the ~65M byte copy cost. A `memory.fill` can't be split across + // slices, so the slice limit must exceed that single-fill cost. Subsequent + // fills only pay the ~65M byte cost (the pages are already accessed/dirty). + // + // The round limit is `slice + slice / 2` (see `dts_subnet_config`), so with a + // 250M slice a round executes ~375M instructions. `long_update` does 8 fills + // (~225M + 7 * 65M ≈ 684M) which is well above a single round, guaranteeing it + // stays paused (`ContinueLong`) after the first tick so it can be aborted, + // while still fitting within the 1B per-message limit. let env = dts_env( NumInstructions::from(1_000_000_000), - NumInstructions::from(100_000_000), + NumInstructions::from(250_000_000), ); let user_id = PrincipalId::new_anonymous(); @@ -2648,6 +2674,10 @@ fn dts_abort_after_dropping_memory_on_state_switch() { (call $msg_reply) ) (func (export "canister_update long_update") + (memory.fill (i32.const 0) (i32.const 42) (i32.const 65536000)) + (memory.fill (i32.const 0) (i32.const 42) (i32.const 65536000)) + (memory.fill (i32.const 0) (i32.const 42) (i32.const 65536000)) + (memory.fill (i32.const 0) (i32.const 42) (i32.const 65536000)) (memory.fill (i32.const 0) (i32.const 42) (i32.const 65536000)) (memory.fill (i32.const 0) (i32.const 42) (i32.const 65536000)) (memory.fill (i32.const 0) (i32.const 42) (i32.const 65536000)) @@ -2748,9 +2778,10 @@ fn yield_for_dirty_pages_copy_works() { Some(IngressState::Received) ); + env.tick(); env.tick(); - // Only the first message must be completed after two rounds. + // Only the first message must be completed after three rounds. assert_matches!( ingress_state(env.ingress_status(&message_ids[0])), Some(IngressState::Completed(_)) @@ -2800,6 +2831,7 @@ fn yield_for_dirty_pages_copy_works_for_many_canisters() { // Neither of messages should be completed after the first round. assert_eq!(num_completed(), 0); + env.tick(); env.tick(); // Only the first message per scheduler core must be completed after two rounds. @@ -2828,7 +2860,7 @@ fn heavy_install_code_prevents_another_install_code_to_start_in_the_same_round() InstallCodeArgs::new( CanisterInstallMode::Reinstall, canister_id, - UNIVERSAL_CANISTER_WASM.to_vec(), + UNIVERSAL_CANISTER_NO_HEARTBEAT_WASM.to_vec(), canister_init, ) .encode(), @@ -2913,9 +2945,10 @@ fn yield_for_dirty_pages_copy_works_for_install_code() { Some(IngressState::Received) ); + env.tick(); env.tick(); - // Only the first message must be completed after two rounds. + // Only the first message must be completed after three rounds. assert_matches!( ingress_state(env.ingress_status(&message_ids[0])), Some(IngressState::Completed(_)) @@ -2975,18 +3008,20 @@ fn yield_for_dirty_pages_copy_works_for_install_code_and_many_canisters() { // Neither of messages should be completed after the first round. assert_eq!(num_completed(), 0); + env.tick(); env.tick(); - // Only the first message must be completed after two rounds. + // Only the first message must be completed after three rounds. assert_eq!(num_completed(), 1); env.tick(); - // Only the first message must be completed after three rounds. + // Only the first message must be completed after four rounds. assert_eq!(num_completed(), 1); + env.tick(); env.tick(); - // Two heavy install code messages must be completed in four rounds. + // Two heavy install code messages must be completed in five rounds. assert_eq!(num_completed(), 2); } diff --git a/rs/execution_environment/tests/subnet_size_test.rs b/rs/execution_environment/tests/subnet_size_test.rs index 6c119a9b810e..c4fd7bfb5adc 100644 --- a/rs/execution_environment/tests/subnet_size_test.rs +++ b/rs/execution_environment/tests/subnet_size_test.rs @@ -44,13 +44,16 @@ const TEST_CANISTER_INSTALL_EXECUTION_INSTRUCTIONS: u64 = 0; fn deterministic_tracker_write_overhead(n_wasm_pages: u64) -> u64 { use ic_config::flag_status::FlagStatus; use ic_sys::PAGE_SIZE; + let cost = ic_config::subnet_config::SchedulerConfig::application_subnet() + .dirty_page_overhead + .get(); const WASM_PAGE_SIZE: u64 = 65536; if EmbeddersConfig::default() .feature_flags .deterministic_memory_tracker == FlagStatus::Enabled { - n_wasm_pages * 2 * (WASM_PAGE_SIZE / PAGE_SIZE as u64) + n_wasm_pages * 2 * (WASM_PAGE_SIZE / PAGE_SIZE as u64) * cost } else { 0 } @@ -1144,8 +1147,7 @@ fn test_subnet_size_execute_message_cost() { let config = get_cycles_account_manager_config(subnet_type); let reference_subnet_size = config.reference_subnet_size; // The `inc` method writes to the heap (first access), so the deterministic - // memory tracker charges an extra 32 instructions in-band (accessed + dirty - // for 1 Wasm page) when enabled. + // memory tracker charges for accessed + dirty Wasm pages when enabled. let reference_instructions_cost = inc_instruction_cost(HypervisorConfig::default()) + deterministic_tracker_write_overhead(1); let reference_cost = calculate_execution_cost( @@ -1154,10 +1156,15 @@ fn test_subnet_size_execute_message_cost() { reference_subnet_size, ); - // Check default cost. + // Check default cost. The fixed part (1019) is the sum of the `inc` body's + // WASM instruction costs plus the system API overhead, while the dirty-page + // charge for the single heap write is derived from the subnet config. + let dirty_page_overhead = ic_config::subnet_config::SchedulerConfig::application_subnet() + .dirty_page_overhead + .get(); assert_eq!( reference_instructions_cost, - 2019 + deterministic_tracker_write_overhead(1) + 1019 + dirty_page_overhead + deterministic_tracker_write_overhead(1) ); let simulated_cost = simulate_execute_message_cost(subnet_type, reference_subnet_size); assert_eq!( diff --git a/rs/memory_tracker/src/deterministic.rs b/rs/memory_tracker/src/deterministic.rs index 691d2f8efab1..3d58ca1a7a64 100644 --- a/rs/memory_tracker/src/deterministic.rs +++ b/rs/memory_tracker/src/deterministic.rs @@ -330,12 +330,13 @@ pub(crate) struct DeterministicMemoryTracker { checksum: RefCell, pub metrics: MemoryTrackerMetrics, state: RefCell, + page_overhead: u64, subtract_instruction_counter: Arc>, } impl DeterministicMemoryTracker { /// Marks a Wasm page as accessed and adds its OS pages to the accessed_pages list. - /// Charges 1 instruction per OS page accessed. + /// Charges instructions per OS page accessed. fn mark_wasm_page_accessed( &self, state: &mut DeterministicState, @@ -344,7 +345,6 @@ impl DeterministicMemoryTracker { state.mark_wasm_page_accessed(wasm_page_idx); // Add the corresponding OS pages to the accessed_pages vector - // and charge 1 instruction per OS page accessed. let os_page_range = Range::from_wasm_page_idx(wasm_page_idx); let mut accessed_pages = self.accessed_pages.borrow_mut(); let num_os_pages = os_page_range.end.get() - os_page_range.start.get(); @@ -352,19 +352,18 @@ impl DeterministicMemoryTracker { accessed_pages.push(PageIndex::new(os_page_idx)); } - // Charge 1 instruction per OS page accessed. - (self.subtract_instruction_counter.lock())(num_os_pages); + // Charge instructions per OS page accessed. + (self.subtract_instruction_counter.lock())(num_os_pages * self.page_overhead); } /// Marks a Wasm page as dirty. - /// Charges 1 instruction per OS page dirtied. + /// Charges instructions per OS page dirtied. fn mark_wasm_page_dirty(&self, state: &mut DeterministicState, wasm_page_idx: WasmPageIndex) { state.mark_wasm_page_dirty(wasm_page_idx); - // Charge 1 instruction per OS page dirtied. let os_page_range = Range::from_wasm_page_idx(wasm_page_idx); let num_os_pages = os_page_range.end.get() - os_page_range.start.get(); - (self.subtract_instruction_counter.lock())(num_os_pages); + (self.subtract_instruction_counter.lock())(num_os_pages * self.page_overhead); } /// A missing OS page handler that provides deterministic prefetching behavior @@ -473,6 +472,7 @@ impl MemoryTracker for DeterministicMemoryTracker { dirty_page_tracking: DirtyPageTracking, page_map: PageMap, memory_limits: MemoryLimits, + page_overhead: u64, subtract_instruction_counter: Arc>, ) -> nix::Result where @@ -506,6 +506,7 @@ impl MemoryTracker for DeterministicMemoryTracker { checksum: RefCell::new(checksum::SigsegChecksum::default()), metrics: MemoryTrackerMetrics::default(), state: RefCell::new(state), + page_overhead, subtract_instruction_counter, }; diff --git a/rs/memory_tracker/src/lib.rs b/rs/memory_tracker/src/lib.rs index 2c812006e6d4..a7f385d1b035 100644 --- a/rs/memory_tracker/src/lib.rs +++ b/rs/memory_tracker/src/lib.rs @@ -384,6 +384,7 @@ pub trait MemoryTracker { dirty_page_tracking: DirtyPageTracking, page_map: PageMap, memory_limits: MemoryLimits, + page_overhead: u64, subtract_instruction_counter: Arc>, ) -> nix::Result where @@ -440,6 +441,7 @@ pub fn new( page_map: PageMap, missing_page_handler_kind: Option, memory_limits: MemoryLimits, + page_overhead: u64, subtract_instruction_counter: Arc>, ) -> nix::Result { match missing_page_handler_kind { @@ -451,6 +453,7 @@ pub fn new( dirty_page_tracking, page_map, memory_limits, + page_overhead, subtract_instruction_counter, )?)) } @@ -461,6 +464,7 @@ pub fn new( dirty_page_tracking, page_map, memory_limits, + page_overhead, subtract_instruction_counter, )?)), } diff --git a/rs/memory_tracker/src/prefetching.rs b/rs/memory_tracker/src/prefetching.rs index 66c16637e430..db9a239ecc38 100644 --- a/rs/memory_tracker/src/prefetching.rs +++ b/rs/memory_tracker/src/prefetching.rs @@ -60,6 +60,7 @@ impl MemoryTracker for PrefetchingMemoryTracker { dirty_page_tracking: DirtyPageTracking, page_map: PageMap, _memory_limits: MemoryLimits, + _page_overhead: u64, _subtract_instruction_counter: Arc>, ) -> nix::Result where