Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions tools/ui/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
24 changes: 24 additions & 0 deletions tools/ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tools/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
42 changes: 13 additions & 29 deletions tools/ui/src/candid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +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 } 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";

Expand Down Expand Up @@ -70,13 +72,6 @@ export async function fetchActor(canisterId: Principal): Promise<ActorSubclass>
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 {
Expand Down Expand Up @@ -136,8 +131,8 @@ function postToPlayground(id: Principal) {

export async function getCycles(canisterId: Principal): Promise<bigint|undefined> {
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;
Expand All @@ -158,11 +153,6 @@ export async function getNames(canisterId: Principal) {
}
}

async function getDidJsFromPostMessage(canisterId: Principal): Promise<undefined | string> {
return new Promise((resolve,reject)=>{})
}


async function getDidJsFromMetadata(canisterId: Principal): Promise<undefined | string> {
// The 'candid' path resolves to the `candid:service` metadata section and is
// read (and certified) via the canister's read_state endpoint.
Expand All @@ -180,15 +170,15 @@ async function getDidJsFromMetadata(canisterId: Principal): Promise<undefined |

export async function getProfiling(canisterId: Principal): Promise<Array<[number, bigint]>|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;
Expand Down Expand Up @@ -263,15 +253,9 @@ async function renderFlameGraph(profiler: any) {
async function didToJs(candid_source: string): Promise<undefined | string> {
// 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<DidjsService>(didjsIdlFactory, { agent, canisterId: didjs_id });
const [js] = await didjs.did_to_js(candid_source);
return js;
}

function is_query(func: IDL.FuncClass): boolean {
Expand Down Expand Up @@ -314,7 +298,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);
Expand Down Expand Up @@ -445,7 +429,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;
Expand Down
4 changes: 2 additions & 2 deletions tools/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand All @@ -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");
Expand Down
10 changes: 10 additions & 0 deletions tools/ui/src/profiler.did
Original file line number Diff line number Diff line change
@@ -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;
}
21 changes: 20 additions & 1 deletion tools/ui/vite.config.ts
Original file line number Diff line number Diff line change
@@ -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}")`
Expand All @@ -11,7 +12,25 @@ 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",
}),
// 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: {
global: "globalThis",
Expand Down
Loading