From 36d288b86059f3086dbf1455c96c474b0f6bd67e Mon Sep 17 00:00:00 2001 From: Raymond Khalife Date: Wed, 17 Jun 2026 20:48:36 -0400 Subject: [PATCH 1/2] chore(ui): use bindgen to create the bindings --- tools/ui/.gitignore | 3 +++ tools/ui/package-lock.json | 24 ++++++++++++++++++++++++ tools/ui/package.json | 1 + tools/ui/src/candid.ts | 29 +++++++++-------------------- tools/ui/src/index.ts | 4 ++-- tools/ui/src/profiler.did | 10 ++++++++++ tools/ui/vite.config.ts | 11 ++++++++++- 7 files changed, 59 insertions(+), 23 deletions(-) create mode 100644 tools/ui/src/profiler.did diff --git a/tools/ui/.gitignore b/tools/ui/.gitignore index 11f5b07b..89404f66 100644 --- a/tools/ui/.gitignore +++ b/tools/ui/.gitignore @@ -17,5 +17,8 @@ node_modules/ dist/ ts-out/ +# Candid bindings generated by @icp-sdk/bindgen (see vite.config.ts) +src/bindings/ + .dfx .icp/cache diff --git a/tools/ui/package-lock.json b/tools/ui/package-lock.json index a8dae33e..ae92ff6e 100644 --- a/tools/ui/package-lock.json +++ b/tools/ui/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "devDependencies": { "@icp-sdk/auth": "^7.1.0", + "@icp-sdk/bindgen": "^0.4.0", "@icp-sdk/canisters": "^3.6.0", "@icp-sdk/core": "^5.4.0", "typescript": "^5.6.2", @@ -484,6 +485,19 @@ "@icp-sdk/core": "^5" } }, + "node_modules/@icp-sdk/bindgen": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@icp-sdk/bindgen/-/bindgen-0.4.0.tgz", + "integrity": "sha512-WXJoPIVnqCgFSNcprVpHwPBNWA1prwPh1/ezQYVrCP+Zj8CCJ7zj9L7p5cFcN7ghxoxfbx6tYT8t/HN9fyeYkA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "commander": "^14.0.3" + }, + "bin": { + "icp-bindgen": "dist/esm/cli/icp-bindgen.js" + } + }, "node_modules/@icp-sdk/canisters": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/@icp-sdk/canisters/-/canisters-3.6.0.tgz", @@ -965,6 +979,16 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/esbuild": { "version": "0.25.12", "dev": true, diff --git a/tools/ui/package.json b/tools/ui/package.json index fd5de9cb..1abbf8bb 100644 --- a/tools/ui/package.json +++ b/tools/ui/package.json @@ -10,6 +10,7 @@ }, "devDependencies": { "@icp-sdk/auth": "^7.1.0", + "@icp-sdk/bindgen": "^0.4.0", "@icp-sdk/canisters": "^3.6.0", "@icp-sdk/core": "^5.4.0", "typescript": "^5.6.2", diff --git a/tools/ui/src/candid.ts b/tools/ui/src/candid.ts index f85bc262..a2985740 100644 --- a/tools/ui/src/candid.ts +++ b/tools/ui/src/candid.ts @@ -6,6 +6,7 @@ import { import {Principal} from '@icp-sdk/core/principal' import { IcManagementCanister } from '@icp-sdk/canisters/ic-management'; import { getCanisterEnv } from '@icp-sdk/core/agent/canister-env'; +import { createActor as createProfilerActor, type Profiler } from './bindings/profiler/profiler'; import './candid.css'; import { AuthClient, type AuthClientCreateOptions } from "@icp-sdk/auth/client"; @@ -70,13 +71,6 @@ export async function fetchActor(canisterId: Principal): Promise return Actor.createActor(candid.idlFactory, { agent, canisterId }); } -export function getProfilerActor(canisterId: Principal): ActorSubclass { - const profiler_interface: IDL.InterfaceFactory = ({ IDL }) => IDL.Service({ - __get_profiling: IDL.Func([IDL.Int32], [IDL.Vec(IDL.Tuple(IDL.Int32, IDL.Int64)), IDL.Opt(IDL.Int32)], ['query']), - __get_cycles: IDL.Func([], [IDL.Int64], ['query']), - }); - return Actor.createActor(profiler_interface, { agent, canisterId }); -} function uint8ArrayToDisplay(array: Uint8Array | number[]) { const uint8Array = new Uint8Array(array); try { @@ -136,8 +130,8 @@ function postToPlayground(id: Principal) { export async function getCycles(canisterId: Principal): Promise { try { - const actor = getProfilerActor(canisterId); - const cycles = await actor.__get_cycles() as bigint; + const actor = createProfilerActor(canisterId.toText(), { agent }); + const cycles = await actor.__get_cycles(); return cycles; } catch(err) { return undefined; @@ -158,11 +152,6 @@ export async function getNames(canisterId: Principal) { } } -async function getDidJsFromPostMessage(canisterId: Principal): Promise { - return new Promise((resolve,reject)=>{}) -} - - async function getDidJsFromMetadata(canisterId: Principal): Promise { // The 'candid' path resolves to the `candid:service` metadata section and is // read (and certified) via the canister's read_state endpoint. @@ -180,15 +169,15 @@ async function getDidJsFromMetadata(canisterId: Principal): Promise|undefined> { try { - const actor = getProfilerActor(canisterId); + const actor = createProfilerActor(canisterId.toText(), { agent }); let info: Array<[number, bigint]> = []; let idx = 0; let cnt = 0; while (cnt < 50) { - const [res, next] = await actor.__get_profiling(idx) as [Array<[number, bigint]>, [number]|[]]; + const [res, next] = await actor.__get_profiling(idx); info = info.concat(res); - if (next.length === 1) { - idx = next[0]; + if (next !== null) { + idx = next; cnt++; } else { break; @@ -314,7 +303,7 @@ function renderMethod(canister: ActorSubclass, name: string, idlFunc: IDL.FuncCl item.appendChild(inputContainer); const inputs: InputBox[] = []; - idlFunc.argTypes.forEach((arg, i) => { + idlFunc.argTypes.forEach((arg, _i) => { const inputbox = renderInput(arg); inputs.push(inputbox); inputbox.render(inputContainer); @@ -445,7 +434,7 @@ function renderMethod(canister: ActorSubclass, name: string, idlFunc: IDL.FuncCl containers.push(jsonContainer); jsonContainer.style.display = setContainerVisibility('json'); left.appendChild(jsonContainer); - jsonContainer.innerText = JSON.stringify(callResult, (k,v) => typeof v === 'bigint'?v.toString():v); + jsonContainer.innerText = JSON.stringify(callResult, (_k,v) => typeof v === 'bigint'?v.toString():v); })().catch(err => { resultDiv.classList.add('error'); left.innerText = err.message; diff --git a/tools/ui/src/index.ts b/tools/ui/src/index.ts index 78f3a18f..3c63b12b 100644 --- a/tools/ui/src/index.ts +++ b/tools/ui/src/index.ts @@ -26,7 +26,7 @@ async function main() { const reader = new FileReader(); reader.addEventListener("load", () => { const encoded = reader.result as string; - const candid = encoded.substr(encoded.indexOf(",") + 1); + const candid = encoded.substring(encoded.indexOf(",") + 1); // update URL with Candid data and refresh window.history.pushState({}, "", window.location.search); window.history.pushState({ candid }, "", `?${params}`); @@ -43,7 +43,7 @@ async function main() { const profiling = await getCycles(canisterId); actor = await fetchActor(canisterId); await renderAuth(); - const names = await getNames(canisterId); + await getNames(canisterId); render(canisterId, actor, profiling); const app = document.getElementById("app"); const progress = document.getElementById("progress"); diff --git a/tools/ui/src/profiler.did b/tools/ui/src/profiler.did new file mode 100644 index 00000000..19e2180a --- /dev/null +++ b/tools/ui/src/profiler.did @@ -0,0 +1,10 @@ +// Interface injected into a canister's Wasm by `ic-wasm instrument`. +// Present only on instrumented canisters; the Candid UI probes for it to +// render the cycle counter and the profiling flamegraph. +service : { + // Get the current cycle counter. + __get_cycles : () -> (int64) query; + // Get the execution trace starting at the given index. Returns the trace + // chunk and, if the log exceeds one chunk, the next index to fetch. + __get_profiling : (int32) -> (vec record { int32; int64 }, opt int32) query; +} diff --git a/tools/ui/vite.config.ts b/tools/ui/vite.config.ts index 7d869f31..a1a45245 100644 --- a/tools/ui/vite.config.ts +++ b/tools/ui/vite.config.ts @@ -1,5 +1,6 @@ import { defineConfig } from "vite"; import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js"; +import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite"; // The `didjs` canister embeds the built frontend directly via // `include_bytes!("../../dist/didjs/{index.html,index.js,favicon.ico}")` @@ -11,7 +12,15 @@ import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js"; export default defineConfig({ // Inline imported CSS into the JS bundle (the canister only serves index.js, // not a separate stylesheet) — mirrors the previous webpack `style-loader`. - plugins: [cssInjectedByJsPlugin()], + plugins: [ + cssInjectedByJsPlugin(), + // Generate typed bindings for the fixed ic-wasm profiler interface + // (`__get_cycles` / `__get_profiling`) from src/profiler.did. + icpBindgen({ + didFile: "./src/profiler.did", + outDir: "./src/bindings/profiler", + }), + ], // Some @dfinity packages reference `global`; map it to `globalThis` for the browser. define: { global: "globalThis", From f47ff9f70609f6bf95c6dc9f1e57356bed91ddf5 Mon Sep 17 00:00:00 2001 From: Raymond Khalife Date: Wed, 17 Jun 2026 21:01:25 -0400 Subject: [PATCH 2/2] use bindgen for didjs --- tools/ui/src/candid.ts | 15 +++++---------- tools/ui/vite.config.ts | 10 ++++++++++ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/tools/ui/src/candid.ts b/tools/ui/src/candid.ts index a2985740..98c96c8d 100644 --- a/tools/ui/src/candid.ts +++ b/tools/ui/src/candid.ts @@ -6,7 +6,8 @@ import { import {Principal} from '@icp-sdk/core/principal' import { IcManagementCanister } from '@icp-sdk/canisters/ic-management'; import { getCanisterEnv } from '@icp-sdk/core/agent/canister-env'; -import { createActor as createProfilerActor, type Profiler } from './bindings/profiler/profiler'; +import { createActor as createProfilerActor } from './bindings/profiler/profiler'; +import { idlFactory as didjsIdlFactory, type _SERVICE as DidjsService } from './bindings/didjs/declarations/didjs.did'; import './candid.css'; import { AuthClient, type AuthClientCreateOptions } from "@icp-sdk/auth/client"; @@ -252,15 +253,9 @@ async function renderFlameGraph(profiler: any) { async function didToJs(candid_source: string): Promise { // call didjs canister const didjs_id = canisterEnv["CANISTER_ID"]; - const didjs_interface: IDL.InterfaceFactory = ({ IDL }) => IDL.Service({ - did_to_js: IDL.Func([IDL.Text], [IDL.Opt(IDL.Text)], ['query']), - }); - const didjs: ActorSubclass = Actor.createActor(didjs_interface, { agent, canisterId: didjs_id }); - const js: any = await didjs.did_to_js(candid_source); - if (JSON.stringify(js) === JSON.stringify([])) { - return undefined; - } - return js[0]; + const didjs = Actor.createActor(didjsIdlFactory, { agent, canisterId: didjs_id }); + const [js] = await didjs.did_to_js(candid_source); + return js; } function is_query(func: IDL.FuncClass): boolean { diff --git a/tools/ui/vite.config.ts b/tools/ui/vite.config.ts index a1a45245..69a1baff 100644 --- a/tools/ui/vite.config.ts +++ b/tools/ui/vite.config.ts @@ -20,6 +20,16 @@ export default defineConfig({ didFile: "./src/profiler.did", outDir: "./src/bindings/profiler", }), + // Generate typed bindings for the didjs canister's own interface + // (`did_to_js` etc.) from its candid definition. The high-level actor + // wrapper is disabled because the `subtype` method has a parameter named + // `new` (a JS reserved word) that the wrapper can't represent; we use the + // low-level idlFactory + _SERVICE from the declarations instead. + icpBindgen({ + didFile: "./src/didjs/didjs.did", + outDir: "./src/bindings/didjs", + output: { actor: { disabled: true } }, + }), ], // Some @dfinity packages reference `global`; map it to `globalThis` for the browser. define: {