From de0776df9fb17d591c1a6f55d834761ddc8340e3 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 10 Jun 2026 17:15:50 +0200 Subject: [PATCH 01/21] chore: migrate motoko/query_stats to icp-cli Replace dfx.json with icp.yaml, update mops.toml to current toolchain versions (moc 1.9.0, core 2.5.0, ic 4.0.0), replace inline management canister actor type with `import { ic } "mo:ic"`, move source to backend/app.mo, add Makefile with numbered tests, CI workflow, and updated README. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/query_stats.yml | 28 +++++ .../.devcontainer/devcontainer.json | 20 ---- motoko/query_stats/BUILD.md | 113 ------------------ motoko/query_stats/Makefile | 22 ++++ motoko/query_stats/README.md | 54 +++++---- .../main.mo => backend/app.mo} | 15 +-- motoko/query_stats/dfx.json | 16 --- motoko/query_stats/icp.yaml | 4 + motoko/query_stats/mops.toml | 19 +-- 9 files changed, 97 insertions(+), 194 deletions(-) create mode 100644 .github/workflows/query_stats.yml delete mode 100644 motoko/query_stats/.devcontainer/devcontainer.json delete mode 100644 motoko/query_stats/BUILD.md create mode 100644 motoko/query_stats/Makefile rename motoko/query_stats/{src/query_stats_backend/main.mo => backend/app.mo} (62%) delete mode 100644 motoko/query_stats/dfx.json create mode 100644 motoko/query_stats/icp.yaml diff --git a/.github/workflows/query_stats.yml b/.github/workflows/query_stats.yml new file mode 100644 index 000000000..f3b913469 --- /dev/null +++ b/.github/workflows/query_stats.yml @@ -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.1 + 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 diff --git a/motoko/query_stats/.devcontainer/devcontainer.json b/motoko/query_stats/.devcontainer/devcontainer.json deleted file mode 100644 index ebb0b8bcc..000000000 --- a/motoko/query_stats/.devcontainer/devcontainer.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "ICP Dev Environment", - "image": "ghcr.io/dfinity/icp-dev-env-slim:22", - "forwardPorts": [4943, 5173], - "portsAttributes": { - "4943": { - "label": "dfx", - "onAutoForward": "ignore" - }, - "5173": { - "label": "vite", - "onAutoForward": "openBrowser" - } - }, - "customizations": { - "vscode": { - "extensions": ["dfinity-foundation.vscode-motoko"] - } - } -} diff --git a/motoko/query_stats/BUILD.md b/motoko/query_stats/BUILD.md deleted file mode 100644 index 24cfcb754..000000000 --- a/motoko/query_stats/BUILD.md +++ /dev/null @@ -1,113 +0,0 @@ -# Continue building locally - -Projects deployed through ICP Ninja are temporary; they will only be live for 20 minutes before they are removed. The command-line tool `dfx` can be used to continue building your ICP Ninja project locally and deploy it to the mainnet. - -To migrate your ICP Ninja project off of the web browser and develop it locally, follow these steps. - -### 1. Install developer tools. - -You can install the developer tools natively or use Dev Containers. - -#### Option 1: Natively install developer tools - -> Installing `dfx` natively is currently only supported on macOS and Linux systems. On Windows, it is recommended to use the Dev Containers option. - -1. Install `dfx` with the following command: - -``` - -sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)" - -``` - -> On Apple Silicon (e.g., Apple M1 chip), make sure you have Rosetta installed (`softwareupdate --install-rosetta`). - -2. [Install NodeJS](https://nodejs.org/en/download/package-manager). - -3. For Rust projects, you will also need to: - -- Install [Rust](https://doc.rust-lang.org/cargo/getting-started/installation.html#install-rust-and-cargo): `curl https://sh.rustup.rs -sSf | sh` - -- Install [candid-extractor](https://crates.io/crates/candid-extractor): `cargo install candid-extractor` - -4. For Motoko projects, you will also need to: - -- Install the Motoko package manager [Mops](https://docs.mops.one/quick-start#2-install-mops-cli): `npm i -g ic-mops` - -Lastly, navigate into your project's directory that you downloaded from ICP Ninja. - -#### Option 2: Dev Containers - -Continue building your projects locally by installing the [Dev Container extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) for VS Code and [Docker](https://docs.docker.com/engine/install/). - -Make sure Docker is running, then navigate into your project's directory that you downloaded from ICP Ninja and start the Dev Container by selecting `Dev-Containers: Reopen in Container` in VS Code's command palette (F1 or Ctrl/Cmd+Shift+P). - -> Note that local development ports (e.g. the ports used by `dfx` or `vite`) are forwarded from the Dev Container to your local machine. In the VS code terminal, use Ctrl/Cmd+Click on the displayed local URLs to open them in your browser. To view the current port mappings, click the "Ports" tab in the VS Code terminal window. - -### 2. Start the local development environment. - -``` -dfx start --background -``` - -### 3. Create a local developer identity. - -To manage your project's canisters, it is recommended that you create a local [developer identity](https://internetcomputer.org/docs/building-apps/getting-started/identities) rather than use the `dfx` default identity that is not stored securely. - -To create a new identity, run the commands: - -``` - -dfx identity new IDENTITY_NAME - -dfx identity use IDENTITY_NAME - -``` - -Replace `IDENTITY_NAME` with your preferred identity name. The first command `dfx start --background` starts the local `dfx` processes, then `dfx identity new` will create a new identity and return your identity's seed phase. Be sure to save this in a safe, secure location. - -The third command `dfx identity use` will tell `dfx` to use your new identity as the active identity. Any canister smart contracts created after running `dfx identity use` will be owned and controlled by the active identity. - -Your identity will have a principal ID associated with it. Principal IDs are used to identify different entities on ICP, such as users and canisters. - -[Learn more about ICP developer identities](https://internetcomputer.org/docs/building-apps/getting-started/identities). - -### 4. Deploy the project locally. - -Deploy your project to your local developer environment with: - -``` -npm install -dfx deploy - -``` - -Your project will be hosted on your local machine. The local canister URLs for your project will be shown in the terminal window as output of the `dfx deploy` command. You can open these URLs in your web browser to view the local instance of your project. - -### 5. Obtain cycles. - -To deploy your project to the mainnet for long-term public accessibility, first you will need [cycles](https://internetcomputer.org/docs/building-apps/getting-started/tokens-and-cycles). Cycles are used to pay for the resources your project uses on the mainnet, such as storage and compute. - -> This cost model is known as ICP's [reverse gas model](https://internetcomputer.org/docs/building-apps/essentials/gas-cost), where developers pay for their project's gas fees rather than users pay for their own gas fees. This model provides an enhanced end user experience since they do not need to hold tokens or sign transactions when using a dapp deployed on ICP. - -> Learn how much a project may cost by using the [pricing calculator](https://internetcomputer.org/docs/building-apps/essentials/cost-estimations-and-examples). - -Cycles can be obtained through [converting ICP tokens into cycles using `dfx`](https://internetcomputer.org/docs/building-apps/developer-tools/dfx/dfx-cycles#dfx-cycles-convert). - -### 6. Deploy to the mainnet. - -Once you have cycles, run the command: - -``` - -dfx deploy --network ic - -``` - -After your project has been deployed to the mainnet, it will continuously require cycles to pay for the resources it uses. You will need to [top up](https://internetcomputer.org/docs/building-apps/canister-management/topping-up) your project's canisters or set up automatic cycles management through a service such as [CycleOps](https://cycleops.dev/). - -> If your project's canisters run out of cycles, they will be removed from the network. - -## Additional examples - -Additional code examples and sample applications can be found in the [DFINITY examples repo](https://github.com/dfinity/examples). diff --git a/motoko/query_stats/Makefile b/motoko/query_stats/Makefile new file mode 100644 index 000000000..d6e1af135 --- /dev/null +++ b/motoko/query_stats/Makefile @@ -0,0 +1,22 @@ +.PHONY: test + +test: + @echo "=== Test 1: load() returns a non-zero timestamp ===" + @result=$$(icp canister call backend load '()') && \ + echo "$$result" && \ + echo "$$result" | grep -q '[0-9]' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 2: get_current_query_stats_as_string() returns stats data ===" + @result=$$(icp canister call backend get_current_query_stats_as_string '()') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'Number of calls' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 3: query stats include all expected fields ===" + @result=$$(icp canister call backend get_current_query_stats_as_string '()') && \ + echo "$$result" && \ + 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) diff --git a/motoko/query_stats/README.md b/motoko/query_stats/README.md index be49523e2..c972ea715 100644 --- a/motoko/query_stats/README.md +++ b/motoko/query_stats/README.md @@ -1,23 +1,31 @@ -# 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 + +[View this sample's code on GitHub](https://github.com/dfinity/examples/tree/master/motoko/query_stats) + +## Overview + +This example demonstrates how a canister can read its own query statistics from the management canister. It uses `ic.canister_status` to retrieve metrics such as the total number of calls, instructions executed, and payload bytes for the canister's query methods. + +## 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 +```bash +icp network start -d +icp deploy +make test +icp network stop +``` + +## 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 dapp. diff --git a/motoko/query_stats/src/query_stats_backend/main.mo b/motoko/query_stats/backend/app.mo similarity index 62% rename from motoko/query_stats/src/query_stats_backend/main.mo rename to motoko/query_stats/backend/app.mo index 33fd7fb4a..f9143be67 100644 --- a/motoko/query_stats/src/query_stats_backend/main.mo +++ b/motoko/query_stats/backend/app.mo @@ -1,26 +1,15 @@ -import Nat "mo:core/Nat"; import Time "mo:core/Time"; import Principal "mo:core/Principal"; +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(); diff --git a/motoko/query_stats/dfx.json b/motoko/query_stats/dfx.json deleted file mode 100644 index 3fff3f5cf..000000000 --- a/motoko/query_stats/dfx.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "canisters": { - "query_stats": { - "main": "src/query_stats_backend/main.mo", - "type": "motoko" - } - }, - "defaults": { - "build": { - "args": "", - "packtool": "mops sources" - } - }, - "output_env_file": ".env", - "version": 1 -} \ No newline at end of file diff --git a/motoko/query_stats/icp.yaml b/motoko/query_stats/icp.yaml new file mode 100644 index 000000000..fb741fade --- /dev/null +++ b/motoko/query_stats/icp.yaml @@ -0,0 +1,4 @@ +canisters: + - name: backend + recipe: + type: "@dfinity/motoko@v5.0.0" diff --git a/motoko/query_stats/mops.toml b/motoko/query_stats/mops.toml index 402697fa4..7902c42eb 100644 --- a/motoko/query_stats/mops.toml +++ b/motoko/query_stats/mops.toml @@ -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) -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" From f56872eb7539d54642cf16a851454923018d531e Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 10 Jun 2026 18:32:08 +0200 Subject: [PATCH 02/21] fix: add Nat import for toText() context notation Co-Authored-By: Claude Sonnet 4.6 --- motoko/query_stats/backend/app.mo | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/motoko/query_stats/backend/app.mo b/motoko/query_stats/backend/app.mo index f9143be67..ef0326cc1 100644 --- a/motoko/query_stats/backend/app.mo +++ b/motoko/query_stats/backend/app.mo @@ -1,5 +1,6 @@ -import Time "mo:core/Time"; +import Nat "mo:core/Nat"; import Principal "mo:core/Principal"; +import Time "mo:core/Time"; import { ic } "mo:ic"; persistent actor QueryStats { From 9d7e3a73dc068e3fc61e1c1aec715b1d796ac554 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 10 Jun 2026 18:44:01 +0200 Subject: [PATCH 03/21] fix: tighten query_stats test 1 grep pattern to match Candid int output Co-Authored-By: Claude Sonnet 4.6 --- motoko/query_stats/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/motoko/query_stats/Makefile b/motoko/query_stats/Makefile index d6e1af135..1bc6d5d92 100644 --- a/motoko/query_stats/Makefile +++ b/motoko/query_stats/Makefile @@ -4,7 +4,7 @@ test: @echo "=== Test 1: load() returns a non-zero timestamp ===" @result=$$(icp canister call backend load '()') && \ echo "$$result" && \ - echo "$$result" | grep -q '[0-9]' && \ + echo "$$result" | grep -qE '\([0-9]+ : int\)' && \ echo "PASS" || (echo "FAIL" && exit 1) @echo "=== Test 2: get_current_query_stats_as_string() returns stats data ===" From 31f0a092aca2bfbd8a7a93187378e04b7e859b7f Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 10 Jun 2026 19:00:04 +0200 Subject: [PATCH 04/21] fix: handle underscore separators in Candid int output in grep pattern Co-Authored-By: Claude Sonnet 4.6 --- motoko/query_stats/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/motoko/query_stats/Makefile b/motoko/query_stats/Makefile index 1bc6d5d92..7abcb673b 100644 --- a/motoko/query_stats/Makefile +++ b/motoko/query_stats/Makefile @@ -4,7 +4,7 @@ test: @echo "=== Test 1: load() returns a non-zero timestamp ===" @result=$$(icp canister call backend load '()') && \ echo "$$result" && \ - echo "$$result" | grep -qE '\([0-9]+ : int\)' && \ + echo "$$result" | grep -qE '\([0-9][0-9_]* : int\)' && \ echo "PASS" || (echo "FAIL" && exit 1) @echo "=== Test 2: get_current_query_stats_as_string() returns stats data ===" From 9f23f0e507a7c346e1e9222eccdc0ad82c0d4953 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 10 Jun 2026 21:36:28 +0200 Subject: [PATCH 05/21] chore: retrigger CI Co-Authored-By: Claude Sonnet 4.6 From f9842737222903c33d5667274c33ba0f70ea1693 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 10 Jun 2026 21:43:15 +0200 Subject: [PATCH 06/21] fix: merge duplicate tests 2+3; assert num_calls_total > 0 Co-Authored-By: Claude Sonnet 4.6 --- motoko/query_stats/Makefile | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/motoko/query_stats/Makefile b/motoko/query_stats/Makefile index 7abcb673b..8c9223c6f 100644 --- a/motoko/query_stats/Makefile +++ b/motoko/query_stats/Makefile @@ -1,22 +1,18 @@ .PHONY: test test: - @echo "=== Test 1: load() returns a non-zero timestamp ===" + @echo "=== Test 1/2: load() returns a non-zero timestamp ===" @result=$$(icp canister call backend load '()') && \ echo "$$result" && \ echo "$$result" | grep -qE '\([0-9][0-9_]* : int\)' && \ echo "PASS" || (echo "FAIL" && exit 1) - @echo "=== Test 2: get_current_query_stats_as_string() returns stats data ===" + @echo "=== Test 2/2: get_current_query_stats_as_string() reports all fields with non-zero call count ===" @result=$$(icp canister call backend get_current_query_stats_as_string '()') && \ echo "$$result" && \ echo "$$result" | grep -q 'Number of calls' && \ - echo "PASS" || (echo "FAIL" && exit 1) - - @echo "=== Test 3: query stats include all expected fields ===" - @result=$$(icp canister call backend get_current_query_stats_as_string '()') && \ - echo "$$result" && \ echo "$$result" | grep -q 'Number of instructions' && \ echo "$$result" | grep -q 'Request payload bytes' && \ echo "$$result" | grep -q 'Response payload bytes' && \ + echo "$$result" | grep -qE 'Number of calls: [1-9]' && \ echo "PASS" || (echo "FAIL" && exit 1) From 88d700f55ec42f99567d7073b5fcef01a5c0321b Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 10 Jun 2026 21:51:47 +0200 Subject: [PATCH 07/21] chore: remove GitHub link and Overview heading; improve tests and README Co-Authored-By: Claude Sonnet 4.6 --- motoko/query_stats/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/motoko/query_stats/README.md b/motoko/query_stats/README.md index c972ea715..091e94266 100644 --- a/motoko/query_stats/README.md +++ b/motoko/query_stats/README.md @@ -1,9 +1,5 @@ # Query Stats -[View this sample's code on GitHub](https://github.com/dfinity/examples/tree/master/motoko/query_stats) - -## Overview - This example demonstrates how a canister can read its own query statistics from the management canister. It uses `ic.canister_status` to retrieve metrics such as the total number of calls, instructions executed, and payload bytes for the canister's query methods. ## Build and deploy from the command line From 6847c016d0b69a880d639f85c67615b7cd27f598 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 10 Jun 2026 21:52:54 +0200 Subject: [PATCH 08/21] fix: drop non-zero call count assertion; query stats have a delay on local replica Co-Authored-By: Claude Sonnet 4.6 --- motoko/query_stats/Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/motoko/query_stats/Makefile b/motoko/query_stats/Makefile index 8c9223c6f..e804fc82d 100644 --- a/motoko/query_stats/Makefile +++ b/motoko/query_stats/Makefile @@ -7,12 +7,11 @@ test: 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() reports all fields with non-zero call count ===" + @echo "=== Test 2/2: get_current_query_stats_as_string() reports all four stat 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 "$$result" | grep -qE 'Number of calls: [1-9]' && \ echo "PASS" || (echo "FAIL" && exit 1) From fac7fba1e879b1444e9aa545bcf64d60e3686c45 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 10 Jun 2026 21:55:25 +0200 Subject: [PATCH 09/21] chore: replace 'dapp' with 'app' Co-Authored-By: Claude Sonnet 4.6 --- motoko/query_stats/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/motoko/query_stats/README.md b/motoko/query_stats/README.md index 091e94266..b9fc17aaf 100644 --- a/motoko/query_stats/README.md +++ b/motoko/query_stats/README.md @@ -24,4 +24,4 @@ icp network stop ## 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 dapp. +Refer to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for information on security and best practices for your ICP app. From 5cb032b19d64a983d20e31648e6280c3adf73eb9 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 00:45:33 +0200 Subject: [PATCH 10/21] chore: bump icp-dev-env to 0.3.2 Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/query_stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/query_stats.yml b/.github/workflows/query_stats.yml index f3b913469..7a762983a 100644 --- a/.github/workflows/query_stats.yml +++ b/.github/workflows/query_stats.yml @@ -15,7 +15,7 @@ concurrency: jobs: motoko-query_stats: runs-on: ubuntu-24.04 - container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.1 + container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.2 env: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: From 43054b903bb6c7fce563acf92f047910daea8ba4 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 00:50:48 +0200 Subject: [PATCH 11/21] revert: back to icp-dev-env 0.3.1 (0.3.2 not yet on ghcr.io) Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/query_stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/query_stats.yml b/.github/workflows/query_stats.yml index 7a762983a..f3b913469 100644 --- a/.github/workflows/query_stats.yml +++ b/.github/workflows/query_stats.yml @@ -15,7 +15,7 @@ concurrency: jobs: motoko-query_stats: runs-on: ubuntu-24.04 - container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.2 + container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.1 env: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: From 37af57153fba0443f8b76af3f0e58b8b7e498548 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 01:21:16 +0200 Subject: [PATCH 12/21] chore: bump icp-dev-env to 0.3.2 (images now published) Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/query_stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/query_stats.yml b/.github/workflows/query_stats.yml index f3b913469..7a762983a 100644 --- a/.github/workflows/query_stats.yml +++ b/.github/workflows/query_stats.yml @@ -15,7 +15,7 @@ concurrency: jobs: motoko-query_stats: runs-on: ubuntu-24.04 - container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.1 + container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.2 env: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: From 2345ec0009bb956beac77717b61d406ab37c0c5c Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 18:42:43 +0200 Subject: [PATCH 13/21] docs: explain query stats 2-epoch aggregation delay; note 0s are expected locally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Query stats require 120+ blocks to aggregate on local PocketIC (2 epochs × 60 blocks). Values are always 0 locally — this is correct IC behavior, not a bug. Non-zero values appear on IC mainnet after enough queries accumulate. Co-Authored-By: Claude Sonnet 4.6 --- motoko/query_stats/Makefile | 5 ++++- motoko/query_stats/README.md | 12 +++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/motoko/query_stats/Makefile b/motoko/query_stats/Makefile index e804fc82d..cbdf04c9f 100644 --- a/motoko/query_stats/Makefile +++ b/motoko/query_stats/Makefile @@ -7,7 +7,10 @@ test: 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() reports all four stat fields ===" + @echo "=== Test 2/2: get_current_query_stats_as_string() returns the four expected fields ===" + @# NOTE: query stats are aggregated with a ~2-epoch delay (120+ blocks on local replica, + @# 1200+ blocks on mainnet). Values will be 0 locally even after multiple query calls — + @# this is expected. On IC mainnet stats accumulate after enough queries are made. @result=$$(icp canister call backend get_current_query_stats_as_string '()') && \ echo "$$result" && \ echo "$$result" | grep -q 'Number of calls' && \ diff --git a/motoko/query_stats/README.md b/motoko/query_stats/README.md index b9fc17aaf..b55ea0bf5 100644 --- a/motoko/query_stats/README.md +++ b/motoko/query_stats/README.md @@ -1,6 +1,16 @@ # Query Stats -This example demonstrates how a canister can read its own query statistics from the management canister. It uses `ic.canister_status` to retrieve metrics such as the total number of calls, instructions executed, and payload bytes for the canister's query methods. +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 **not updated in real-time**. The IC aggregates them with a minimum 2-epoch delay: + +- Each epoch is **60 blocks** on local PocketIC (vs 600 on mainnet) +- Stats for epoch N are only committed once 2/3+ of nodes have submitted records for epoch N+1 +- This means **at least 120 blocks** must pass after a query call before its contribution appears in `canister_status().query_stats` + +**On a local replica**, stats will show `0` for all fields even after making many query calls — this is expected behavior, not a bug. Non-zero values are observable on IC mainnet after enough queries accumulate across multiple epochs. ## Build and deploy from the command line From c27ec813448445bd4c1f84a3f8e6e42d5e5b4c46 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 18:47:21 +0200 Subject: [PATCH 14/21] fix: use --query flag for load() calls so they register in query_stats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit icp canister call makes UPDATE calls by default. UPDATE calls to query functions are NOT counted in query_stats.num_calls_total — only actual QUERY calls are tracked. Adding --query ensures the calls are recorded. The 2-epoch aggregation delay (120 blocks × 100ms ≈ 12s) still means the first run shows 0; the second run shows non-zero values from run 1. Co-Authored-By: Claude Sonnet 4.6 --- motoko/query_stats/Makefile | 8 ++++---- motoko/query_stats/README.md | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/motoko/query_stats/Makefile b/motoko/query_stats/Makefile index cbdf04c9f..ca4fc7d16 100644 --- a/motoko/query_stats/Makefile +++ b/motoko/query_stats/Makefile @@ -2,15 +2,15 @@ test: @echo "=== Test 1/2: load() returns a non-zero timestamp ===" - @result=$$(icp canister call backend load '()') && \ + @# Uses --query so this call is counted 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 ===" - @# NOTE: query stats are aggregated with a ~2-epoch delay (120+ blocks on local replica, - @# 1200+ blocks on mainnet). Values will be 0 locally even after multiple query calls — - @# this is expected. On IC mainnet stats accumulate after enough queries are made. + @# NOTE: query stats are aggregated with a 2-epoch delay (120+ blocks on local replica). + @# This run's calls won't appear yet — run make test a second time to see non-zero values. @result=$$(icp canister call backend get_current_query_stats_as_string '()') && \ echo "$$result" && \ echo "$$result" | grep -q 'Number of calls' && \ diff --git a/motoko/query_stats/README.md b/motoko/query_stats/README.md index b55ea0bf5..f5f50adab 100644 --- a/motoko/query_stats/README.md +++ b/motoko/query_stats/README.md @@ -4,13 +4,13 @@ This example demonstrates how a canister can read its own query statistics using ## How query stats work -Query stats are **not updated in real-time**. The IC aggregates them with a minimum 2-epoch delay: +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) - Stats for epoch N are only committed once 2/3+ of nodes have submitted records for epoch N+1 -- This means **at least 120 blocks** must pass after a query call before its contribution appears in `canister_status().query_stats` +- At least **2 epochs** (120+ blocks) must pass after a query call before it appears in `canister_status().query_stats` -**On a local replica**, stats will show `0` for all fields even after making many query calls — this is expected behavior, not a bug. Non-zero values are observable on IC mainnet after enough queries accumulate across multiple epochs. +**On a local replica**, the first `make test` run will show `0` for all fields because the stats from that run haven't aggregated yet. Run `make test` a second time (with the same network still running) and you'll see non-zero values from the first run. ## Build and deploy from the command line @@ -28,7 +28,8 @@ cd examples/motoko/query_stats ```bash icp network start -d icp deploy -make test +make test # first run: stats show 0 (aggregation lag) +make test # second run: stats show non-zero values from the first run icp network stop ``` From d2fae1bd0ad94d3db46ca7da856c31128b345db9 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 18:53:25 +0200 Subject: [PATCH 15/21] feat: add make test-stats; fix --query flag; clarify aggregation timing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - make test: fast structural check (CI), stats show 0 due to lag - make test-stats: generates load, waits ~20s for 2-epoch aggregation, verifies non-zero values — the full end-to-end demonstration - Use --query flag on load() calls (update calls not counted in query_stats) - README: document both targets, explain timing (2 epochs × 60 blocks × 100ms) Co-Authored-By: Claude Sonnet 4.6 --- motoko/query_stats/Makefile | 29 +++++++++++++++++++++++++---- motoko/query_stats/README.md | 22 +++++++++++++++++----- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/motoko/query_stats/Makefile b/motoko/query_stats/Makefile index ca4fc7d16..0021fac20 100644 --- a/motoko/query_stats/Makefile +++ b/motoko/query_stats/Makefile @@ -1,16 +1,17 @@ -.PHONY: test +.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 ===" - @# Uses --query so this call is counted in query_stats (update calls are not tracked) + @# --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 ===" - @# NOTE: query stats are aggregated with a 2-epoch delay (120+ blocks on local replica). - @# This run's calls won't appear yet — run make test a second time to see non-zero values. @result=$$(icp canister call backend get_current_query_stats_as_string '()') && \ echo "$$result" && \ echo "$$result" | grep -q 'Number of calls' && \ @@ -18,3 +19,23 @@ test: echo "$$result" | grep -q 'Request payload bytes' && \ echo "$$result" | grep -q 'Response payload bytes' && \ echo "PASS" || (echo "FAIL" && exit 1) + +# Full demonstration — generates query load, waits for the 2-epoch aggregation +# window (~15s), then verifies non-zero stats. +# +# Query stats require 120+ blocks to aggregate (2 epochs × 60 blocks × 100ms = 12s minimum). +# Only --query calls count; update calls are not tracked in query_stats. +test-stats: + @echo "=== Generating query load (20 calls) ===" + @for i in $$(seq 1 20); do \ + icp canister call --query backend load '()' > /dev/null; \ + done + @echo "=== Waiting ~20s for 2-epoch aggregation window ===" + @sleep 20 + + @echo "=== Verifying non-zero query stats ===" + @result=$$(icp canister call backend get_current_query_stats_as_string '()') && \ + echo "$$result" && \ + echo "$$result" | grep -qE 'Number of calls: [1-9]' && \ + echo "PASS: query stats are non-zero after aggregation" || \ + (echo "FAIL: stats still 0 — try again in a few seconds" && exit 1) diff --git a/motoko/query_stats/README.md b/motoko/query_stats/README.md index f5f50adab..d114677f0 100644 --- a/motoko/query_stats/README.md +++ b/motoko/query_stats/README.md @@ -4,13 +4,14 @@ This example demonstrates how a canister can read its own query statistics using ## How query stats work -Query stats are **aggregated with a 2-epoch delay**, not updated per-call: +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 -- At least **2 epochs** (120+ blocks) must pass after a query call before it appears in `canister_status().query_stats` +- Minimum wait: **2 epochs × 60 blocks × 100ms ≈ 15–20 seconds** after the query calls -**On a local replica**, the first `make test` run will show `0` for all fields because the stats from that run haven't aggregated yet. Run `make test` a second time (with the same network still running) and you'll see non-zero values from the first run. +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`. ## Build and deploy from the command line @@ -25,14 +26,25 @@ 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 # first run: stats show 0 (aggregation lag) -make test # second run: stats show non-zero values from the first run +make test-stats icp network stop ``` +`make test-stats` calls `load()` 20 times with `--query`, waits 20 seconds for the aggregation window, then verifies that `Number of calls` is non-zero. + ## 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. From a7abc72954ef74a2b809e09ba6100e5913b74328 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 18:53:58 +0200 Subject: [PATCH 16/21] chore: reduce test-stats load calls from 20 to 3 (1 is sufficient) Co-Authored-By: Claude Sonnet 4.6 --- motoko/query_stats/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/motoko/query_stats/Makefile b/motoko/query_stats/Makefile index 0021fac20..19fb1fddd 100644 --- a/motoko/query_stats/Makefile +++ b/motoko/query_stats/Makefile @@ -26,8 +26,8 @@ test: # Query stats require 120+ blocks to aggregate (2 epochs × 60 blocks × 100ms = 12s minimum). # Only --query calls count; update calls are not tracked in query_stats. test-stats: - @echo "=== Generating query load (20 calls) ===" - @for i in $$(seq 1 20); do \ + @echo "=== Generating query load (3 calls) ===" + @for i in $$(seq 1 3); do \ icp canister call --query backend load '()' > /dev/null; \ done @echo "=== Waiting ~20s for 2-epoch aggregation window ===" From ca6b85448dac3b82fc89fef91cd25971255311fb Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 19:03:20 +0200 Subject: [PATCH 17/21] fix: poll up to 60s for stats instead of fixed sleep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed sleep of 20s fails on loaded machines where each PocketIC block takes 150-200ms (120 blocks × 200ms = 24s exceeds the sleep). Poll every 3s up to 60s and report how long aggregation actually took. Co-Authored-By: Claude Sonnet 4.6 --- motoko/query_stats/Makefile | 19 +++++++++++-------- motoko/query_stats/README.md | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/motoko/query_stats/Makefile b/motoko/query_stats/Makefile index 19fb1fddd..1149ced26 100644 --- a/motoko/query_stats/Makefile +++ b/motoko/query_stats/Makefile @@ -30,12 +30,15 @@ test-stats: @for i in $$(seq 1 3); do \ icp canister call --query backend load '()' > /dev/null; \ done - @echo "=== Waiting ~20s for 2-epoch aggregation window ===" - @sleep 20 - - @echo "=== Verifying non-zero query stats ===" - @result=$$(icp canister call backend get_current_query_stats_as_string '()') && \ - echo "$$result" && \ + @echo "=== Waiting for 2-epoch aggregation (up to 60s) ===" + @# Each epoch = 60 blocks; auto-progress advances blocks every 100ms minimum. + @# On slower machines each block may take 150-200ms: 120 blocks × 200ms = 24s worst case. + @secs=0; \ + while [ $$secs -lt 60 ]; do \ + result=$$(icp canister call backend get_current_query_stats_as_string '()'); \ echo "$$result" | grep -qE 'Number of calls: [1-9]' && \ - echo "PASS: query stats are non-zero after aggregation" || \ - (echo "FAIL: stats still 0 — try again in a few seconds" && exit 1) + echo "$$result" && echo "PASS: query stats are non-zero (after ~$${secs}s)" && exit 0; \ + sleep 3; \ + secs=$$(($$secs + 3)); \ + done; \ + echo "FAIL: stats still 0 after 60s" && exit 1 diff --git a/motoko/query_stats/README.md b/motoko/query_stats/README.md index d114677f0..9feacd0aa 100644 --- a/motoko/query_stats/README.md +++ b/motoko/query_stats/README.md @@ -9,7 +9,7 @@ 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 ≈ 15–20 seconds** after the query calls +- Minimum wait: **2 epochs × 60 blocks × 100ms ≈ 12 seconds**; on slower machines up to **60 seconds** (each block can take 150–200ms under load) 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`. From 39ca6937d85fb3ab923dde4826fc7cbd8a096bf6 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 19:07:38 +0200 Subject: [PATCH 18/21] fix: make 13 query calls (PocketIC 13-node integer division) PocketIC simulates a 13-node subnet. It distributes stats with integer division (num_calls /= 13) before aggregation. With fewer than 13 calls, every node reports 0 and stats are always 0 regardless of wait time. Also: print final result on failure for easier debugging. Co-Authored-By: Claude Sonnet 4.6 --- motoko/query_stats/Makefile | 8 ++++++-- motoko/query_stats/README.md | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/motoko/query_stats/Makefile b/motoko/query_stats/Makefile index 1149ced26..954eb1789 100644 --- a/motoko/query_stats/Makefile +++ b/motoko/query_stats/Makefile @@ -26,8 +26,10 @@ test: # Query stats require 120+ blocks to aggregate (2 epochs × 60 blocks × 100ms = 12s minimum). # Only --query calls count; update calls are not tracked in query_stats. test-stats: - @echo "=== Generating query load (3 calls) ===" - @for i in $$(seq 1 3); do \ + @echo "=== Generating query load (13 calls) ===" + @# PocketIC simulates a 13-node subnet and uses integer division when spreading + @# stats across nodes (num_calls /= 13). Fewer than 13 calls rounds to 0. + @for i in $$(seq 1 13); do \ icp canister call --query backend load '()' > /dev/null; \ done @echo "=== Waiting for 2-epoch aggregation (up to 60s) ===" @@ -41,4 +43,6 @@ test-stats: 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 60s" && exit 1 diff --git a/motoko/query_stats/README.md b/motoko/query_stats/README.md index 9feacd0aa..677111a66 100644 --- a/motoko/query_stats/README.md +++ b/motoko/query_stats/README.md @@ -13,6 +13,8 @@ Query stats are **aggregated with a 2-epoch delay**, not updated per call: 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`. +PocketIC simulates a 13-node subnet for consensus. When distributing stats across nodes it uses integer division (`num_calls / 13`), so **fewer than 13 query calls round to zero** — `make test-stats` makes exactly 13 calls to ensure non-zero values survive the division. + ## Build and deploy from the command line ### Prerequisites From f33c6df1e57908b36925bb17111d7bd46711bfed Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 19:08:14 +0200 Subject: [PATCH 19/21] chore: lower timeout from 60s to 30s Co-Authored-By: Claude Sonnet 4.6 --- motoko/query_stats/Makefile | 10 +++++----- motoko/query_stats/README.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/motoko/query_stats/Makefile b/motoko/query_stats/Makefile index 954eb1789..790ce8954 100644 --- a/motoko/query_stats/Makefile +++ b/motoko/query_stats/Makefile @@ -32,11 +32,11 @@ test-stats: @for i in $$(seq 1 13); do \ icp canister call --query backend load '()' > /dev/null; \ done - @echo "=== Waiting for 2-epoch aggregation (up to 60s) ===" - @# Each epoch = 60 blocks; auto-progress advances blocks every 100ms minimum. - @# On slower machines each block may take 150-200ms: 120 blocks × 200ms = 24s worst case. + @echo "=== Waiting for 2-epoch aggregation (up to 30s) ===" + @# Each epoch = 60 blocks; auto-progress advances ~100ms/block. + @# 120 blocks minimum; 30s gives margin for slower machines. @secs=0; \ - while [ $$secs -lt 60 ]; do \ + while [ $$secs -lt 30 ]; do \ 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; \ @@ -45,4 +45,4 @@ test-stats: done; \ result=$$(icp canister call backend get_current_query_stats_as_string '()'); \ echo "$$result"; \ - echo "FAIL: stats still 0 after 60s" && exit 1 + echo "FAIL: stats still 0 after 30s" && exit 1 diff --git a/motoko/query_stats/README.md b/motoko/query_stats/README.md index 677111a66..36aa62828 100644 --- a/motoko/query_stats/README.md +++ b/motoko/query_stats/README.md @@ -9,7 +9,7 @@ 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**; on slower machines up to **60 seconds** (each block can take 150–200ms under load) +- 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`. From 1ff15ed68e07709c9fd6ee45e0a493192768affe Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 19:15:31 +0200 Subject: [PATCH 20/21] fix: keep making queries to flush stats across epoch boundaries set_epoch_from_height is only called during query execution (query_context.rs). Sleeping without making queries means epoch transitions never trigger a stats flush to the channel. Fix: poll every 3s with 13 --query calls per round so stats are flushed when an epoch boundary is crossed during the loop. Co-Authored-By: Claude Sonnet 4.6 --- motoko/query_stats/Makefile | 23 ++++++++++------------- motoko/query_stats/README.md | 8 +++++++- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/motoko/query_stats/Makefile b/motoko/query_stats/Makefile index 790ce8954..732cad3d9 100644 --- a/motoko/query_stats/Makefile +++ b/motoko/query_stats/Makefile @@ -20,23 +20,20 @@ test: echo "$$result" | grep -q 'Response payload bytes' && \ echo "PASS" || (echo "FAIL" && exit 1) -# Full demonstration — generates query load, waits for the 2-epoch aggregation -# window (~15s), then verifies non-zero stats. +# Full demonstration — continuously makes query calls until stats become non-zero. # -# Query stats require 120+ blocks to aggregate (2 epochs × 60 blocks × 100ms = 12s minimum). -# Only --query calls count; update calls are not tracked in query_stats. +# 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 "=== Generating query load (13 calls) ===" - @# PocketIC simulates a 13-node subnet and uses integer division when spreading - @# stats across nodes (num_calls /= 13). Fewer than 13 calls rounds to 0. - @for i in $$(seq 1 13); do \ - icp canister call --query backend load '()' > /dev/null; \ - done - @echo "=== Waiting for 2-epoch aggregation (up to 30s) ===" - @# Each epoch = 60 blocks; auto-progress advances ~100ms/block. - @# 120 blocks minimum; 30s gives margin for slower machines. + @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; \ diff --git a/motoko/query_stats/README.md b/motoko/query_stats/README.md index 36aa62828..b1cc8f65a 100644 --- a/motoko/query_stats/README.md +++ b/motoko/query_stats/README.md @@ -13,7 +13,13 @@ Query stats are **aggregated with a 2-epoch delay**, not updated per call: 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`. -PocketIC simulates a 13-node subnet for consensus. When distributing stats across nodes it uses integer division (`num_calls / 13`), so **fewer than 13 query calls round to zero** — `make test-stats` makes exactly 13 calls to ensure non-zero values survive the division. +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 From 181d06943686bd684cc4af5c65d6df0c29b73797 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 11 Jun 2026 19:18:17 +0200 Subject: [PATCH 21/21] docs: fix README to reflect current make test-stats behavior Co-Authored-By: Claude Sonnet 4.6 --- motoko/query_stats/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/motoko/query_stats/README.md b/motoko/query_stats/README.md index b1cc8f65a..b156ac64f 100644 --- a/motoko/query_stats/README.md +++ b/motoko/query_stats/README.md @@ -51,7 +51,7 @@ make test-stats icp network stop ``` -`make test-stats` calls `load()` 20 times with `--query`, waits 20 seconds for the aggregation window, then verifies that `Number of calls` is non-zero. +`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