Skip to content

Commit ba5ffe0

Browse files
authored
fix: EXC-1741: Fix full execution round definition (#1772)
The full execution round is: 1. When a canister is scheduled first on a CPU core, during the first inner round. 2. When a canister has nothing to execute (NextExecution::None) or canister just executed a full slice of instructions.
1 parent 54c3542 commit ba5ffe0

File tree

2 files changed

+133
-1
lines changed

2 files changed

+133
-1
lines changed

rs/execution_environment/src/scheduler.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,7 @@ impl SchedulerImpl {
735735
&measurement_scope,
736736
&mut round_limits,
737737
registry_settings.subnet_size,
738+
is_first_iteration,
738739
);
739740
let instructions_consumed = instructions_before - round_limits.instructions;
740741
drop(execution_timer);
@@ -874,6 +875,7 @@ impl SchedulerImpl {
874875
measurement_scope: &MeasurementScope,
875876
round_limits: &mut RoundLimits,
876877
subnet_size: usize,
878+
is_first_iteration: bool,
877879
) -> (
878880
Vec<CanisterState>,
879881
BTreeSet<CanisterId>,
@@ -943,6 +945,7 @@ impl SchedulerImpl {
943945
deterministic_time_slicing,
944946
round_limits,
945947
subnet_size,
948+
is_first_iteration,
946949
);
947950
});
948951
}
@@ -1967,6 +1970,7 @@ fn execute_canisters_on_thread(
19671970
deterministic_time_slicing: FlagStatus,
19681971
mut round_limits: RoundLimits,
19691972
subnet_size: usize,
1973+
is_first_iteration: bool,
19701974
) -> ExecutionThreadResult {
19711975
// Since this function runs on a helper thread, we cannot use a nested scope
19721976
// here. Instead, we propagate metrics to the outer scope manually via
@@ -2088,7 +2092,14 @@ fn execute_canisters_on_thread(
20882092
if let Some(es) = &mut canister.execution_state {
20892093
es.last_executed_round = round_id;
20902094
}
2091-
if !canister.has_input() || rank == 0 {
2095+
let full_message_execution = match canister.next_execution() {
2096+
NextExecution::None => true,
2097+
NextExecution::StartNew => false,
2098+
// We just finished a full slice of executions.
2099+
NextExecution::ContinueLong | NextExecution::ContinueInstallCode => true,
2100+
};
2101+
let scheduled_first = is_first_iteration && rank == 0;
2102+
if full_message_execution || scheduled_first {
20922103
// The very first canister is considered to have a full execution round for
20932104
// scheduling purposes even if it did not complete within the round.
20942105
canister.scheduler_state.last_full_execution_round = round_id;

rs/execution_environment/src/scheduler/tests.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5815,3 +5815,124 @@ fn scheduled_heap_delta_limit_scaling() {
58155815
assert_eq!(10, scheduled_limit(50, 50, 9, 10, 5));
58165816
assert_eq!(10, scheduled_limit(55, 50, 9, 10, 5));
58175817
}
5818+
5819+
#[test]
5820+
fn inner_round_first_execution_is_not_a_full_execution() {
5821+
let scheduler_cores = 2;
5822+
let instructions = 20;
5823+
let max_messages_per_round = 3;
5824+
let mut test = SchedulerTestBuilder::new()
5825+
.with_scheduler_config(SchedulerConfig {
5826+
scheduler_cores,
5827+
max_instructions_per_round: (instructions * max_messages_per_round).into(),
5828+
max_instructions_per_message: instructions.into(),
5829+
max_instructions_per_message_without_dts: instructions.into(),
5830+
max_instructions_per_slice: instructions.into(),
5831+
instruction_overhead_per_execution: NumInstructions::from(0),
5832+
instruction_overhead_per_canister: NumInstructions::from(0),
5833+
instruction_overhead_per_canister_for_finalization: NumInstructions::from(0),
5834+
..SchedulerConfig::application_subnet()
5835+
})
5836+
.build();
5837+
5838+
// Bump up the round number.
5839+
test.execute_round(ExecutionRoundType::OrdinaryRound);
5840+
5841+
// Create `scheduler_cores * 2` canisters, so target canister is not scheduled first.
5842+
let mut canister_ids = vec![];
5843+
for _ in 0..scheduler_cores * 2 {
5844+
canister_ids.push(test.create_canister());
5845+
}
5846+
// Create target canister after.
5847+
let target_id = test.create_canister();
5848+
// Send messages to the target canister.
5849+
for canister_id in &canister_ids {
5850+
let message = ingress(instructions).call(
5851+
other_side(target_id, instructions - 1),
5852+
on_response(instructions - 2),
5853+
);
5854+
test.send_ingress(*canister_id, message);
5855+
}
5856+
5857+
test.execute_round(ExecutionRoundType::OrdinaryRound);
5858+
5859+
let mut total_accumulated_priority = 0;
5860+
let mut total_priority_credit = 0;
5861+
for canister in test.state().canisters_iter() {
5862+
let system_state = &canister.system_state;
5863+
let scheduler_state = &canister.scheduler_state;
5864+
// All ingresses should be executed in the previous round.
5865+
assert_eq!(system_state.queues().ingress_queue_size(), 0);
5866+
assert_eq!(system_state.canister_metrics.executed, 1);
5867+
if canister.canister_id() == target_id {
5868+
// The target canister, despite being executed first in the second inner round,
5869+
// should not be marked as fully executed.
5870+
assert_ne!(test.last_round(), 0.into());
5871+
assert_eq!(scheduler_state.last_full_execution_round, 0.into());
5872+
} else {
5873+
assert_eq!(scheduler_state.last_full_execution_round, test.last_round());
5874+
}
5875+
total_accumulated_priority += scheduler_state.accumulated_priority.get();
5876+
total_priority_credit += scheduler_state.priority_credit.get();
5877+
}
5878+
// The accumulated priority invariant should be respected.
5879+
assert_eq!(total_accumulated_priority - total_priority_credit, 0);
5880+
}
5881+
5882+
#[test]
5883+
fn inner_round_long_execution_is_a_full_execution() {
5884+
let scheduler_cores = 2;
5885+
let slice = 20;
5886+
let mut test = SchedulerTestBuilder::new()
5887+
.with_scheduler_config(SchedulerConfig {
5888+
scheduler_cores,
5889+
max_instructions_per_round: (slice * 2).into(),
5890+
max_instructions_per_message: (slice * 10).into(),
5891+
max_instructions_per_message_without_dts: slice.into(),
5892+
max_instructions_per_slice: slice.into(),
5893+
instruction_overhead_per_execution: NumInstructions::from(0),
5894+
instruction_overhead_per_canister: NumInstructions::from(0),
5895+
instruction_overhead_per_canister_for_finalization: NumInstructions::from(0),
5896+
..SchedulerConfig::application_subnet()
5897+
})
5898+
.build();
5899+
5900+
// Bump up the round number.
5901+
test.execute_round(ExecutionRoundType::OrdinaryRound);
5902+
5903+
// Create `scheduler_cores` canisters, so target canister is not scheduled first.
5904+
let mut canister_ids = vec![];
5905+
for _ in 0..scheduler_cores {
5906+
let canister_id = test.create_canister();
5907+
test.send_ingress(canister_id, ingress(slice));
5908+
canister_ids.push(canister_id);
5909+
}
5910+
// Create a target canister with two long executions.
5911+
let target_id = test.create_canister();
5912+
test.send_ingress(target_id, ingress(slice * 2 + 1));
5913+
test.send_ingress(target_id, ingress(slice * 2 + 1));
5914+
5915+
test.execute_round(ExecutionRoundType::OrdinaryRound);
5916+
5917+
let mut total_accumulated_priority = 0;
5918+
let mut total_priority_credit = 0;
5919+
for canister in test.state().canisters_iter() {
5920+
let system_state = &canister.system_state;
5921+
let scheduler_state = &canister.scheduler_state;
5922+
// All canisters should be executed.
5923+
assert_eq!(system_state.canister_metrics.executed, 1);
5924+
if canister.canister_id() == target_id {
5925+
// The target canister was not executed first, and still have messages.
5926+
assert_eq!(system_state.queues().ingress_queue_size(), 1);
5927+
} else {
5928+
assert_eq!(system_state.queues().ingress_queue_size(), 0);
5929+
}
5930+
// All canisters should be marked as fully executed. The target canister,
5931+
// despite still having messages, executed a full slice of instructions.
5932+
assert_eq!(scheduler_state.last_full_execution_round, test.last_round());
5933+
total_accumulated_priority += scheduler_state.accumulated_priority.get();
5934+
total_priority_credit += scheduler_state.priority_credit.get();
5935+
}
5936+
// The accumulated priority invariant should be respected.
5937+
assert_eq!(total_accumulated_priority - total_priority_credit, 0);
5938+
}

0 commit comments

Comments
 (0)