@nite-framework/nite-zk-profiler 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/report.js CHANGED
@@ -1,74 +1,198 @@
1
+ import { bold, costColor, dim, gray, green, red, visibleLength, yellow } from "./colors.js";
2
+ import { formatBytes, formatMs } from "./deep.js";
3
+ import { estimateProvingMs, formatDuration } from "./estimate.js";
4
+ /** Pad accounting for ANSI escapes, which do not occupy screen columns. */
1
5
  function pad(text, width) {
2
- return text.padEnd(width);
6
+ return text + " ".repeat(Math.max(0, width - visibleLength(text)));
3
7
  }
4
8
  function padLeft(text, width) {
5
- return text.padStart(width);
9
+ return " ".repeat(Math.max(0, width - visibleLength(text))) + text;
6
10
  }
7
- function costLabel(relative) {
8
- return `${relative}x`;
11
+ function table(costs, opts, indent) {
12
+ const { deep, calibration, showEstimate } = opts;
13
+ const cells = costs.map((c) => {
14
+ const paint = costColor(c.relativeCost);
15
+ const d = deep?.get(c.circuit);
16
+ return {
17
+ circuit: c.circuit,
18
+ rows: String(c.rows),
19
+ k: paint(String(c.k)),
20
+ capacity: String(c.capacity),
21
+ cost: paint(`${c.relativeCost}x`),
22
+ prove: showEstimate ? `~${formatDuration(estimateProvingMs(c.k, calibration).ms)}` : "",
23
+ setup: d ? formatMs(d.setupMs) : "",
24
+ key: d ? formatBytes(d.proverKeyBytes) : "",
25
+ };
26
+ });
27
+ const width = (header, get) => Math.max(header.length, ...cells.map((c) => visibleLength(get(c))));
28
+ const w = {
29
+ name: width("circuit", (c) => c.circuit),
30
+ rows: width("rows", (c) => c.rows),
31
+ k: width("k", (c) => c.k),
32
+ cap: width("capacity", (c) => c.capacity),
33
+ cost: width("cost", (c) => c.cost),
34
+ prove: showEstimate ? width("est. prove", (c) => c.prove) : 0,
35
+ setup: deep ? width("setup", (c) => c.setup) : 0,
36
+ key: deep ? width("prover key", (c) => c.key) : 0,
37
+ };
38
+ const head = () => {
39
+ let h = `${indent}${pad("circuit", w.name)} ${padLeft("rows", w.rows)} ${padLeft("k", w.k)} ` +
40
+ `${padLeft("capacity", w.cap)} ${padLeft("cost", w.cost)}`;
41
+ if (showEstimate)
42
+ h += ` ${padLeft("est. prove", w.prove)}`;
43
+ if (deep)
44
+ h += ` ${padLeft("setup", w.setup)} ${padLeft("prover key", w.key)}`;
45
+ return dim(h);
46
+ };
47
+ const lines = [head()];
48
+ for (const c of cells) {
49
+ let line = `${indent}${pad(c.circuit, w.name)} ${padLeft(c.rows, w.rows)} ${padLeft(c.k, w.k)} ` +
50
+ `${padLeft(c.capacity, w.cap)} ${padLeft(c.cost, w.cost)}`;
51
+ if (showEstimate)
52
+ line += ` ${padLeft(c.prove, w.prove)}`;
53
+ if (deep)
54
+ line += ` ${padLeft(c.setup, w.setup)} ${padLeft(c.key, w.key)}`;
55
+ lines.push(line);
56
+ }
57
+ return lines;
9
58
  }
10
- /** Human readable per circuit cost table. */
11
- export function formatProfile(costs, toolchain, elapsedMs) {
12
- const nameWidth = Math.max(7, ...costs.map((c) => c.circuit.length));
13
- const rowsWidth = Math.max(4, ...costs.map((c) => String(c.rows).length));
14
- const capWidth = Math.max(8, ...costs.map((c) => String(c.capacity).length));
15
- const costWidth = Math.max(4, ...costs.map((c) => costLabel(c.relativeCost).length));
59
+ /** Per circuit cost table, one block per contract. */
60
+ export function formatProfile(contracts, opts) {
16
61
  const lines = [""];
17
- lines.push(` ${pad("circuit", nameWidth)} ${padLeft("rows", rowsWidth)} ${padLeft("k", 3)} ` +
18
- `${padLeft("capacity", capWidth)} ${padLeft("cost", costWidth)}`);
19
- for (const c of costs) {
20
- lines.push(` ${pad(c.circuit, nameWidth)} ${padLeft(String(c.rows), rowsWidth)} ` +
21
- `${padLeft(String(c.k), 3)} ${padLeft(String(c.capacity), capWidth)} ` +
22
- `${padLeft(costLabel(c.relativeCost), costWidth)}`);
62
+ const many = contracts.length > 1;
63
+ for (const { source, costs } of contracts) {
64
+ if (many) {
65
+ lines.push(bold(` ${source}`));
66
+ lines.push(...table(costs, opts, " "));
67
+ lines.push("");
68
+ }
69
+ else {
70
+ lines.push(...table(costs, opts, " "));
71
+ lines.push("");
72
+ }
73
+ }
74
+ const all = contracts.flatMap((c) => c.costs);
75
+ const circuits = all.length;
76
+ const suffix = opts.cached ? " (cached)" : "";
77
+ const contractNote = many ? `${contracts.length} contracts, ` : "";
78
+ lines.push(gray(` ${contractNote}${circuits} circuit${circuits === 1 ? "" : "s"}, ` +
79
+ `toolchain ${opts.toolchain.version}, ${opts.toolchain.zkirVersion}, ` +
80
+ `${(opts.elapsedMs / 1000).toFixed(1)}s${suffix}`));
81
+ if (all.length > 0) {
82
+ const worst = all.reduce((a, b) => (b.k > a.k ? b : a), all[0]);
83
+ const cheapest = Math.min(...all.map((c) => c.k));
84
+ if (worst.k > cheapest) {
85
+ lines.push(gray(` ${bold(worst.circuit)} dominates at k=${worst.k}, ` +
86
+ `${2 ** (worst.k - cheapest)}x the cheapest circuit here.`));
87
+ }
88
+ }
89
+ if (opts.showEstimate) {
90
+ lines.push(gray(opts.calibration
91
+ ? ` est. prove is modelled as 2^k, calibrated from an observed ${formatDuration(opts.calibration.observedMs)} proof at k=${opts.calibration.observedK}.`
92
+ : " est. prove is modelled as 2^k on an uncalibrated default. Run `nite-zk calibrate` to anchor it."));
23
93
  }
24
- const plural = costs.length === 1 ? "circuit" : "circuits";
25
- lines.push("");
26
- lines.push(` ${costs.length} ${plural}, toolchain ${toolchain.version}, ` +
27
- `${toolchain.zkirVersion}, ${(elapsedMs / 1000).toFixed(1)}s`);
28
94
  lines.push("");
29
95
  return lines.join("\n");
30
96
  }
31
97
  const STATUS_NOTE = {
32
- under: (r) => `under by ${(r.maxK ?? 0) - (r.k ?? 0)}`,
33
- at: () => "at budget",
98
+ under: (r) => green(`under by ${(r.maxK ?? 0) - (r.k ?? 0)}`),
99
+ at: () => green("at budget"),
34
100
  over: (r) => {
35
101
  const by = (r.k ?? 0) - (r.maxK ?? 0);
36
- return `over by ${by}, about ${2 ** by}x`;
102
+ return red(`over by ${by}, about ${2 ** by}x`);
37
103
  },
38
- undeclared: () => "not in budget",
39
- stale: () => "no longer in contract",
104
+ undeclared: () => yellow("not in budget"),
105
+ stale: () => dim("no longer in contract"),
40
106
  };
41
- /** Human readable budget comparison. */
107
+ /** Budget comparison, grouped by contract when there is more than one. */
42
108
  export function formatCheck(result) {
43
- const nameWidth = Math.max(7, ...result.rows.map((r) => r.circuit.length));
109
+ const contracts = [...new Set(result.rows.map((r) => r.contract))];
110
+ const many = contracts.length > 1;
44
111
  const lines = [""];
45
- for (const row of result.rows) {
46
- const k = row.k === undefined ? " -" : padLeft(String(row.k), 3);
47
- const maxK = row.maxK === undefined ? " -" : padLeft(String(row.maxK), 3);
48
- lines.push(` ${pad(row.circuit, nameWidth)} k ${k} budget ${maxK} ${STATUS_NOTE[row.status](row)}`);
112
+ for (const contract of contracts) {
113
+ const rows = result.rows.filter((r) => r.contract === contract);
114
+ const indent = many ? " " : " ";
115
+ if (many)
116
+ lines.push(bold(` ${contract}`));
117
+ const wName = Math.max(7, ...rows.map((r) => r.circuit.length));
118
+ for (const row of rows) {
119
+ const k = row.k === undefined ? " -" : padLeft(String(row.k), 3);
120
+ const maxK = row.maxK === undefined ? " -" : padLeft(String(row.maxK), 3);
121
+ const paint = row.status === "over" ? red : (t) => t;
122
+ lines.push(`${indent}${pad(paint(row.circuit), wName)} k ${k} ${dim("budget")} ${maxK} ` +
123
+ STATUS_NOTE[row.status](row));
124
+ }
125
+ if (many)
126
+ lines.push("");
49
127
  }
50
128
  const over = result.rows.filter((r) => r.status === "over").length;
51
129
  const undeclared = result.rows.filter((r) => r.status === "undeclared").length;
52
- lines.push("");
130
+ if (!many)
131
+ lines.push("");
53
132
  if (over > 0) {
54
- lines.push(` FAIL: ${over} circuit${over === 1 ? "" : "s"} over budget`);
133
+ lines.push(red(bold(` FAIL: ${over} circuit${over === 1 ? "" : "s"} over budget`)));
55
134
  }
56
135
  else if (result.failed) {
57
- lines.push(` FAIL: ${undeclared} circuit${undeclared === 1 ? "" : "s"} not declared in the budget (--strict)`);
136
+ lines.push(red(bold(` FAIL: ${undeclared} circuit${undeclared === 1 ? "" : "s"} not declared (--strict)`)));
58
137
  }
59
138
  else {
60
- lines.push(" OK: every circuit within budget");
139
+ lines.push(green(bold(" OK: every circuit within budget")));
61
140
  }
62
141
  lines.push("");
63
142
  return lines.join("\n");
64
143
  }
65
- export function profileJson(costs, toolchain) {
144
+ /** Comparison against a git ref. */
145
+ export function formatDiff(result) {
146
+ const lines = [""];
147
+ const wName = Math.max(7, ...result.rows.map((r) => r.circuit.length));
148
+ for (const row of result.rows) {
149
+ const before = row.before === undefined ? dim(" -") : padLeft(String(row.before), 3);
150
+ const after = row.after === undefined ? dim(" -") : padLeft(String(row.after), 3);
151
+ let note;
152
+ if (row.before === undefined)
153
+ note = yellow("new circuit");
154
+ else if (row.after === undefined)
155
+ note = dim("removed");
156
+ else if (row.after > row.before)
157
+ note = red(`+${row.after - row.before}, about ${2 ** (row.after - row.before)}x more expensive`);
158
+ else if (row.after < row.before)
159
+ note = green(`${row.after - row.before}, about ${2 ** (row.before - row.after)}x cheaper`);
160
+ else
161
+ note = dim("unchanged");
162
+ lines.push(` ${pad(row.circuit, wName)} k ${before} ${dim("->")} ${after} ${note}`);
163
+ }
164
+ const worse = result.rows.filter((r) => r.before !== undefined && r.after !== undefined && r.after > r.before).length;
165
+ lines.push("");
166
+ lines.push(worse > 0
167
+ ? red(bold(` ${worse} circuit${worse === 1 ? "" : "s"} more expensive than ${result.ref}`))
168
+ : green(bold(` nothing more expensive than ${result.ref}`)));
169
+ lines.push("");
170
+ return lines.join("\n");
171
+ }
172
+ export function profileJson(contracts, opts) {
66
173
  return JSON.stringify({
67
- toolchain: toolchain.version,
68
- zkir: toolchain.zkirVersion,
69
- circuits: costs,
174
+ toolchain: opts.toolchain.version,
175
+ zkir: opts.toolchain.zkirVersion,
176
+ contracts: contracts.map(({ source, costs }) => ({
177
+ source,
178
+ circuits: costs.map((c) => {
179
+ const d = opts.deep?.get(c.circuit);
180
+ const est = opts.showEstimate
181
+ ? {
182
+ estimatedProvingMs: Math.round(estimateProvingMs(c.k, opts.calibration).ms),
183
+ estimateCalibrated: opts.calibration !== undefined,
184
+ }
185
+ : {};
186
+ return d
187
+ ? { ...c, ...est, setupMs: Math.round(d.setupMs), proverKeyBytes: d.proverKeyBytes }
188
+ : { ...c, ...est };
189
+ }),
190
+ })),
70
191
  }, null, 2);
71
192
  }
72
193
  export function checkJson(result) {
73
194
  return JSON.stringify(result, null, 2);
74
195
  }
196
+ export function diffJson(result) {
197
+ return JSON.stringify(result, null, 2);
198
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Read from package.json rather than hardcoded, because `npm version` updates
3
+ * the manifest and would silently leave a literal behind, so `--version` would
4
+ * report the wrong release.
5
+ *
6
+ * Resolves from this module, which sits one level below the manifest in both
7
+ * `src/` and the published `dist/`.
8
+ */
9
+ export declare function toolVersion(): string;
@@ -0,0 +1,19 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /**
4
+ * Read from package.json rather than hardcoded, because `npm version` updates
5
+ * the manifest and would silently leave a literal behind, so `--version` would
6
+ * report the wrong release.
7
+ *
8
+ * Resolves from this module, which sits one level below the manifest in both
9
+ * `src/` and the published `dist/`.
10
+ */
11
+ export function toolVersion() {
12
+ try {
13
+ const manifest = join(import.meta.dirname, "..", "package.json");
14
+ return JSON.parse(readFileSync(manifest, "utf8")).version ?? "unknown";
15
+ }
16
+ catch {
17
+ return "unknown";
18
+ }
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nite-framework/nite-zk-profiler",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "See what a Compact circuit costs to prove, without generating proving keys",
5
5
  "type": "module",
6
6
  "bin": {