Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion rs/embedders/src/wasmtime_embedder/system_api/routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ pub(super) fn resolve_destination(
// Figure out the destination subnet based on the method and the payload.
let method = Ic00Method::from_str(method_name);
match method {
Ok(Ic00Method::CreateCanister)
Ok(Ic00Method::ListCanisters)
| Ok(Ic00Method::CreateCanister)
| Ok(Ic00Method::RawRand)
| Ok(Ic00Method::ProvisionalCreateCanisterWithCycles)
| Ok(Ic00Method::HttpRequest)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ impl SystemStateModifications {
| Ok(Ic00Method::CanisterStatus)
| Ok(Ic00Method::CanisterInfo)
| Ok(Ic00Method::CanisterMetadata)
| Ok(Ic00Method::ListCanisters)
| Ok(Ic00Method::StartCanister)
| Ok(Ic00Method::StopCanister)
| Ok(Ic00Method::DeleteCanister)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use crate::create_canisters::CreateCanistersArgs;
use crate::utils::{CANISTERS_PER_BATCH, expect_reply, test_canister_wasm};
use candid::Encode;
use criterion::{BenchmarkGroup, Criterion, criterion_group, criterion_main};
use ic_base_types::CanisterId;
use ic_config::subnet_config::SubnetConfig;
use ic_config::{execution_environment::Config as HypervisorConfig, flag_status::FlagStatus};
use ic_registry_subnet_type::SubnetType;
use ic_state_machine_tests::{StateMachine, StateMachineBuilder, StateMachineConfig};
use ic_types_cycles::{CanisterCyclesCostSchedule, Cycles};

/// Canister ID assigned to the subnet-admin test canister. On a fresh subnet
/// the first created canister gets the first ID in the subnet's allocation
/// range (i.e. `0`), so the test canister is created first (before the
/// canisters populating the subnet) to receive this ID.
fn admin_canister_id() -> CanisterId {
CanisterId::from_u64(0)
}

/// Builds a `StateMachine` whose subnet has subnet admins configured (which
/// requires a `Free` cost schedule on an application subnet), installs the test
/// canister as the sole subnet admin, and populates the subnet with
/// `canisters_number` additional canisters. Returns the `StateMachine` and the
/// test canister ID.
fn setup_with_canisters(canisters_number: u64) -> (StateMachine, CanisterId) {
let hypervisor_config = HypervisorConfig {
rate_limiting_of_heap_delta: FlagStatus::Disabled,
..Default::default()
};
let admin = admin_canister_id();
let env = StateMachineBuilder::new()
.with_config(Some(StateMachineConfig::new(
SubnetConfig::new(SubnetType::Application),
hypervisor_config,
)))
.with_checkpoints_enabled(false)
.with_subnet_type(SubnetType::Application)
.with_cost_schedule(CanisterCyclesCostSchedule::Free)
.with_subnet_admins(vec![admin.get()])
.build();

// Create the test canister first so that it receives the first canister ID
// in the subnet's allocation range, which matches the pre-configured
// subnet-admin ID.
let test_canister = env.create_canister_with_cycles(None, Cycles::new(u128::MAX / 2), None);
assert_eq!(test_canister, admin);
env.install_existing_canister(test_canister, test_canister_wasm(), vec![])
.expect("failed to install the test canister");

// Populate the subnet with `canisters_number` additional canisters via the
// test canister (batched inter-canister calls). The canisters are created
// with gaps in between so that the subnet's canister IDs form roughly
// `canisters_number` distinct ranges, which `list_canisters` must report.
//
// The work is split into chunks so that no single ingress message exceeds
// the state machine's per-message tick budget (each chunk creates twice as
// many canisters and then deletes half of them). Gaps are preserved across
// chunk boundaries because each chunk ends with a deleted canister ID.
const CHUNK: u64 = 5_000;
let mut remaining_to_create = canisters_number;
let mut created_ranges = 0;
while remaining_to_create > 0 {
let chunk = remaining_to_create.min(CHUNK);
remaining_to_create -= chunk;
let result = env.execute_ingress(
test_canister,
"create_canisters_with_gaps",
Encode!(&CreateCanistersArgs {
canisters_number: chunk,
canisters_per_batch: CANISTERS_PER_BATCH,
initial_cycles: 0,
})
.unwrap(),
);
created_ranges += expect_reply::<u64>(result);
}
assert_eq!(created_ranges, canisters_number);

(env, test_canister)
}

fn run_bench<M: criterion::measurement::Measurement>(
group: &mut BenchmarkGroup<M>,
bench_name: &str,
canisters_number: u64,
) {
// `list_canisters` is read-only, so the environment (and its set of
// canisters) does not change across iterations and can be set up once.
let (env, test_canister) = setup_with_canisters(canisters_number);
group.bench_function(bench_name, |b| {
b.iter(|| {
let result = env.execute_ingress(test_canister, "list_canisters", Encode!().unwrap());
let ranges: u64 = expect_reply(result);
// The canisters are created with gaps, so there should be roughly
// one range per canister on the subnet.
assert!(ranges >= canisters_number / 2);
});
});
}

pub fn list_canisters_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("list_canisters");

run_bench(&mut group, "10", 10);
run_bench(&mut group, "100", 100);
run_bench(&mut group, "1k", 1_000);
run_bench(&mut group, "10k", 10_000);
run_bench(&mut group, "50k", 50_000);

group.finish();
}

criterion_group!(benchmarks, list_canisters_benchmark);
criterion_main!(benchmarks);
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod create_execution_state;
mod ecdsa;
mod http_request;
mod install_code;
mod list_canisters;
mod update_settings;
mod utils;

Expand All @@ -18,6 +19,7 @@ fn all_benchmarks(c: &mut Criterion) {
ecdsa::ecdsa_benchmark(c);
http_request::http_request_benchmark(c);
install_code::install_code_benchmark(c);
list_canisters::list_canisters_benchmark(c);
update_settings::update_settings_benchmark(c);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ type http_request_args = record {

service : {
"create_canisters" : (create_canisters_args) -> (vec principal);
"create_canisters_with_gaps" : (create_canisters_args) -> (nat64);
"install_code" : (install_code_args) -> ();
"update_settings" : (update_settings_args) -> ();
"ecdsa_public_key" : (ecdsa_args) -> ();
"sign_with_ecdsa" : (ecdsa_args) -> ();
"http_request" : (http_request_args) -> ();
"list_canisters" : () -> (nat64);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ use ic_cdk::api::management_canister::http_request::{
http_request as ic_cdk_http_request,
};
use ic_cdk::api::management_canister::main::{
CanisterInstallMode, CanisterSettings, CreateCanisterArgument, InstallCodeArgument,
UpdateSettingsArgument, create_canister as ic_cdk_create_canister,
install_code as ic_cdk_install_code, update_settings as ic_cdk_update_settings,
CanisterIdRecord, CanisterInstallMode, CanisterSettings, CreateCanisterArgument,
InstallCodeArgument, UpdateSettingsArgument, create_canister as ic_cdk_create_canister,
delete_canister as ic_cdk_delete_canister, install_code as ic_cdk_install_code,
stop_canister as ic_cdk_stop_canister, update_settings as ic_cdk_update_settings,
};
use ic_cdk::call::Call;
use ic_cdk::update;
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -59,6 +61,62 @@ async fn create_canisters(args: CreateCanistersArgs) -> Vec<Principal> {
result
}

/// Creates `2 * args.canisters_number` canisters and then deletes every other
/// one (in canister ID order), leaving `args.canisters_number` canisters
/// separated by gaps. As a result, the management canister's `list_canisters`
/// method reports (roughly) one ID range per remaining canister. Returns the
/// number of remaining canisters.
#[update]
async fn create_canisters_with_gaps(args: CreateCanistersArgs) -> u64 {
let mut canister_ids = create_canisters(CreateCanistersArgs {
canisters_number: args.canisters_number * 2,
canisters_per_batch: args.canisters_per_batch,
initial_cycles: args.initial_cycles,
})
.await;

// Canister ID principals encode the canister index in big-endian order, so
// sorting by the principal's bytes yields the numeric canister ID order.
canister_ids.sort_by(|a, b| a.as_slice().cmp(b.as_slice()));

// Delete every other canister so that the remaining ones are separated by
// gaps (i.e. each remaining canister forms its own ID range).
let to_delete: Vec<Principal> = canister_ids.iter().skip(1).step_by(2).copied().collect();
let mut remaining = to_delete.as_slice();
while !remaining.is_empty() {
let batch_size = (args.canisters_per_batch as usize).min(remaining.len());
let (batch, rest) = remaining.split_at(batch_size);
remaining = rest;

// A canister must be stopped before it can be deleted.
let stop_futures: Vec<_> = batch
.iter()
.map(|canister_id| {
ic_cdk_stop_canister(CanisterIdRecord {
canister_id: *canister_id,
})
})
.collect();
join_all(stop_futures).await.into_iter().for_each(|r| {
r.unwrap(); // Reject if there is an error.
});

let delete_futures: Vec<_> = batch
.iter()
.map(|canister_id| {
ic_cdk_delete_canister(CanisterIdRecord {
canister_id: *canister_id,
})
})
.collect();
join_all(delete_futures).await.into_iter().for_each(|r| {
r.unwrap(); // Reject if there is an error.
});
}

(canister_ids.len() - to_delete.len()) as u64
}

#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
pub struct InstallCodeArgs {
pub canister_ids: Vec<Principal>,
Expand Down Expand Up @@ -220,4 +278,29 @@ async fn http_request(args: HttpRequestArgs) {
});
}

#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
pub struct CanisterIdRange {
pub start: Principal,
pub end: Principal,
}

#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
pub struct ListCanistersResult {
pub canisters: Vec<CanisterIdRange>,
}

/// Calls the management canister's `list_canisters` method (which takes no
/// arguments) and returns the number of canister ID ranges reported for the
/// subnet. This canister must be a subnet admin for the call to succeed.
#[update]
async fn list_canisters() -> u64 {
let result: ListCanistersResult =
Call::unbounded_wait(Principal::management_canister(), "list_canisters")
.await
.expect("list_canisters call failed")
.candid()
.expect("failed to decode list_canisters response");
result.canisters.len() as u64
}

fn main() {}
2 changes: 2 additions & 0 deletions rs/execution_environment/src/canister_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ impl CanisterManager {
Err(_)
| Ok(Ic00Method::CanisterInfo)
| Ok(Ic00Method::CanisterMetadata)
// `list_canisters` can only be called via inter-canister calls by subnet admins.
| Ok(Ic00Method::ListCanisters)
| Ok(Ic00Method::ECDSAPublicKey)
| Ok(Ic00Method::SetupInitialDKG)
| Ok(Ic00Method::SignWithECDSA)
Expand Down
63 changes: 61 additions & 2 deletions rs/execution_environment/src/execution/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::{
ExecuteMessageResult, HypervisorMetrics, RoundLimits, as_round_instructions,
canister_manager::types::CanisterManagerError, metrics::CallTreeMetrics,
};
use candid::Encode;
use ic_base_types::{CanisterId, NumBytes, SubnetId};
use ic_embedders::{
wasm_executor::{CanisterStateChanges, ExecutionStateChanges, SliceExecutionOutput},
Expand All @@ -18,11 +19,14 @@ use ic_interfaces::execution_environment::{
HypervisorError, HypervisorResult, SubnetAvailableMemory, WasmExecutionOutput,
};
use ic_logger::{ReplicaLogger, error, fatal, info, warn};
use ic_management_canister_types_private::CanisterStatusType;
use ic_management_canister_types_private::{
CanisterIdRange, CanisterStatusType, EmptyBlob, ListCanistersResponse, Payload as _,
};
use ic_registry_routing_table::canister_id_into_u64;
use ic_registry_subnet_type::SubnetType;
use ic_replicated_state::{
CallContext, CallContextAction, CallOrigin, CanisterState, ExecutionState, NetworkTopology,
SystemState,
ReplicatedState, SystemState,
};
use ic_types::ingress::{IngressState, IngressStatus, WasmResult};
use ic_types::messages::{
Expand Down Expand Up @@ -417,6 +421,61 @@ pub(crate) fn validate_controller_or_subnet_admin(
}
}

/// Computes the response to the `list_canisters` management canister method.
///
/// The method takes no arguments and is only available on subnets with subnet
/// admins configured, in which case the caller must be a subnet admin. On
/// success, it returns the Candid-encoded `ListCanistersResponse` listing the
/// ranges of canister IDs hosted on this subnet.
pub(crate) fn list_canisters(
state: &ReplicatedState,
caller: &PrincipalId,
payload: &[u8],
) -> Result<Vec<u8>, UserError> {
EmptyBlob::decode(payload)?;
match state.get_own_subnet_admins() {
Some(ref admins) => validate_subnet_admin(admins, caller).map_err(UserError::from)?,
None => {
return Err(UserError::new(
ErrorCode::CanisterRejectedMessage,
"list_canisters is only available on subnets with subnet admins",
));
}
}
let mut canisters: Vec<CanisterIdRange> = Vec::new();
for id in state.canister_states().all_keys() {
let id_u64 = canister_id_into_u64(*id);
match canisters.last_mut() {
Some(last) if canister_id_into_u64(last.end).checked_add(1) == Some(id_u64) => {
last.end = *id;
}
_ => canisters.push(CanisterIdRange {
start: *id,
end: *id,
}),
}
}
let response = ListCanistersResponse { canisters };
Ok(Encode!(&response).unwrap())
}

/// Computes the number of round instructions consumed by executing the
/// `list_canisters` management method against the given state.
///
/// The cost model was derived from the `list_canisters` benchmark using the
/// conversion `2B instructions = 1 second` (i.e. `2M instructions = 1 ms`):
/// - a base cost of 20M instructions (≈10ms), and
/// - a variable cost of 16K instructions per canister hosted on the subnet
/// (`list_canisters` iterates over all of them to build the ID ranges).
/// The variable cost reflects the worst case where the canister IDs form
/// gaps so that each canister becomes its own ID range.
pub(crate) fn list_canisters_instructions(state: &ReplicatedState) -> NumInstructions {
const BASE_INSTRUCTIONS: u64 = 20_000_000;
const INSTRUCTIONS_PER_CANISTER: u64 = 16_000;
let num_canisters = state.num_canisters() as u64;
NumInstructions::new(BASE_INSTRUCTIONS + INSTRUCTIONS_PER_CANISTER * num_canisters)
}

/// Unregisters the callback corresponding to the given response.
//
// TODO(DSM-95): Consider making this only apply to non-replicated call origins.
Expand Down
Loading
Loading