Skip to content

Commit

Permalink
Merge branch 'andriy/run-126-round-complexity' into 'master'
Browse files Browse the repository at this point in the history
RUN-126: Implement Round Complexity

This MR adds an execution complexity into the round limits.
Each executed message complexity is subtracted from the round limit,
and once the round limit is reached, the inner round breaks and
the round finishes.

Closes RUN-126 

Closes RUN-126

See merge request dfinity-lab/public/ic!8231
  • Loading branch information
berestovskyy committed Feb 4, 2023
2 parents bc53a1c + 03f315a commit a50f141
Show file tree
Hide file tree
Showing 26 changed files with 587 additions and 175 deletions.
37 changes: 31 additions & 6 deletions rs/canister_sandbox/backend_lib/src/dts.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use ic_embedders::wasm_executor::SliceExecutionOutput;
use ic_interfaces::execution_environment::{
HypervisorError, HypervisorResult, OutOfInstructionsHandler,
ExecutionComplexity, HypervisorError, HypervisorResult, OutOfInstructionsHandler,
};
use ic_types::NumInstructions;
use std::sync::{Arc, Condvar, Mutex};
Expand Down Expand Up @@ -33,6 +33,9 @@ struct State {
// The number of instructions that have been executed so far.
// Invariant: it does not exceed `total_instruction_limit`.
instructions_executed: i64,

// The execution complexity accumulated at the beginning of the round.
execution_complexity: ExecutionComplexity,
}

impl State {
Expand All @@ -44,6 +47,7 @@ impl State {
max_slice_instruction_limit,
slice_instruction_limit: max_slice_instruction_limit,
instructions_executed: 0,
execution_complexity: ExecutionComplexity::default(),
};
result.check_invariants();
result
Expand Down Expand Up @@ -94,12 +98,21 @@ impl State {
.max(0)
}

/// Returns the newly observed execution complexity in the current slice.
fn newly_observed_complexity(
&self,
execution_complexity: &ExecutionComplexity,
) -> ExecutionComplexity {
execution_complexity - &self.execution_complexity
}

/// Updates the state to prepare for the next slice.
fn update(&mut self, instruction_counter: i64) {
fn update(&mut self, instruction_counter: i64, execution_complexity: ExecutionComplexity) {
self.instructions_executed = self
.instructions_executed
.saturating_add(self.newly_executed(instruction_counter));
self.slice_instruction_limit = self.next_slice_instruction_limit(instruction_counter);
self.execution_complexity = execution_complexity;
self.check_invariants();
}

Expand Down Expand Up @@ -155,14 +168,19 @@ impl DeterministicTimeSlicing {
// - transitions to `Paused` if it is possible to continue the execution in
// the next slice.
// - or returns the `InstructionLimitExceeded` error.
fn try_pause(&self, instruction_counter: i64) -> Result<SliceExecutionOutput, HypervisorError> {
fn try_pause(
&self,
instruction_counter: i64,
execution_complexity: ExecutionComplexity,
) -> Result<SliceExecutionOutput, HypervisorError> {
let mut state = self.state.lock().unwrap();
assert_eq!(state.execution_status, ExecutionStatus::Running);
if state.is_last_slice() {
return Err(HypervisorError::InstructionLimitExceeded);
}

let newly_executed = state.newly_executed(instruction_counter);
let newly_observed_complexity = state.newly_observed_complexity(&execution_complexity);
if state.next_slice_instruction_limit(instruction_counter) == 0 {
// If the next slice doesn't have any instructions left, then
// execution will fail anyway, so we can return the error now.
Expand All @@ -174,10 +192,11 @@ impl DeterministicTimeSlicing {

// At this pont we know that the next slice will be able to run, so we
// can commit the state changes and pause now.
state.update(instruction_counter);
state.update(instruction_counter, execution_complexity);
state.execution_status = ExecutionStatus::Paused;
Ok(SliceExecutionOutput {
executed_instructions: NumInstructions::from(newly_executed as u64),
execution_complexity: newly_observed_complexity,
})
}

Expand Down Expand Up @@ -251,8 +270,14 @@ impl DeterministicTimeSlicingHandler {
}

impl OutOfInstructionsHandler for DeterministicTimeSlicingHandler {
fn out_of_instructions(&self, instruction_counter: i64) -> HypervisorResult<i64> {
let slice = self.dts.try_pause(instruction_counter)?;
fn out_of_instructions(
&self,
instruction_counter: i64,
execution_complexity: ExecutionComplexity,
) -> HypervisorResult<i64> {
let slice = self
.dts
.try_pause(instruction_counter, execution_complexity)?;
let paused = PausedExecution {
dts: self.dts.clone(),
};
Expand Down
32 changes: 18 additions & 14 deletions rs/canister_sandbox/backend_lib/src/dts/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use std::{
thread,
};

use ic_interfaces::execution_environment::{HypervisorError, OutOfInstructionsHandler};
use ic_interfaces::execution_environment::{
ExecutionComplexity, HypervisorError, OutOfInstructionsHandler,
};
use ic_types::NumInstructions;

use super::{DeterministicTimeSlicingHandler, PausedExecution};
Expand All @@ -15,17 +17,17 @@ fn dts_state_updates() {
assert_eq!(state.instructions_executed, 0);
assert_eq!(state.total_instructions_left(), 2500);
assert!(!state.is_last_slice());
state.update(0);
state.update(0, ExecutionComplexity::default());
assert_eq!(state.slice_instruction_limit, 1000);
assert_eq!(state.instructions_executed, 1000);
assert_eq!(state.total_instructions_left(), 1500);
assert!(!state.is_last_slice());
state.update(0);
state.update(0, ExecutionComplexity::default());
assert_eq!(state.slice_instruction_limit, 500);
assert_eq!(state.instructions_executed, 2000);
assert_eq!(state.total_instructions_left(), 500);
assert!(state.is_last_slice());
state.update(-500);
state.update(-500, ExecutionComplexity::default());
assert_eq!(state.slice_instruction_limit, 0);
assert_eq!(state.instructions_executed, 3000);
assert_eq!(state.total_instructions_left(), -500);
Expand All @@ -39,7 +41,7 @@ fn dts_state_updates_invalid_instructions() {
assert_eq!(state.instructions_executed, 0);
assert_eq!(state.total_instructions_left(), 2500);
assert!(!state.is_last_slice());
state.update(4000);
state.update(4000, ExecutionComplexity::default());
assert_eq!(state.slice_instruction_limit, 2000);
assert_eq!(state.instructions_executed, 0);
assert_eq!(state.total_instructions_left(), 2500);
Expand All @@ -53,7 +55,7 @@ fn dts_state_updates_saturating() {
assert_eq!(state.instructions_executed, 0);
assert_eq!(state.total_instructions_left(), 2500);
assert!(!state.is_last_slice());
state.update(i64::MIN);
state.update(i64::MIN, ExecutionComplexity::default());
assert_eq!(state.slice_instruction_limit, 0);
assert_eq!(state.instructions_executed, i64::MAX);
assert_eq!(state.total_instructions_left(), 2500 - i64::MAX);
Expand All @@ -74,13 +76,13 @@ fn pause_and_resume_works() {
}
});
// Slice 1: executes 1000 instructions before calling `out_of_instructions()`.
let next_slice_limit = dts.out_of_instructions(0).unwrap();
let next_slice_limit = dts.out_of_instructions(0, Default::default()).unwrap();
assert_eq!(1000, next_slice_limit);
// Slice 2: executes 1000 instructions before calling `out_of_instructions()`.
let next_slice_limit = dts.out_of_instructions(0).unwrap();
let next_slice_limit = dts.out_of_instructions(0, Default::default()).unwrap();
assert_eq!(500, next_slice_limit);
// Slice 3: executes 500 instructions before calling `out_of_instructions()`.
let error = dts.out_of_instructions(0);
let error = dts.out_of_instructions(0, Default::default());
assert_eq!(error, Err(HypervisorError::InstructionLimitExceeded));
drop(dts);
control_thread.join().unwrap();
Expand All @@ -98,11 +100,11 @@ fn early_exit_if_slice_does_not_any_instructions_left() {
paused_execution.resume();
});
// Slice 1: executes 1500 instructions before calling `out_of_instructions()`.
let new_slice_limit = dts.out_of_instructions(-500).unwrap();
let new_slice_limit = dts.out_of_instructions(-500, Default::default()).unwrap();
assert_eq!(500, new_slice_limit);
// Slice 2: executes 1500 instructions before calling `out_of_instructions()`
// and fails because the next slice wouldn't have any slice instructions left.
let error = dts.out_of_instructions(-1000);
let error = dts.out_of_instructions(-1000, Default::default());
assert_eq!(
error,
Err(HypervisorError::SliceOverrun {
Expand All @@ -128,13 +130,15 @@ fn invalid_instructions() {
}
});
// Slice 1: executes 1000 instructions before calling `out_of_instructions()`.
let new_instructions = dts.out_of_instructions(0).unwrap();
let new_instructions = dts.out_of_instructions(0, Default::default()).unwrap();
assert_eq!(1000, new_instructions);
// Slice 2: executes 0 instructions before calling `out_of_instructions()`.
let new_instructions = dts.out_of_instructions(i64::MAX).unwrap();
let new_instructions = dts
.out_of_instructions(i64::MAX, Default::default())
.unwrap();
assert_eq!(1000, new_instructions);
// Slice 3: executes more than i64::MAX instructions before calling `out_of_instructions()`.
let error = dts.out_of_instructions(i64::MIN);
let error = dts.out_of_instructions(i64::MIN, Default::default());
assert_eq!(
error,
Err(HypervisorError::SliceOverrun {
Expand Down
2 changes: 1 addition & 1 deletion rs/canister_sandbox/backend_lib/src/sandbox_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ impl Execution {
exec_input.canister_current_memory_usage,
exec_input.execution_parameters,
exec_input.subnet_available_memory,
exec_input.sandox_safe_system_state,
exec_input.sandbox_safe_system_state,
&self.embedder_cache,
&self.sandbox_manager.embedder,
&mut wasm_memory,
Expand Down
4 changes: 2 additions & 2 deletions rs/canister_sandbox/backend_lib/src/sandbox_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ mod tests {
subnet_available_memory: SubnetAvailableMemory::new(i64::MAX / 2, i64::MAX / 2),
next_wasm_memory_id,
next_stable_memory_id,
sandox_safe_system_state: sandbox_safe_system_state(),
sandbox_safe_system_state: sandbox_safe_system_state(),
wasm_reserved_pages: NumWasmPages::from(0),
}
}
Expand All @@ -273,7 +273,7 @@ mod tests {
subnet_available_memory: SubnetAvailableMemory::new(i64::MAX / 2, i64::MAX / 2),
next_wasm_memory_id: MemoryId::new(),
next_stable_memory_id: MemoryId::new(),
sandox_safe_system_state: sandbox_safe_system_state(),
sandbox_safe_system_state: sandbox_safe_system_state(),
wasm_reserved_pages: NumWasmPages::from(0),
}
}
Expand Down
2 changes: 1 addition & 1 deletion rs/canister_sandbox/common/src/protocol/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub struct SandboxExecInput {
pub next_stable_memory_id: MemoryId,
// View of the system_state that is safe for the sandboxed process to
// access.
pub sandox_safe_system_state: SandboxSafeSystemState,
pub sandbox_safe_system_state: SandboxSafeSystemState,
pub wasm_reserved_pages: NumWasmPages,
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,7 @@ impl WasmExecutor for SandboxedExecutionController {
subnet_available_memory,
next_wasm_memory_id,
next_stable_memory_id,
sandox_safe_system_state: sandbox_safe_system_state,
sandbox_safe_system_state,
wasm_reserved_pages: get_wasm_reserved_pages(execution_state),
},
})
Expand Down
15 changes: 13 additions & 2 deletions rs/embedders/src/wasm_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ use crate::{
};
use ic_config::flag_status::FlagStatus;
use ic_interfaces::execution_environment::{
HypervisorError, HypervisorResult, InstanceStats, OutOfInstructionsHandler,
SubnetAvailableMemory, SystemApi, WasmExecutionOutput,
ExecutionComplexity, HypervisorError, HypervisorResult, InstanceStats,
OutOfInstructionsHandler, SubnetAvailableMemory, SystemApi, WasmExecutionOutput,
};
use ic_logger::{warn, ReplicaLogger};
use ic_metrics::MetricsRegistry;
Expand Down Expand Up @@ -118,6 +118,8 @@ impl WasmExecutorMetrics {
pub struct SliceExecutionOutput {
/// The number of instructions executed by the slice.
pub executed_instructions: NumInstructions,
/// The complexity observed in the slice.
pub execution_complexity: ExecutionComplexity,
}

/// Represents a paused WebAssembly execution that can be resumed or aborted.
Expand Down Expand Up @@ -455,6 +457,7 @@ pub fn wasm_execution_error(
WasmExecutionResult::Finished(
SliceExecutionOutput {
executed_instructions: NumInstructions::from(0),
execution_complexity: ExecutionComplexity::default(),
},
WasmExecutionOutput {
wasm_result: Err(err),
Expand Down Expand Up @@ -618,6 +621,7 @@ pub fn process(
return (
SliceExecutionOutput {
executed_instructions: first_slice_instruction_limit,
execution_complexity: ExecutionComplexity::default(),
},
WasmExecutionOutput {
wasm_result: Err(err),
Expand Down Expand Up @@ -677,6 +681,7 @@ pub fn process(

let mut allocated_bytes = NumBytes::from(0);
let mut allocated_message_bytes = NumBytes::from(0);
let mut execution_complexity = ExecutionComplexity::default();

let wasm_state_changes = match run_result {
Ok(run_result) => {
Expand Down Expand Up @@ -717,6 +722,11 @@ pub fn process(
.store_data()
.system_api
.get_allocated_message_bytes();
execution_complexity = instance
.store_data()
.system_api
.execution_complexity()
.clone();

Some(WasmStateChanges::new(
wasm_memory_delta,
Expand All @@ -733,6 +743,7 @@ pub fn process(
(
SliceExecutionOutput {
executed_instructions: slice_instructions_executed,
execution_complexity,
},
WasmExecutionOutput {
wasm_result,
Expand Down
Loading

0 comments on commit a50f141

Please sign in to comment.