Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a26c963
feat: Add subnet_metrics management canister endpoint
Aug 5, 2026
8134604
fix: Correct subnet_metrics composite-query test and document field f…
Aug 5, 2026
eabc3d2
refactor: Simplify subnet_metrics change
Aug 5, 2026
0e795b7
refactor: Record subnet_metrics as counts_toward_round_limit
Aug 5, 2026
8896448
Merge branch 'master' into subnet-metrics-endpoint
Dfinity-Bjoern Aug 6, 2026
044e826
fix: Drop the subnet_metrics composite-query routing arm after merge
Aug 6, 2026
823c390
fix: Adapt subnet_metrics system tests to the merged query path
Aug 6, 2026
2408570
Merge branch 'master' into subnet-metrics-endpoint
mraszyk Aug 24, 2026
96d47b5
feat: Store the canisters' consumed cycles in SubnetMetrics
mraszyk Aug 24, 2026
f0c4e65
Apply suggestions from code review
mraszyk Aug 25, 2026
ab95a76
fix: Derive the consumed cycles gauge from the stored aggregate
mraszyk Aug 25, 2026
f522f61
Merge branch 'master' into subnet-metrics-endpoint
Dfinity-Bjoern Aug 25, 2026
9742bc3
test: Drop the consumed cycles restart test until V29 lands
mraszyk Aug 25, 2026
68077b4
Update rs/replicated_state/src/metadata_state.rs
mraszyk Aug 25, 2026
34ad262
docs: Trim the consumed cycles comments
mraszyk Aug 25, 2026
6b63089
Merge branch 'mraszyk/store-consumed-cycles-by-canisters' into subnet…
mraszyk Aug 26, 2026
4d10879
fix: Refresh the stored consumed cycles in the subnet_metrics tests
mraszyk Aug 26, 2026
ad35e5b
refactor: Charge no instructions for the subnet_metrics endpoint
mraszyk Aug 26, 2026
d83af6d
test: Keep only the cross-subnet subnet_metrics system test
mraszyk Aug 26, 2026
5094449
docs: Rejustify the unconditional canister repartitioning
mraszyk Aug 26, 2026
969f733
Merge remote-tracking branch 'origin/master' into subnet-metrics-endp…
mraszyk Aug 26, 2026
ade7910
test: Drop the hot/cold partitioning tests
mraszyk Aug 26, 2026
425128b
refactor: Revert collateral edits outside the subnet_metrics endpoint
mraszyk Aug 26, 2026
78cb669
Merge remote-tracking branch 'origin/master' into subnet-metrics-endp…
mraszyk Aug 26, 2026
c7e0089
test: Consolidate the subnet_metrics tests and align its docs
mraszyk Aug 26, 2026
98a7574
docs: Record the subnet_metrics types under [Unreleased]
mraszyk Aug 26, 2026
c21f1dc
fix: Report a committed consumed-cycles aggregate from subnet_metrics
mraszyk Aug 26, 2026
f622601
Merge branch 'master' into subnet-metrics-endpoint
mraszyk Aug 26, 2026
99b7d3a
Merge branch 'master' into subnet-metrics-endpoint
mraszyk Aug 27, 2026
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
5 changes: 5 additions & 0 deletions packages/ic-management-canister-types/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Types for `subnet_metrics`:
- Added the types `SubnetMetricsArgs` and `SubnetMetricsResult`.

## [0.9.0] - 2026-08-13

### Added
Expand Down
59 changes: 59 additions & 0 deletions packages/ic-management-canister-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,65 @@ pub struct SubnetInfoResult {
pub registry_version: u64,
}

/// # Subnet Metrics Args.
///
/// Argument type of [`subnet_metrics`](https://docs.internetcomputer.org/references/management-canister/#subnet_metrics).
#[derive(
CandidType, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone,
)]
pub struct SubnetMetricsArgs {
/// Subnet ID.
pub subnet_id: Principal,
}

/// # Subnet Metrics Result.
///
/// Result type of [`subnet_metrics`](https://docs.internetcomputer.org/references/management-canister/#subnet_metrics).
///
/// This API is EXPERIMENTAL and may evolve in a non-backward-compatible way.
///
/// # Freshness
///
/// Only `block_height` is current as of the block in which the call is executed.
/// The other four are read from the subnet's aggregated metrics, which the replica
/// updates at the *end* of a round, so they describe the state as of an earlier
/// block:
///
/// - `num_canisters`, `consumed_cycles_total` and `update_transactions_total` are
/// as of the end of the previous round.
/// - `canister_state_bytes` is recomputed only every 10 rounds, because summing it
/// over every canister is expensive and it does not need to be exact. It can
/// therefore be up to ten rounds stale, and reads as `0` for the first rounds
/// after the subnet is created.
///
/// These are the same values, with the same staleness, that `read_state` returns
/// for the `/subnet/<subnet_id>/metrics` path, so the two agree.
Comment thread
mraszyk marked this conversation as resolved.
#[derive(
CandidType, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone,
)]
pub struct SubnetMetricsResult {
/// Height of the block in whose execution the call is processed.
/// Monotonically non-decreasing for a given subnet; the heights of different
/// subnets are unrelated.
pub block_height: Nat,
/// Number of canisters on the subnet, as of the end of the previous round.
pub num_canisters: Nat,
/// Total size in bytes of the state taken by the canisters on the subnet, as
/// of the end of the previous round.
///
/// Recomputed only every 10 rounds, so this can be up to ten rounds stale
/// (and reads as `0` for the first rounds after the subnet is created). See
/// the type-level "Freshness" note.
pub canister_state_bytes: Nat,
/// Total cycles removed from circulation on the subnet by all current and
/// deleted canisters, as of the end of the previous round.
pub consumed_cycles_total: Nat,
/// Total number of transactions processed on the subnet, i.e. the total
/// number of messages executed in replicated mode, as of the end of the
/// previous round.
pub update_transactions_total: Nat,
}

/// # Canister ID Range.
///
/// A closed range of canister IDs, both endpoints inclusive.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ fn node_metrics_history(_: NodeMetricsHistoryArgs) -> NodeMetricsHistoryResult {
unimplemented!()
}

#[candid_method(update)]
fn subnet_metrics(_: SubnetMetricsArgs) -> SubnetMetricsResult {
unimplemented!()
}

#[candid_method(update)]
fn provisional_create_canister_with_cycles(
_: ProvisionalCreateCanisterWithCyclesArgs,
Expand Down
33 changes: 33 additions & 0 deletions packages/ic-management-canister-types/tests/ic.did
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,38 @@ type node_metrics_history_result = vec record {
node_metrics : vec node_metrics;
};

type subnet_metrics_args = record {
subnet_id : principal;
};

// This API is EXPERIMENTAL and may evolve in a non-backward-compatible way.
//
// Only `block_height` is current as of the block in which the call is executed.
// The other four fields are read from the subnet's aggregated metrics, which the
// replica updates at the *end* of a round, so they describe the state as of an
// earlier block; see the individual fields.
type subnet_metrics_result = record {
// Height of the block in whose execution this call is processed.
// Monotonically non-decreasing for a given subnet; the heights of different
// subnets are unrelated.
block_height : nat;
// Number of canisters on the subnet, as of the end of the previous round.
num_canisters : nat;
// Total size in bytes of the state taken by the canisters on the subnet, as
// of the end of the previous round. Recomputed only every 10 rounds, because
// summing it over every canister is expensive and it does not need to be
// exact, so it can be up to ten rounds stale and reads as 0 for the first
// rounds after the subnet is created.
canister_state_bytes : nat;
// Total cycles removed from circulation on the subnet by all current and
// deleted canisters, as of the end of the previous round.
consumed_cycles_total : nat;
// Total number of transactions processed on the subnet, i.e. the total
// number of messages executed in replicated mode, as of the end of the
// previous round.
update_transactions_total : nat;
};

type subnet_info_args = record {
subnet_id : principal;
};
Expand Down Expand Up @@ -709,6 +741,7 @@ service ic : {

// metrics interface
node_metrics_history : (node_metrics_history_args) -> (node_metrics_history_result);
subnet_metrics : (subnet_metrics_args) -> (subnet_metrics_result);

// subnet info
subnet_info : (subnet_info_args) -> (subnet_info_result);
Expand Down
7 changes: 4 additions & 3 deletions rs/embedders/src/wasmtime_embedder/system_api/routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ use ic_management_canister_types_private::{
NodeMetricsHistoryArgs, Payload, ProvisionalTopUpCanisterArgs, ReadCanisterSnapshotDataArgs,
ReadCanisterSnapshotMetadataArgs, RenameCanisterArgs, ReshareChainKeyArgs,
SchnorrPublicKeyArgs, SetupInitialDKGArgs, SignWithECDSAArgs, SignWithSchnorrArgs,
StoredChunksArgs, SubnetInfoArgs, TakeCanisterSnapshotArgs, UninstallCodeArgs,
UpdateSettingsArgs, UploadCanisterSnapshotDataArgs, UploadCanisterSnapshotMetadataArgs,
UploadChunkArgs, VetKdDeriveKeyArgs, VetKdPublicKeyArgs,
StoredChunksArgs, SubnetInfoArgs, SubnetMetricsArgs, TakeCanisterSnapshotArgs,
UninstallCodeArgs, UpdateSettingsArgs, UploadCanisterSnapshotDataArgs,
UploadCanisterSnapshotMetadataArgs, UploadChunkArgs, VetKdDeriveKeyArgs, VetKdPublicKeyArgs,
};
use ic_replicated_state::NetworkTopology;
use itertools::Itertools;
Expand Down Expand Up @@ -201,6 +201,7 @@ pub(super) fn resolve_destination(
Ok(Ic00Method::NodeMetricsHistory) => {
Ok(NodeMetricsHistoryArgs::decode(payload)?.subnet_id)
}
Ok(Ic00Method::SubnetMetrics) => Ok(SubnetMetricsArgs::decode(payload)?.subnet_id),
Ok(Ic00Method::SubnetInfo) => Ok(SubnetInfoArgs::decode(payload)?.subnet_id),
Ok(Ic00Method::FetchCanisterLogs) => {
let canister_id = FetchCanisterLogsRequest::decode(payload)?.get_canister_id();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ impl SystemStateModifications {
| Ok(Ic00Method::BitcoinSendTransaction)
| Ok(Ic00Method::BitcoinGetCurrentFeePercentiles)
| Ok(Ic00Method::NodeMetricsHistory)
| Ok(Ic00Method::SubnetMetrics)
| Ok(Ic00Method::SubnetInfo)
| Ok(Ic00Method::FetchCanisterLogs)
| Ok(Ic00Method::UploadChunk)
Expand Down
4 changes: 4 additions & 0 deletions rs/execution_environment/src/canister_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ impl CanisterManager {
| Ok(Ic00Method::BitcoinSendTransactionInternal)
| Ok(Ic00Method::BitcoinGetCurrentFeePercentiles)
| Ok(Ic00Method::NodeMetricsHistory)
// Unreachable for `SubnetMetrics`: `extract_effective_canister_id`
// rejects it earlier, at the ingress filter. Listed for exhaustiveness
// and as defence in depth.
| Ok(Ic00Method::SubnetMetrics)
| Ok(Ic00Method::SubnetInfo)
// `RenameCanister` can only be called from the NNS subnet.
| Ok(Ic00Method::RenameCanister) => Err(UserError::new(
Expand Down
132 changes: 130 additions & 2 deletions rs/execution_environment/src/canister_manager/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ use ic_management_canister_types_private::{
InstallCodeArgsV2, Method, NodeMetricsHistoryArgs, NodeMetricsHistoryResponse,
OnLowWasmMemoryHookStatus, Payload, ProvisionalCreateCanisterWithCyclesArgs,
RenameCanisterArgs, RenameToArgs, StoredChunksArgs, StoredChunksReply, SubnetInfoArgs,
SubnetInfoResponse, TakeCanisterSnapshotArgs, UpdateSettingsArgs, UploadChunkArgs,
UploadChunkReply, WasmMemoryPersistence,
SubnetInfoResponse, SubnetMetricsArgs, SubnetMetricsResponse, TakeCanisterSnapshotArgs,
UpdateSettingsArgs, UploadChunkArgs, UploadChunkReply, WasmMemoryPersistence,
};
use ic_metrics::MetricsRegistry;
use ic_registry_provisional_whitelist::ProvisionalWhitelist;
Expand Down Expand Up @@ -6328,6 +6328,134 @@ fn subnet_info_ingress_fails() {
);
}

/// Sends the given payload to `subnet_metrics` as an inter-canister call from a
/// canister on a remote subnet, executes it, and returns the decoded response or
/// the reject.
fn subnet_metrics_raw_call(
test: &mut ExecutionTest,
payload: Vec<u8>,
) -> Result<SubnetMetricsResponse, (RejectCode, String)> {
test.inject_call_to_ic00(Method::SubnetMetrics, payload, Cycles::zero());
test.execute_subnet_message();
// Route the response back towards the caller (on a different subnet) so that
// it can be inspected via `xnet_messages`.
test.induct_messages();
// The response to the one injected call is the only message crossing the
// subnet boundary.
assert_eq!(test.xnet_messages().len(), 1);
match &test.get_xnet_response(0).response_payload {
ic_types::messages::Payload::Data(bytes) => {
Ok(Decode!(bytes, SubnetMetricsResponse).unwrap())
}
ic_types::messages::Payload::Reject(context) => {
Err((context.code(), context.message().to_string()))
}
}
}

/// As [`subnet_metrics_raw_call`], with a well-formed payload naming `subnet_id`.
fn subnet_metrics_call(
test: &mut ExecutionTest,
subnet_id: PrincipalId,
) -> Result<SubnetMetricsResponse, (RejectCode, String)> {
subnet_metrics_raw_call(test, SubnetMetricsArgs { subnet_id }.encode())
}

#[test]
fn subnet_metrics_ingress_update_fails_at_ingress_filter() {
let own_subnet_id = subnet_test_id(1);
let mut test = ExecutionTestBuilder::new()
.with_own_subnet_id(own_subnet_id)
.build();
let payload = SubnetMetricsArgs {
subnet_id: own_subnet_id.get(),
}
.encode();

let result = test.should_accept_ingress_message(IC_00, Method::SubnetMetrics, payload);
assert_eq!(
result,
Err(UserError::new(
ErrorCode::CanisterRejectedMessage,
"ic00 method subnet_metrics can not be called via ingress messages"
))
);
}

#[test]
fn subnet_metrics_ingress_update_fails_at_execution() {
let own_subnet_id = subnet_test_id(1);
let mut test = ExecutionTestBuilder::new()
.with_own_subnet_id(own_subnet_id)
.build();
let payload = SubnetMetricsArgs {
subnet_id: own_subnet_id.get(),
}
.encode();
test.subnet_message(Method::SubnetMetrics, payload)
.unwrap_err()
.assert_contains(
ErrorCode::CanisterContractViolation,
"subnet_metrics cannot be called by a user",
);
}

#[test]
fn subnet_metrics_ingress_query_fails() {
let own_subnet_id = subnet_test_id(1);
let mut test = ExecutionTestBuilder::new()
.with_own_subnet_id(own_subnet_id)
.build();
let payload = SubnetMetricsArgs {
subnet_id: own_subnet_id.get(),
}
.encode();
test.non_replicated_query(CanisterId::ic_00(), "subnet_metrics", payload)
.unwrap_err()
.assert_contains(
ErrorCode::CanisterMethodNotFound,
"Query method subnet_metrics not found.",
);
}

#[test]
fn subnet_metrics_foreign_subnet_id_is_rejected() {
let own_subnet_id = subnet_test_id(1);
let other_subnet_id = subnet_test_id(3);
let caller_canister = canister_test_id(1);
let mut test = ExecutionTestBuilder::new()
.with_own_subnet_id(own_subnet_id)
.with_caller(subnet_test_id(2), caller_canister)
.build();

let (code, message) = subnet_metrics_call(&mut test, other_subnet_id.get()).unwrap_err();
assert_eq!(code, RejectCode::CanisterReject);
assert!(
message.contains("does not match current subnet ID"),
"unexpected reject message: {message}"
);
}

#[test]
fn subnet_metrics_malformed_payload_is_rejected() {
let own_subnet_id = subnet_test_id(1);
let caller_canister = canister_test_id(1);
let mut test = ExecutionTestBuilder::new()
.with_own_subnet_id(own_subnet_id)
.with_caller(subnet_test_id(2), caller_canister)
.build();

let (code, message) =
subnet_metrics_raw_call(&mut test, EmptyBlob.encode()).expect_err("expected a reject");
// The Candid decode failure surfaces as `ErrorCode::InvalidManagementPayload`
// (`candid_error_to_user_error`), which maps to `RejectCode::CanisterReject`.
assert_eq!(code, RejectCode::CanisterReject);
assert!(
message.contains("Error decoding candid"),
"unexpected reject message: {message}"
);
}

#[test]
fn node_metrics_history_update_succeeds() {
let own_subnet_id = subnet_test_id(1);
Expand Down
Loading
Loading