Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
de0776d
chore: migrate motoko/query_stats to icp-cli
marc0olo Jun 10, 2026
f56872e
fix: add Nat import for toText() context notation
marc0olo Jun 10, 2026
9d7e3a7
fix: tighten query_stats test 1 grep pattern to match Candid int output
marc0olo Jun 10, 2026
31f0a09
fix: handle underscore separators in Candid int output in grep pattern
marc0olo Jun 10, 2026
9f23f0e
chore: retrigger CI
marc0olo Jun 10, 2026
f984273
fix: merge duplicate tests 2+3; assert num_calls_total > 0
marc0olo Jun 10, 2026
88d700f
chore: remove GitHub link and Overview heading; improve tests and README
marc0olo Jun 10, 2026
6847c01
fix: drop non-zero call count assertion; query stats have a delay on …
marc0olo Jun 10, 2026
fac7fba
chore: replace 'dapp' with 'app'
marc0olo Jun 10, 2026
5cb032b
chore: bump icp-dev-env to 0.3.2
marc0olo Jun 10, 2026
43054b9
revert: back to icp-dev-env 0.3.1 (0.3.2 not yet on ghcr.io)
marc0olo Jun 10, 2026
37af571
chore: bump icp-dev-env to 0.3.2 (images now published)
marc0olo Jun 10, 2026
2345ec0
docs: explain query stats 2-epoch aggregation delay; note 0s are expe…
marc0olo Jun 11, 2026
c27ec81
fix: use --query flag for load() calls so they register in query_stats
marc0olo Jun 11, 2026
d2fae1b
feat: add make test-stats; fix --query flag; clarify aggregation timing
marc0olo Jun 11, 2026
a7abc72
chore: reduce test-stats load calls from 20 to 3 (1 is sufficient)
marc0olo Jun 11, 2026
ca6b854
fix: poll up to 60s for stats instead of fixed sleep
marc0olo Jun 11, 2026
39ca693
fix: make 13 query calls (PocketIC 13-node integer division)
marc0olo Jun 11, 2026
f33c6df
chore: lower timeout from 60s to 30s
marc0olo Jun 11, 2026
1ff15ed
fix: keep making queries to flush stats across epoch boundaries
marc0olo Jun 11, 2026
181d069
docs: fix README to reflect current make test-stats behavior
marc0olo Jun 11, 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
28 changes: 28 additions & 0 deletions .github/workflows/query_stats.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: query_stats

on:
push:
branches: [master]
pull_request:
paths:
- motoko/query_stats/**
- .github/workflows/query_stats.yml

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
motoko-query_stats:
runs-on: ubuntu-24.04
container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.2
env:
ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Deploy and test
working-directory: motoko/query_stats
run: |
icp network start -d
icp deploy
make test
20 changes: 0 additions & 20 deletions motoko/query_stats/.devcontainer/devcontainer.json

This file was deleted.

113 changes: 0 additions & 113 deletions motoko/query_stats/BUILD.md

This file was deleted.

45 changes: 45 additions & 0 deletions motoko/query_stats/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
.PHONY: test test-stats

# Fast structural test — verifies the API shape and that load() works.
# Query stats will show 0 due to the 2-epoch aggregation delay; use
# `make test-stats` to wait for aggregation and verify non-zero values.
test:
@echo "=== Test 1/2: load() returns a non-zero timestamp ==="
@# --query ensures this call is recorded in query_stats (update calls are not tracked)
@result=$$(icp canister call --query backend load '()') && \
echo "$$result" && \
echo "$$result" | grep -qE '\([0-9][0-9_]* : int\)' && \
echo "PASS" || (echo "FAIL" && exit 1)

@echo "=== Test 2/2: get_current_query_stats_as_string() returns the four expected fields ==="
@result=$$(icp canister call backend get_current_query_stats_as_string '()') && \
echo "$$result" && \
echo "$$result" | grep -q 'Number of calls' && \
echo "$$result" | grep -q 'Number of instructions' && \
echo "$$result" | grep -q 'Request payload bytes' && \
echo "$$result" | grep -q 'Response payload bytes' && \
echo "PASS" || (echo "FAIL" && exit 1)

# Full demonstration — continuously makes query calls until stats become non-zero.
#
# Key requirements:
# 1. Only --query calls count (update calls not tracked in query_stats)
# 2. PocketIC uses integer division (num_calls /= 13 nodes) — need 13+ calls per round
# 3. set_epoch_from_height is only called DURING query execution, so queries must
# keep running to flush stats when epoch boundaries are crossed
test-stats:
@echo "=== Polling query stats (making 13 --query calls every 3s, up to 30s) ==="
@secs=0; \
while [ $$secs -lt 30 ]; do \
for i in $$(seq 1 13); do \
icp canister call --query backend load '()' > /dev/null; \
done; \
result=$$(icp canister call backend get_current_query_stats_as_string '()'); \
echo "$$result" | grep -qE 'Number of calls: [1-9]' && \
echo "$$result" && echo "PASS: query stats are non-zero (after ~$${secs}s)" && exit 0; \
sleep 3; \
secs=$$(($$secs + 3)); \
done; \
result=$$(icp canister call backend get_current_query_stats_as_string '()'); \
echo "$$result"; \
echo "FAIL: stats still 0 after 30s" && exit 1
81 changes: 58 additions & 23 deletions motoko/query_stats/README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,58 @@
# Query stats

## Deploying from ICP Ninja

[![](https://icp.ninja/assets/open.svg)](https://icp.ninja/editor?g=https://github.com/dfinity/examples/tree/master/motoko/query_stats)

## Build and deploy from the command-line

### 1. [Download and install the IC SDK.](https://internetcomputer.org/docs/building-apps/getting-started/install)

### 2. Download your project from ICP Ninja using the 'Download files' button on the upper left corner, or [clone the GitHub examples repository.](https://github.com/dfinity/examples/)

### 3. Navigate into the project's directory.

### 4. Deploy the project to your local environment:

```
dfx start --background --clean && dfx deploy
```

## Security considerations and best practices

If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the [security best practices](https://internetcomputer.org/docs/building-apps/security/overview) for developing on ICP. This example may not implement all the best practices.
# Query Stats

This example demonstrates how a canister can read its own query statistics using `ic.canister_status`. It retrieves metrics such as the total number of query calls, instructions executed, and payload bytes.

## How query stats work

Query stats are **aggregated with a 2-epoch delay**, not updated per call:

- Each epoch is **60 blocks** on local PocketIC (vs 600 on mainnet)
- Blocks advance every ~100ms with auto-progress enabled
- Stats for epoch N are only committed once 2/3+ of nodes have submitted records for epoch N+1
- Minimum wait: **2 epochs × 60 blocks × 100ms ≈ 12 seconds**; `make test-stats` polls up to 30 seconds to accommodate slower machines

Only **query calls** are tracked — calls made without `--query` go through consensus as update calls and are not counted in `query_stats.num_calls_total`.

Three things are required for stats to appear locally:

1. **Use `--query`** — `icp canister call` makes update calls by default; only query calls are tracked in `query_stats`
2. **Make 13+ calls per round** — PocketIC simulates a 13-node subnet and uses integer division (`num_calls / 13`); fewer than 13 calls round to zero
3. **Keep making queries continuously** — `set_epoch_from_height` is only invoked during query execution; queries must keep running across epoch boundaries to flush accumulated stats into the payload pipeline

`make test-stats` makes 13 calls every 3 seconds for up to 30 seconds.

## Build and deploy from the command line

### Prerequisites
- Node.js
- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm`

### Install
```bash
git clone https://github.com/dfinity/examples
cd examples/motoko/query_stats
```

### Deploy and test

**Fast test** (verifies API shape; stats show 0 due to aggregation delay):
```bash
icp network start -d
icp deploy
make test
icp network stop
```

**Full demonstration** (generates load, waits ~20s, verifies non-zero stats):
```bash
icp network start -d
icp deploy
make test-stats
icp network stop
```

`make test-stats` calls `load()` 13 times with `--query` every 3 seconds (up to 30 seconds total), verifying non-zero stats once they appear.

## Security considerations and best practices

Refer to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for information on security and best practices for your ICP app.
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
import Nat "mo:core/Nat";
import Time "mo:core/Time";
import Principal "mo:core/Principal";
import Time "mo:core/Time";
import { ic } "mo:ic";

persistent actor QueryStats {

transient let IC = actor "aaaaa-aa" : actor {
canister_status : { canister_id : Principal } -> async {
query_stats : {
num_calls_total : Nat;
num_instructions_total : Nat;
request_payload_bytes_total : Nat;
response_payload_bytes_total : Nat;
};
};
};

public query func load() : async Int {
Time.now();
};

public func get_current_query_stats_as_string() : async Text {
let stats = await IC.canister_status({
let stats = await ic.canister_status({
canister_id = Principal.fromActor(QueryStats);
});
"Number of calls: " # stats.query_stats.num_calls_total.toText() # " - Number of instructions: " # stats.query_stats.num_instructions_total.toText() # " - Request payload bytes: " # stats.query_stats.request_payload_bytes_total.toText() # " - Response payload bytes: " # stats.query_stats.response_payload_bytes_total.toText();
Expand Down
16 changes: 0 additions & 16 deletions motoko/query_stats/dfx.json

This file was deleted.

4 changes: 4 additions & 0 deletions motoko/query_stats/icp.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
canisters:
- name: backend
recipe:
type: "@dfinity/motoko@v5.0.0"
19 changes: 10 additions & 9 deletions motoko/query_stats/mops.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# Motoko dependencies (https://mops.one/)

[toolchain]
moc = "1.5.1"
moc = "1.9.0"

[dependencies]
core = "2.4.0"

core = "2.5.0"
ic = "4.0.0"

[moc]
# M0236: use context dot notation (e.g. x.toText() instead of Nat.toText(x))
# M0237: redundant explicit implicit arguments (e.g. Nat.compare is inferred automatically)
# M0223: redundant type instantiation (e.g. Array.tabulate instead of Array.tabulate<T>)
args = ["-W=M0236,M0237,M0223"]
# M0236: use context dot notation
# M0237: redundant explicit implicit arguments
# M0223: redundant type instantiation
args = ["--default-persistent-actors", "-W=M0236,M0237,M0223"]

[canisters.backend]
main = "backend/app.mo"
Loading