spec: add subnet_metrics management canister endpoint - #333
Dfinity-Bjoern wants to merge 16 commits into
Conversation
Proposal for discussion. Adds a subnet_metrics endpoint returning the subnet block height plus the four subnet-wide metrics that are currently only reachable by external users via the certified state tree path /subnet/<subnet_id>/metrics.
|
🤖 Here's your preview: https://hada6-4yaaa-aaaam-abaha-cai.icp0.io |
- Drop the own-subnet restriction: cross-subnet calls are handled by the existing message routing protocol, so no restriction is needed. - Report the subnet's latest certified height rather than the height of the block containing the call, and rename the field to certified_height. - Keep nat for all fields, since consumed_cycles_total cannot be nat64.
Replace certified_height with block_height, defined as the height of the block in whose execution the call is processed on the target subnet.
Co-authored-by: mraszyk <31483726+mraszyk@users.noreply.github.com>
Co-authored-by: mraszyk <31483726+mraszyk@users.noreply.github.com>
mraszyk
left a comment
There was a problem hiding this comment.
LGTM now, I'll approve once this is rolled out to all subnets to prevent accidental merge before that
…reshness Addresses the CI failure and the Copilot review. No production logic changed; this is test code and doc comments only. **Composite-query system test.** `subnet_metrics_composite_query_fails` asserted that the routing rejection's message reaches the caller. It does not: `reject_subnet_message_routing`'s synthesized response is never delivered on the query path, so the universal canister never replies and the outer query fails `CanisterError` / "did not produce a response". This is established platform behaviour of the composite-query arm in `resolve_destination`, not something this change introduced. A control experiment showed `fetch_canister_logs` — which has the identical arm and ships enabled — behaves identically, while `canister_status`, which has no such arm, does deliver its reject (no arm means the request is created and `QueryContext::handle_request`'s reject is delivered normally). The test now asserts the real behaviour and says plainly that this makes it weak: it cannot distinguish the arm from any other failure to reply, and would pass against a stub. The method-specific assertion lives in `resolve_subnet_metrics_rejects_composite_query` in `routing.rs`, which tests `resolve_destination` directly. The division of labour is: the unit test proves the arm, the system test documents user-visible behaviour. The now-inert `.on_reject(...)` is kept deliberately, so that if the platform ever does deliver the reject, the test fails loudly rather than quietly continuing to assert the swallowed behaviour. All five `subnet_metrics` system tests now pass, verified by execution on a Linux host rather than by inspection — including the cross-subnet attribution test, which is the first genuine cross-subnet management-call test in the repo. **Field freshness docs.** Per review, the Rust doc comments described values as "current" when four of the five lag: only `block_height` is current, the other four are as of end-of-previous-round, and `canister_state_bytes` is refreshed only every 10 rounds (so it reads 0 early in a subnet's life). Documented on both `SubnetMetricsResult` and `SubnetMetricsResponse`. The review also asked for the same wording change in the two `ic.did` fixtures. Deliberately not done: those must stay byte-identical to the upstream spec's `public/references/ic.did`. That wording fix belongs in dfinity/developer-docs#333, which already carries an open item on imprecise gauge-vs-counter wording. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ndpoint # Conflicts: # docs/references/ic-interface-spec/changelog.md
Verified the subnet_metrics text field by field against the deployed replica (release-2026-09-03_04-41-base) and corrected three claims: * Only block_height describes the block in whose execution the call is processed. The other four fields are aggregates refreshed at block boundaries, so they describe an earlier block and need not be mutually consistent. * canister_state_bytes is refreshed only every 10 blocks, at heights that are multiples of 10, and reads 0 until the first refresh after the subnet was created. * consumed_cycles_total is not a monotonic counter: it nets out refunds of cycles charged in advance, so it can decrease. It also covers deleted canisters and cycles consumed on behalf of the subnet itself. update_transactions_total is monotonically non-decreasing. The state tree description of the same aggregates in index.md is corrected accordingly, including canister_state_bytes, which was described as accumulating since the subnet was created.
…ndpoint # Conflicts: # docs/references/ic-interface-spec/changelog.md
Documents dfinity/ic#11587, which adds a sixth field to the endpoint this branch specifies. It reports the total instructions the subnet accounted for across the execution phases of all rounds, in units of one million and rounded up, and unlike the four aggregates it has no path under `/subnet/<subnet_id>/metrics` in the certified state tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Merged Conflict resolution: the only conflict was in New field, Claims adjusted, because the field breaks the previous four-plus-one split:
The endpoint was verified against |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The replica adds each deleted canister's remaining balance to the consumed total, not only the cycles it was charged. Say so wherever the field is described, and move the 0.69.0 changelog date to 2026-09-28. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Summary
Specifies the EXPERIMENTAL management canister endpoint
subnet_metrics, which returns subnet-wide metrics to canisters: the subnet block height, the four aggregates the certified state tree already exposes at/subnet/<subnet_id>/metrics, and the total round instructions.The block height is new; the four aggregates exist in the state tree but are unreachable from canister code, since no System API or management canister method exposes state tree paths. That is why they are bundled rather than only the block height and
consumed_cycles_totaloriginally requested.Covers two replica PRs:
million_round_instructions_total.Changes
ic-interface-spec/management-canister.md: new section forsubnet_metricswith the full field semantics.ic-interface-spec/abstract-behavior.md: new transition forsubnet_metrics; the metric values are implementation-specific and attached cycles are refunded.ic-interface-spec/index.md: new definitions of cost schedules (normal and free) and nominal cycles, and corrected descriptions of the four aggregates under/subnet/<subnet_id>/metrics, so the state tree and the method describe the same quantities.ic-interface-spec/changelog.md: version 0.69.0 entry.references/management-canister.md: condensed reference entry linking to the spec.public/references/ic.did: new types and method, with field comments.Interface
Callable by canisters only (not via ingress), and
subnet_idmay name any subnet, not only the caller's.Field semantics
block_heightdescribes the block in whose execution the call is processed. The other five are aggregates refreshed at block boundaries, so they describe an earlier block; they are not refreshed at the same rate, so they need not be mutually consistent.num_canistersandcanister_state_bytesare current values, not counters.canister_state_bytesrefreshes only at heights that are multiples of 10, so it can be up to 10 blocks behindblock_heightand up to 9 blocks behind the other aggregates. It reads 0 before the first refresh.consumed_cycles_totalis measured in nominal cycles: resource charges are recorded at normal cost-schedule rates even on a subnet with a free cost schedule, where no cycles are deducted from canister balances. It sums the historical consumption of current canisters, the subnet's retained accounting for deleted canisters (including their remaining balances at deletion), the cycles charged at the subnet level for HTTPS outcalls, threshold signature requests, and vetKD requests, and the cycles attached to dropped best-effort responses. Refunds of prepaid charges reduce the total. Subnet splitting redistributes canisters' histories, so the total can decrease and can include consumption from before the receiving subnet was created. It is not a count of cycles actually burned on this subnet.update_transactions_totalcounts messages executed in replicated mode. It andmillion_round_instructions_totalare monotonically non-decreasing counters covering the subnet's lifetime, or the period since each metric was introduced for subnets that predate it.million_round_instructions_totalis reported in units of one million and rounded up: a value of42represents 41,000,001 through 42,000,000 instructions. Besides the executed Wasm instructions it covers the fixed per-execution and per-canister overheads charged by the scheduler and the charges for work performed outside of Wasm execution (compilation, chunk assembly, snapshots), so it is not a Wasm instruction meter.block_heightandmillion_round_instructions_totalare the two fields with no path under/subnet/<subnet_id>/metrics, so they are available only through this method and cannot be verified against a certificate.Follow-ups outside this repo
ic-cdkand Motoko management canister bindings.🤖 Generated with Claude Code