@nite-framework/nite-zk-profiler 0.1.3 → 0.2.0
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/README.md +104 -6
- package/dist/budget.d.ts +36 -10
- package/dist/budget.js +94 -37
- package/dist/cache.d.ts +16 -0
- package/dist/cache.js +54 -0
- package/dist/cli.d.ts +10 -3
- package/dist/cli.js +211 -50
- package/dist/colors.d.ts +40 -0
- package/dist/colors.js +69 -0
- package/dist/compile.d.ts +1 -1
- package/dist/compile.js +17 -8
- package/dist/deep.d.ts +24 -0
- package/dist/deep.js +60 -0
- package/dist/diff.d.ts +30 -0
- package/dist/diff.js +64 -0
- package/dist/estimate.d.ts +38 -0
- package/dist/estimate.js +85 -0
- package/dist/measure.d.ts +14 -5
- package/dist/measure.js +72 -17
- package/dist/progress.d.ts +19 -0
- package/dist/progress.js +51 -0
- package/dist/report.d.ts +20 -6
- package/dist/report.js +163 -39
- package/dist/version.d.ts +9 -0
- package/dist/version.js +19 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,33 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
2
3
|
import { analyze } from "./analyze.js";
|
|
3
|
-
import { DEFAULT_BUDGET_PATH, budgetFrom, check, readBudget, writeBudget, } from "./budget.js";
|
|
4
|
+
import { DEFAULT_BUDGET_PATH, budgetFrom, budgetSources, check, readBudget, writeBudget, } from "./budget.js";
|
|
5
|
+
import { cacheKey, readCache, writeCache } from "./cache.js";
|
|
6
|
+
import { setColorEnabled } from "./colors.js";
|
|
4
7
|
import { compileSkipZk } from "./compile.js";
|
|
8
|
+
import { measureDeep } from "./deep.js";
|
|
9
|
+
import { diffCosts, materialise, pathWithinRef, repoRoot } from "./diff.js";
|
|
5
10
|
import { ProfilerError } from "./errors.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
11
|
+
import { calibrationFrom, readCalibration, writeCalibration } from "./estimate.js";
|
|
12
|
+
import { measureParallel } from "./measure.js";
|
|
13
|
+
import { Progress } from "./progress.js";
|
|
14
|
+
import { checkJson, diffJson, formatCheck, formatDiff, formatProfile, profileJson, } from "./report.js";
|
|
8
15
|
import { SUPPORTED_RANGES, resolveToolchain } from "./toolchain.js";
|
|
16
|
+
import { toolVersion } from "./version.js";
|
|
9
17
|
const USAGE = `nite-zk - see what a Compact circuit costs to prove
|
|
10
18
|
|
|
11
19
|
Usage:
|
|
12
|
-
nite-zk profile <source
|
|
13
|
-
nite-zk save <source
|
|
14
|
-
nite-zk check <source
|
|
20
|
+
nite-zk profile <source...> Report rows, k and relative cost per circuit
|
|
21
|
+
nite-zk save <source...> Write zk-budget.json from current measurements
|
|
22
|
+
nite-zk check [<source...>] Compare against zk-budget.json
|
|
23
|
+
Sources are optional once saved, since the
|
|
24
|
+
budget records which contracts it describes.
|
|
25
|
+
nite-zk diff <ref> [<source>] Compare a contract against a git ref
|
|
26
|
+
nite-zk calibrate --observed <ms> --at-k <k>
|
|
27
|
+
Anchor proving estimates to a real proof
|
|
15
28
|
|
|
16
29
|
Options:
|
|
17
|
-
--
|
|
18
|
-
--
|
|
19
|
-
|
|
20
|
-
--
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
--estimate Show modelled proving time per circuit
|
|
31
|
+
--deep Also generate real proving keys and report
|
|
32
|
+
measured setup time and prover key size
|
|
33
|
+
--json Machine readable output
|
|
34
|
+
--out <dir> Compile into a specific directory (kept)
|
|
35
|
+
--budget <file> Budget path (default: ${DEFAULT_BUDGET_PATH})
|
|
36
|
+
--strict check: fail on circuits missing from the budget
|
|
37
|
+
--no-color Plain output (also honours NO_COLOR)
|
|
38
|
+
--no-cache Ignore cached measurements for this run
|
|
39
|
+
+VERSION Pin the Compact toolchain, e.g. +0.31.1
|
|
40
|
+
-h, --help Show this message
|
|
41
|
+
-v, --version Show the tool version
|
|
24
42
|
|
|
25
43
|
Supported Compact toolchains: ${SUPPORTED_RANGES.join(", ")}
|
|
26
44
|
`;
|
|
27
45
|
export function parseArgs(argv) {
|
|
28
46
|
const opts = {
|
|
47
|
+
sources: [],
|
|
29
48
|
json: false,
|
|
30
49
|
strict: false,
|
|
50
|
+
deep: false,
|
|
51
|
+
estimate: false,
|
|
52
|
+
noColor: false,
|
|
53
|
+
noCache: false,
|
|
31
54
|
budget: DEFAULT_BUDGET_PATH,
|
|
32
55
|
help: false,
|
|
33
56
|
version: false,
|
|
@@ -38,6 +61,14 @@ export function parseArgs(argv) {
|
|
|
38
61
|
opts.json = true;
|
|
39
62
|
else if (arg === "--strict")
|
|
40
63
|
opts.strict = true;
|
|
64
|
+
else if (arg === "--deep")
|
|
65
|
+
opts.deep = true;
|
|
66
|
+
else if (arg === "--estimate")
|
|
67
|
+
opts.estimate = true;
|
|
68
|
+
else if (arg === "--no-color")
|
|
69
|
+
opts.noColor = true;
|
|
70
|
+
else if (arg === "--no-cache")
|
|
71
|
+
opts.noCache = true;
|
|
41
72
|
else if (arg === "-h" || arg === "--help")
|
|
42
73
|
opts.help = true;
|
|
43
74
|
else if (arg === "-v" || arg === "--version")
|
|
@@ -46,70 +77,200 @@ export function parseArgs(argv) {
|
|
|
46
77
|
opts.out = argv[++i];
|
|
47
78
|
else if (arg === "--budget")
|
|
48
79
|
opts.budget = argv[++i] ?? DEFAULT_BUDGET_PATH;
|
|
80
|
+
else if (arg === "--observed")
|
|
81
|
+
opts.observedMs = Number(argv[++i]);
|
|
82
|
+
else if (arg === "--at-k")
|
|
83
|
+
opts.atK = Number(argv[++i]);
|
|
49
84
|
else if (arg.startsWith("+"))
|
|
50
85
|
opts.versionArg = arg;
|
|
51
86
|
else if (!opts.command)
|
|
52
87
|
opts.command = arg;
|
|
53
|
-
else if (
|
|
54
|
-
opts.
|
|
88
|
+
else if (opts.command === "diff" && opts.ref === undefined)
|
|
89
|
+
opts.ref = arg;
|
|
90
|
+
else
|
|
91
|
+
opts.sources.push(arg);
|
|
55
92
|
}
|
|
56
93
|
return opts;
|
|
57
94
|
}
|
|
58
|
-
/** Compile and measure
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
// or wildly wrong duration.
|
|
62
|
-
const started = performance.now();
|
|
95
|
+
/** Compile and measure one contract. */
|
|
96
|
+
async function profileOne(source, opts, progress, label) {
|
|
97
|
+
progress.update(`${label}compiling`);
|
|
63
98
|
const toolchain = resolveToolchain(opts.versionArg);
|
|
64
|
-
const compiled = compileSkipZk(source, toolchain, opts.out);
|
|
99
|
+
const compiled = await compileSkipZk(source, toolchain, opts.out);
|
|
65
100
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
101
|
+
// The compile is the cheap half, so it always runs and its output keys the
|
|
102
|
+
// cache. Identical IR cannot produce different constraint counts.
|
|
103
|
+
const key = opts.noCache ? undefined : cacheKey(compiled.zkirDir, toolchain);
|
|
104
|
+
let measurements = key ? readCache(key) : undefined;
|
|
105
|
+
const cached = measurements !== undefined;
|
|
106
|
+
if (!measurements) {
|
|
107
|
+
measurements = await measureParallel(compiled.zkirDir, toolchain, source, (d, t) => progress.update(`${label}measuring ${d}/${t}`));
|
|
108
|
+
if (key)
|
|
109
|
+
writeCache(key, measurements);
|
|
110
|
+
}
|
|
111
|
+
let deep;
|
|
112
|
+
if (opts.deep) {
|
|
113
|
+
const results = await measureDeep(measurements, compiled.zkirDir, toolchain, (c, i, n) => progress.update(`${label}generating proving keys ${i + 1}/${n} ${c}`));
|
|
114
|
+
deep = new Map(results.map((r) => [r.circuit, r]));
|
|
115
|
+
}
|
|
116
|
+
return { costs: analyze(measurements), toolchain, deep, cached };
|
|
68
117
|
}
|
|
69
118
|
finally {
|
|
70
119
|
compiled.cleanup();
|
|
71
120
|
}
|
|
72
121
|
}
|
|
73
|
-
|
|
122
|
+
/** Compile and measure every requested contract. */
|
|
123
|
+
async function profileAll(sources, opts) {
|
|
124
|
+
const started = performance.now();
|
|
125
|
+
const progress = new Progress(!opts.json);
|
|
126
|
+
progress.start("resolving toolchain");
|
|
127
|
+
try {
|
|
128
|
+
const contracts = [];
|
|
129
|
+
let toolchain;
|
|
130
|
+
let deep;
|
|
131
|
+
let cached = true;
|
|
132
|
+
for (const [i, source] of sources.entries()) {
|
|
133
|
+
const label = sources.length > 1 ? `[${i + 1}/${sources.length}] ` : "";
|
|
134
|
+
const r = await profileOne(source, opts, progress, label);
|
|
135
|
+
contracts.push({ source, costs: r.costs });
|
|
136
|
+
toolchain = r.toolchain;
|
|
137
|
+
if (r.deep)
|
|
138
|
+
deep = new Map([...(deep ?? []), ...r.deep]);
|
|
139
|
+
if (!r.cached)
|
|
140
|
+
cached = false;
|
|
141
|
+
}
|
|
142
|
+
progress.stop();
|
|
143
|
+
return { contracts, toolchain, deep, cached, elapsedMs: performance.now() - started };
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
progress.stop();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Paths recorded in a budget are relative to the budget file. */
|
|
150
|
+
function sourcesFromBudget(budgetPath, recorded) {
|
|
151
|
+
const base = dirname(resolve(budgetPath));
|
|
152
|
+
return recorded.map((s) => (isAbsolute(s) ? s : resolve(base, s)));
|
|
153
|
+
}
|
|
154
|
+
function profileOptions(opts, run) {
|
|
155
|
+
return {
|
|
156
|
+
toolchain: run.toolchain,
|
|
157
|
+
elapsedMs: run.elapsedMs,
|
|
158
|
+
deep: run.deep,
|
|
159
|
+
cached: run.cached,
|
|
160
|
+
calibration: readCalibration(opts.budget),
|
|
161
|
+
showEstimate: opts.estimate,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function runDiff(opts) {
|
|
165
|
+
if (!opts.ref) {
|
|
166
|
+
throw new ProfilerError("diff needs a git ref", " nite-zk diff main src/Main.compact");
|
|
167
|
+
}
|
|
168
|
+
const root = repoRoot();
|
|
169
|
+
const sources = opts.sources.length
|
|
170
|
+
? opts.sources
|
|
171
|
+
: sourcesFromBudget(opts.budget, budgetSources(readBudget(opts.budget)));
|
|
172
|
+
if (sources.length !== 1) {
|
|
173
|
+
throw new ProfilerError("diff compares one contract at a time", `Got ${sources.length} contracts. Name the one to compare:\n nite-zk diff ${opts.ref} src/Main.compact`);
|
|
174
|
+
}
|
|
175
|
+
const source = sources[0];
|
|
176
|
+
const after = await profileAll([source], opts);
|
|
177
|
+
const exported = await materialise(opts.ref);
|
|
178
|
+
try {
|
|
179
|
+
const basePath = pathWithinRef(source, root, exported.dir);
|
|
180
|
+
const before = await profileAll([basePath], opts);
|
|
181
|
+
const result = diffCosts(opts.ref, before.contracts[0].costs, after.contracts[0].costs);
|
|
182
|
+
process.stdout.write(opts.json ? `${diffJson(result)}\n` : formatDiff(result));
|
|
183
|
+
return result.regressed ? 1 : 0;
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
exported.cleanup();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
export async function run(argv) {
|
|
74
190
|
const opts = parseArgs(argv);
|
|
191
|
+
if (opts.noColor || opts.json)
|
|
192
|
+
setColorEnabled(false);
|
|
193
|
+
// Checked before the no-command case, since `nite-zk -v` carries no command
|
|
194
|
+
// and would otherwise fall through to the usage text.
|
|
195
|
+
if (opts.version) {
|
|
196
|
+
process.stdout.write(`nite-zk-profiler ${toolVersion()}\n`);
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
75
199
|
if (opts.help || !opts.command) {
|
|
76
200
|
process.stdout.write(USAGE);
|
|
77
201
|
return opts.command ? 0 : 1;
|
|
78
202
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return 0;
|
|
82
|
-
}
|
|
83
|
-
if (!["profile", "save", "check"].includes(opts.command)) {
|
|
203
|
+
const known = ["profile", "save", "check", "diff", "calibrate"];
|
|
204
|
+
if (!known.includes(opts.command)) {
|
|
84
205
|
process.stderr.write(`Unknown command: ${opts.command}\n\n${USAGE}`);
|
|
85
206
|
return 1;
|
|
86
207
|
}
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
208
|
+
if (opts.command === "calibrate") {
|
|
209
|
+
if (!opts.observedMs || !opts.atK) {
|
|
210
|
+
throw new ProfilerError("calibrate needs an observed proof", "Time one real proof, then record it:\n" +
|
|
211
|
+
" nite-zk calibrate --observed 9000 --at-k 16\n" +
|
|
212
|
+
"where --observed is milliseconds and --at-k is that circuit's k.");
|
|
213
|
+
}
|
|
214
|
+
const calibration = calibrationFrom(opts.observedMs, opts.atK);
|
|
215
|
+
writeCalibration(opts.budget, calibration);
|
|
216
|
+
process.stdout.write(`Calibrated: ${calibration.msPerDomainRow.toFixed(4)} ms per domain row, ` +
|
|
217
|
+
`from ${opts.observedMs}ms at k=${opts.atK}.\n` +
|
|
218
|
+
"Estimates now use this machine's rate. Re-run `nite-zk calibrate` if the prover changes.\n");
|
|
96
219
|
return 0;
|
|
97
220
|
}
|
|
98
|
-
if (opts.command === "
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
221
|
+
if (opts.command === "diff")
|
|
222
|
+
return runDiff(opts);
|
|
223
|
+
if (opts.command !== "check" && opts.sources.length === 0) {
|
|
224
|
+
throw new ProfilerError(`${opts.command} needs at least one source file`, ` nite-zk ${opts.command} src/Main.compact`);
|
|
225
|
+
}
|
|
226
|
+
if (opts.command === "check") {
|
|
227
|
+
// Read the budget first, so a missing one is reported before spending time
|
|
228
|
+
// on a compile, and so it can supply the source paths.
|
|
229
|
+
const budget = readBudget(opts.budget);
|
|
230
|
+
const recorded = budgetSources(budget);
|
|
231
|
+
const sources = opts.sources.length
|
|
232
|
+
? opts.sources
|
|
233
|
+
: sourcesFromBudget(opts.budget, recorded);
|
|
234
|
+
if (sources.length === 0) {
|
|
235
|
+
throw new ProfilerError("check needs a source file", `${opts.budget} does not record which contracts it describes.\n` +
|
|
236
|
+
"Either pass them:\n nite-zk check src/Main.compact\n" +
|
|
237
|
+
"or rewrite the budget so it remembers:\n nite-zk save src/Main.compact");
|
|
238
|
+
}
|
|
239
|
+
const run_ = await profileAll(sources, opts);
|
|
240
|
+
// Compare using the paths as the budget records them.
|
|
241
|
+
const base = dirname(resolve(opts.budget));
|
|
242
|
+
const keyed = run_.contracts.map((c, i) => ({
|
|
243
|
+
source: opts.sources.length ? relative(base, resolve(c.source)) : recorded[i] ?? c.source,
|
|
244
|
+
costs: c.costs,
|
|
245
|
+
}));
|
|
246
|
+
const result = check(keyed, budget, opts.strict);
|
|
247
|
+
process.stdout.write(opts.json ? `${checkJson(result)}\n` : formatCheck(result));
|
|
248
|
+
return result.failed ? 1 : 0;
|
|
249
|
+
}
|
|
250
|
+
const run_ = await profileAll(opts.sources, opts);
|
|
251
|
+
if (opts.command === "profile") {
|
|
252
|
+
const o = profileOptions(opts, run_);
|
|
253
|
+
process.stdout.write(opts.json ? `${profileJson(run_.contracts, o)}\n` : formatProfile(run_.contracts, o));
|
|
104
254
|
return 0;
|
|
105
255
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
256
|
+
// save
|
|
257
|
+
//
|
|
258
|
+
// Record the supported line rather than the exact patch version, so a routine
|
|
259
|
+
// toolchain bump inside 0.31.x does not invalidate the budget. Sources are
|
|
260
|
+
// stored relative to the budget file so the pair stays portable.
|
|
261
|
+
const base = dirname(resolve(opts.budget));
|
|
262
|
+
const line = `${run_.toolchain.version.split(".").slice(0, 2).join(".")}.x`;
|
|
263
|
+
const budget = budgetFrom(run_.contracts.map((c) => ({ source: relative(base, resolve(c.source)), costs: c.costs })), line);
|
|
264
|
+
writeBudget(opts.budget, budget);
|
|
265
|
+
const circuits = run_.contracts.reduce((n, c) => n + c.costs.length, 0);
|
|
266
|
+
process.stdout.write(`Wrote ${opts.budget}: ${run_.contracts.length} contract${run_.contracts.length === 1 ? "" : "s"}, ` +
|
|
267
|
+
`${circuits} circuit${circuits === 1 ? "" : "s"}\n` +
|
|
268
|
+
"Check it in, then run `nite-zk check` in CI.\n");
|
|
269
|
+
return 0;
|
|
109
270
|
}
|
|
110
|
-
export function main(argv) {
|
|
271
|
+
export async function main(argv) {
|
|
111
272
|
try {
|
|
112
|
-
return run(argv);
|
|
273
|
+
return await run(argv);
|
|
113
274
|
}
|
|
114
275
|
catch (e) {
|
|
115
276
|
if (e instanceof ProfilerError) {
|
|
@@ -126,5 +287,5 @@ export function main(argv) {
|
|
|
126
287
|
const invokedDirectly = process.argv[1] !== undefined &&
|
|
127
288
|
/(?:^|[\\/])(?:cli\.(?:ts|js)|nite-zk)$/.test(process.argv[1]);
|
|
128
289
|
if (invokedDirectly) {
|
|
129
|
-
|
|
290
|
+
main(process.argv.slice(2)).then((code) => process.exit(code));
|
|
130
291
|
}
|
package/dist/colors.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal ANSI styling.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately not a dependency. A profiler that prints a table needs eight
|
|
5
|
+
* escape codes, and a zero dependency CLI is easier to trust and audit than one
|
|
6
|
+
* that pulls a package in to add colour.
|
|
7
|
+
*/
|
|
8
|
+
/** Test seam, and lets the CLI force plain output for `--json`. */
|
|
9
|
+
export declare function setColorEnabled(value: boolean): void;
|
|
10
|
+
declare const CODES: {
|
|
11
|
+
readonly reset: 0;
|
|
12
|
+
readonly bold: 1;
|
|
13
|
+
readonly dim: 2;
|
|
14
|
+
readonly red: 31;
|
|
15
|
+
readonly green: 32;
|
|
16
|
+
readonly yellow: 33;
|
|
17
|
+
readonly blue: 34;
|
|
18
|
+
readonly magenta: 35;
|
|
19
|
+
readonly cyan: 36;
|
|
20
|
+
readonly gray: 90;
|
|
21
|
+
};
|
|
22
|
+
export type Style = keyof typeof CODES;
|
|
23
|
+
export declare const bold: (t: string) => string;
|
|
24
|
+
export declare const dim: (t: string) => string;
|
|
25
|
+
export declare const red: (t: string) => string;
|
|
26
|
+
export declare const green: (t: string) => string;
|
|
27
|
+
export declare const yellow: (t: string) => string;
|
|
28
|
+
export declare const cyan: (t: string) => string;
|
|
29
|
+
export declare const gray: (t: string) => string;
|
|
30
|
+
export declare const boldRed: (t: string) => string;
|
|
31
|
+
export declare const boldYellow: (t: string) => string;
|
|
32
|
+
/**
|
|
33
|
+
* Colour a circuit by how much it costs relative to the cheapest one here.
|
|
34
|
+
* The thresholds are deliberately coarse: the point is to make the expensive
|
|
35
|
+
* circuits findable at a glance, not to encode a precise scale.
|
|
36
|
+
*/
|
|
37
|
+
export declare function costColor(relativeCost: number): (t: string) => string;
|
|
38
|
+
/** Width of a string once escape sequences are discounted. */
|
|
39
|
+
export declare function visibleLength(text: string): number;
|
|
40
|
+
export {};
|
package/dist/colors.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal ANSI styling.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately not a dependency. A profiler that prints a table needs eight
|
|
5
|
+
* escape codes, and a zero dependency CLI is easier to trust and audit than one
|
|
6
|
+
* that pulls a package in to add colour.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Honours the NO_COLOR convention, FORCE_COLOR, and whether stdout is a
|
|
10
|
+
* terminal, so piping to a file or a CI log produces clean text.
|
|
11
|
+
*/
|
|
12
|
+
function colorEnabled() {
|
|
13
|
+
if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== "")
|
|
14
|
+
return false;
|
|
15
|
+
if (process.env.FORCE_COLOR !== undefined && process.env.FORCE_COLOR !== "0")
|
|
16
|
+
return true;
|
|
17
|
+
return process.stdout.isTTY === true;
|
|
18
|
+
}
|
|
19
|
+
let enabled = colorEnabled();
|
|
20
|
+
/** Test seam, and lets the CLI force plain output for `--json`. */
|
|
21
|
+
export function setColorEnabled(value) {
|
|
22
|
+
enabled = value;
|
|
23
|
+
}
|
|
24
|
+
const CODES = {
|
|
25
|
+
reset: 0,
|
|
26
|
+
bold: 1,
|
|
27
|
+
dim: 2,
|
|
28
|
+
red: 31,
|
|
29
|
+
green: 32,
|
|
30
|
+
yellow: 33,
|
|
31
|
+
blue: 34,
|
|
32
|
+
magenta: 35,
|
|
33
|
+
cyan: 36,
|
|
34
|
+
gray: 90,
|
|
35
|
+
};
|
|
36
|
+
function wrap(text, ...styles) {
|
|
37
|
+
if (!enabled || styles.length === 0)
|
|
38
|
+
return text;
|
|
39
|
+
const open = styles.map((s) => `[${CODES[s]}m`).join("");
|
|
40
|
+
return `${open}${text}[${CODES.reset}m`;
|
|
41
|
+
}
|
|
42
|
+
export const bold = (t) => wrap(t, "bold");
|
|
43
|
+
export const dim = (t) => wrap(t, "dim");
|
|
44
|
+
export const red = (t) => wrap(t, "red");
|
|
45
|
+
export const green = (t) => wrap(t, "green");
|
|
46
|
+
export const yellow = (t) => wrap(t, "yellow");
|
|
47
|
+
export const cyan = (t) => wrap(t, "cyan");
|
|
48
|
+
export const gray = (t) => wrap(t, "gray");
|
|
49
|
+
export const boldRed = (t) => wrap(t, "bold", "red");
|
|
50
|
+
export const boldYellow = (t) => wrap(t, "bold", "yellow");
|
|
51
|
+
/**
|
|
52
|
+
* Colour a circuit by how much it costs relative to the cheapest one here.
|
|
53
|
+
* The thresholds are deliberately coarse: the point is to make the expensive
|
|
54
|
+
* circuits findable at a glance, not to encode a precise scale.
|
|
55
|
+
*/
|
|
56
|
+
export function costColor(relativeCost) {
|
|
57
|
+
if (relativeCost >= 32)
|
|
58
|
+
return boldRed;
|
|
59
|
+
if (relativeCost >= 8)
|
|
60
|
+
return red;
|
|
61
|
+
if (relativeCost >= 2)
|
|
62
|
+
return yellow;
|
|
63
|
+
return green;
|
|
64
|
+
}
|
|
65
|
+
/** Width of a string once escape sequences are discounted. */
|
|
66
|
+
export function visibleLength(text) {
|
|
67
|
+
// eslint-disable-next-line no-control-regex
|
|
68
|
+
return text.replace(/\[[0-9;]*m/g, "").length;
|
|
69
|
+
}
|
package/dist/compile.d.ts
CHANGED
|
@@ -13,4 +13,4 @@ export interface CompileResult {
|
|
|
13
13
|
* `--skip-zk` is what makes profiling fast enough to sit in an edit loop: it
|
|
14
14
|
* emits the IR and skips key generation, which is the slow part.
|
|
15
15
|
*/
|
|
16
|
-
export declare function compileSkipZk(source: string, toolchain: Toolchain, outDir?: string): CompileResult
|
|
16
|
+
export declare function compileSkipZk(source: string, toolchain: Toolchain, outDir?: string): Promise<CompileResult>;
|
package/dist/compile.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
@@ -9,7 +9,7 @@ import { ProfilerError } from "./errors.js";
|
|
|
9
9
|
* `--skip-zk` is what makes profiling fast enough to sit in an edit loop: it
|
|
10
10
|
* emits the IR and skips key generation, which is the slow part.
|
|
11
11
|
*/
|
|
12
|
-
export function compileSkipZk(source, toolchain, outDir) {
|
|
12
|
+
export async function compileSkipZk(source, toolchain, outDir) {
|
|
13
13
|
const temporary = outDir === undefined;
|
|
14
14
|
const target = temporary
|
|
15
15
|
? mkdtempSync(join(tmpdir(), "nite-zk-"))
|
|
@@ -18,21 +18,30 @@ export function compileSkipZk(source, toolchain, outDir) {
|
|
|
18
18
|
if (toolchain.versionArg)
|
|
19
19
|
args.push(toolchain.versionArg);
|
|
20
20
|
args.push("--skip-zk", resolve(source), target);
|
|
21
|
-
const res = spawnSync("compact", args, { encoding: "utf8" });
|
|
22
21
|
const cleanup = () => {
|
|
23
22
|
if (temporary)
|
|
24
23
|
rmSync(target, { recursive: true, force: true });
|
|
25
24
|
};
|
|
26
|
-
|
|
25
|
+
// Spawned asynchronously rather than with spawnSync so the event loop stays
|
|
26
|
+
// free. A synchronous spawn blocks timers, which freezes the progress
|
|
27
|
+
// display for the whole compile.
|
|
28
|
+
const { status, output, error } = await new Promise((resolvePromise) => {
|
|
29
|
+
const child = spawn("compact", args);
|
|
30
|
+
let out = "";
|
|
31
|
+
child.stdout.on("data", (d) => (out += d));
|
|
32
|
+
child.stderr.on("data", (d) => (out += d));
|
|
33
|
+
child.on("error", (e) => resolvePromise({ status: null, output: out, error: e }));
|
|
34
|
+
child.on("close", (code) => resolvePromise({ status: code, output: out }));
|
|
35
|
+
});
|
|
36
|
+
if (error) {
|
|
27
37
|
cleanup();
|
|
28
|
-
throw new ProfilerError("Could not run `compact compile`", String(
|
|
38
|
+
throw new ProfilerError("Could not run `compact compile`", String(error));
|
|
29
39
|
}
|
|
30
|
-
if (
|
|
40
|
+
if (status !== 0) {
|
|
31
41
|
// The compiler's own diagnostics are better than anything worth inventing
|
|
32
42
|
// here, so they are passed through unchanged.
|
|
33
|
-
const diagnostics = `${res.stdout ?? ""}${res.stderr ?? ""}`.trim();
|
|
34
43
|
cleanup();
|
|
35
|
-
throw new ProfilerError(`Compilation failed for ${source}`,
|
|
44
|
+
throw new ProfilerError(`Compilation failed for ${source}`, output.trim() || `compact exited with status ${status}`);
|
|
36
45
|
}
|
|
37
46
|
return { outDir: target, zkirDir: join(target, "zkir"), cleanup };
|
|
38
47
|
}
|
package/dist/deep.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Measurement } from "./measure.ts";
|
|
2
|
+
import type { Toolchain } from "./toolchain.ts";
|
|
3
|
+
export interface DeepMeasurement extends Measurement {
|
|
4
|
+
/** Wall clock time to generate the proving and verifying keys, in ms. */
|
|
5
|
+
setupMs: number;
|
|
6
|
+
/** Size of the generated prover key, in bytes. */
|
|
7
|
+
proverKeyBytes: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Generate real proving keys and measure how long it takes.
|
|
11
|
+
*
|
|
12
|
+
* This is the expensive counterpart to `mock-compile`. It is opt in because it
|
|
13
|
+
* does the work the fast path deliberately skips, which is minutes rather than
|
|
14
|
+
* seconds on a contract full of high `k` circuits.
|
|
15
|
+
*
|
|
16
|
+
* Two things come out of it that cannot be derived from `k` alone: the actual
|
|
17
|
+
* setup time on this machine, and the prover key size, which is what users end
|
|
18
|
+
* up downloading and holding in memory.
|
|
19
|
+
*/
|
|
20
|
+
export declare function measureDeep(measurements: Measurement[], zkirDir: string, toolchain: Toolchain, onProgress?: (circuit: string, index: number, total: number) => void): Promise<DeepMeasurement[]>;
|
|
21
|
+
/** Human readable byte size. */
|
|
22
|
+
export declare function formatBytes(bytes: number): string;
|
|
23
|
+
/** Human readable duration. */
|
|
24
|
+
export declare function formatMs(ms: number): string;
|
package/dist/deep.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtempSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { ProfilerError } from "./errors.js";
|
|
6
|
+
/**
|
|
7
|
+
* Generate real proving keys and measure how long it takes.
|
|
8
|
+
*
|
|
9
|
+
* This is the expensive counterpart to `mock-compile`. It is opt in because it
|
|
10
|
+
* does the work the fast path deliberately skips, which is minutes rather than
|
|
11
|
+
* seconds on a contract full of high `k` circuits.
|
|
12
|
+
*
|
|
13
|
+
* Two things come out of it that cannot be derived from `k` alone: the actual
|
|
14
|
+
* setup time on this machine, and the prover key size, which is what users end
|
|
15
|
+
* up downloading and holding in memory.
|
|
16
|
+
*/
|
|
17
|
+
export async function measureDeep(measurements, zkirDir, toolchain, onProgress) {
|
|
18
|
+
const workDir = mkdtempSync(join(tmpdir(), "nite-zk-keys-"));
|
|
19
|
+
const results = [];
|
|
20
|
+
try {
|
|
21
|
+
for (const [i, m] of measurements.entries()) {
|
|
22
|
+
onProgress?.(m.circuit, i, measurements.length);
|
|
23
|
+
const irFile = join(zkirDir, `${m.circuit}.zkir`);
|
|
24
|
+
const pk = join(workDir, `${m.circuit}.pk`);
|
|
25
|
+
const vk = join(workDir, `${m.circuit}.vk`);
|
|
26
|
+
const started = performance.now();
|
|
27
|
+
// Async so the progress display keeps animating. Key generation is the
|
|
28
|
+
// longest wait this tool has, so a frozen spinner here reads as a hang.
|
|
29
|
+
const { status, err } = await new Promise((resolvePromise) => {
|
|
30
|
+
const child = spawn(toolchain.zkirPath, ["compile", irFile, pk, vk]);
|
|
31
|
+
let captured = "";
|
|
32
|
+
child.stderr.on("data", (d) => (captured += d));
|
|
33
|
+
child.on("close", (code) => resolvePromise({ status: code, err: captured }));
|
|
34
|
+
});
|
|
35
|
+
const setupMs = performance.now() - started;
|
|
36
|
+
if (status !== 0) {
|
|
37
|
+
throw new ProfilerError(`Key generation failed for ${m.circuit}`, err.trim() || `zkir compile exited with ${status}`);
|
|
38
|
+
}
|
|
39
|
+
results.push({ ...m, setupMs, proverKeyBytes: statSync(pk).size });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
/** Human readable byte size. */
|
|
48
|
+
export function formatBytes(bytes) {
|
|
49
|
+
if (bytes < 1024)
|
|
50
|
+
return `${bytes} B`;
|
|
51
|
+
if (bytes < 1024 * 1024)
|
|
52
|
+
return `${(bytes / 1024).toFixed(0)} KB`;
|
|
53
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
54
|
+
}
|
|
55
|
+
/** Human readable duration. */
|
|
56
|
+
export function formatMs(ms) {
|
|
57
|
+
if (ms < 1000)
|
|
58
|
+
return `${ms.toFixed(0)}ms`;
|
|
59
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
60
|
+
}
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CircuitCost } from "./analyze.ts";
|
|
2
|
+
export interface DiffRow {
|
|
3
|
+
circuit: string;
|
|
4
|
+
/** Absent when the circuit does not exist on the base ref. */
|
|
5
|
+
before?: number;
|
|
6
|
+
/** Absent when the circuit was removed. */
|
|
7
|
+
after?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface DiffResult {
|
|
10
|
+
ref: string;
|
|
11
|
+
rows: DiffRow[];
|
|
12
|
+
/** True when any circuit costs more than it did on the base ref. */
|
|
13
|
+
regressed: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Repository root, so ref paths can be resolved the way git sees them. */
|
|
16
|
+
export declare function repoRoot(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Materialise a ref into a temporary directory.
|
|
19
|
+
*
|
|
20
|
+
* `git archive` is used rather than a worktree or a checkout, because it never
|
|
21
|
+
* touches the working tree or the index. Profiling a branch must not disturb
|
|
22
|
+
* uncommitted work.
|
|
23
|
+
*/
|
|
24
|
+
export declare function materialise(ref: string): Promise<{
|
|
25
|
+
dir: string;
|
|
26
|
+
cleanup: () => void;
|
|
27
|
+
}>;
|
|
28
|
+
/** Where a working tree path lives inside the exported ref. */
|
|
29
|
+
export declare function pathWithinRef(source: string, root: string, refDir: string): string;
|
|
30
|
+
export declare function diffCosts(ref: string, before: CircuitCost[], after: CircuitCost[]): DiffResult;
|