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

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 CHANGED
@@ -45,7 +45,8 @@ $ nite-zk profile Sample.compact
45
45
  insert32 2299 13 8192 256x ~2.6s
46
46
 
47
47
  4 circuits, toolchain 0.31.1, midnight-zkir 2.1.0, 0.9s
48
- est. prove is modelled as 2^k on an uncalibrated default. Run `nite-zk calibrate` to anchor it.
48
+ est. prove models prover work as 2^k, on an uncalibrated default. Run `nite-zk calibrate` to anchor it to your prover.
49
+ It excludes network time, and assumes the proving key for that k is already on the prover.
49
50
  ```
50
51
 
51
52
  `k` is the number that matters. `cost` is `2^k` expressed relative to the cheapest circuit in the contract, so you can see at a glance which circuits dominate your proving budget.
@@ -385,6 +386,7 @@ nite-zk calibrate --observed <ms> --at-k <k>
385
386
  --out <dir> Compile into a specific directory
386
387
  --budget <file> Use a different baseline path
387
388
  --strict check: fail on circuits missing from the budget
389
+ --replace save: overwrite the budget instead of merging
388
390
  --no-color Plain output (NO_COLOR is honoured too)
389
391
  --no-cache Ignore cached measurements for this run
390
392
  +VERSION Pin the Compact toolchain, e.g. +0.31.1
@@ -402,6 +404,23 @@ Wrote zk-budget.json: 2 contracts, 16 circuits
402
404
  `check` then reads every contract back out of the budget, so CI stays one step
403
405
  whether the repository holds one contract or ten.
404
406
 
407
+ Contracts can also be added one at a time. `save` merges into an existing
408
+ budget rather than replacing it, and says what it did:
409
+
410
+ ```text
411
+ $ nite-zk save packages/mint/src/mint.compact
412
+ Wrote /repo/zk-budget.json
413
+ 1 contract, 3 circuits
414
+ added: packages/mint/src/mint.compact
415
+ kept: packages/pool/src/lending.compact
416
+ ```
417
+
418
+ `--replace` writes a fresh file when you do want the old entries gone.
419
+
420
+ The budget is written relative to the working directory, and `save` prints the
421
+ full path it wrote, so running it from the wrong directory is visible
422
+ immediately rather than leaving a file somewhere unexpected.
423
+
405
424
  ### Comparing against a branch
406
425
 
407
426
  ```text
@@ -434,12 +453,15 @@ $ nite-zk calibrate --observed 9000 --at-k 16
434
453
  Calibrated: 0.1373 ms per domain row, from 9000ms at k=16.
435
454
  ```
436
455
 
437
- Two limits worth stating plainly. Proving delegated to a remote proof server is
438
- often dominated by network round trip and server load rather than by circuit
439
- size, and no model of `k` can see that. And the same gate degree effect that
440
- makes key size unpredictable, described below, puts a factor of about two around
441
- any figure derived from `k` alone. Treat it as a ratio you can reason with, not
442
- a stopwatch.
456
+ The figure models **prover work only**. It excludes network round trip and
457
+ server queueing, and it assumes the proving key for that `k` is already on the
458
+ prover rather than being fetched. Wall clock time against a remote proof server
459
+ is frequently dominated by exactly those two things, which no model of `k` can
460
+ see. The same gate degree effect that makes key size unpredictable, described
461
+ below, puts a factor of about two around any figure derived from `k` alone.
462
+
463
+ So it answers "how much work is this circuit" rather than "how long will my user
464
+ wait". The first is what you control while writing Compact.
443
465
 
444
466
  `save` records which contracts the budget describes, so `check` needs no
445
467
  arguments afterwards. That is what makes it a one line CI step. It also records
package/dist/budget.d.ts CHANGED
@@ -44,6 +44,22 @@ export interface ContractCosts {
44
44
  }
45
45
  /** Build a budget granting every circuit exactly what it currently costs. */
46
46
  export declare function budgetFrom(contracts: ContractCosts[], toolchainLine: string): Budget;
47
+ export interface MergeSummary {
48
+ added: string[];
49
+ updated: string[];
50
+ kept: string[];
51
+ }
52
+ /**
53
+ * Fold a new save into an existing budget.
54
+ *
55
+ * Saving one contract must not silently discard the others. A monorepo is
56
+ * saved a contract at a time, and without merging each save wipes the previous
57
+ * one, which looks like it worked and quietly loses the ceilings you committed.
58
+ */
59
+ export declare function mergeBudgets(existing: Budget, incoming: Budget): {
60
+ budget: Budget;
61
+ summary: MergeSummary;
62
+ };
47
63
  export declare function writeBudget(path: string, budget: Budget): void;
48
64
  export declare function readBudget(path: string): Budget;
49
65
  /** Every contract the budget describes. */
package/dist/budget.js CHANGED
@@ -24,6 +24,33 @@ export function budgetFrom(contracts, toolchainLine) {
24
24
  contracts: out,
25
25
  };
26
26
  }
27
+ /**
28
+ * Fold a new save into an existing budget.
29
+ *
30
+ * Saving one contract must not silently discard the others. A monorepo is
31
+ * saved a contract at a time, and without merging each save wipes the previous
32
+ * one, which looks like it worked and quietly loses the ceilings you committed.
33
+ */
34
+ export function mergeBudgets(existing, incoming) {
35
+ const contracts = { ...existing.contracts };
36
+ const summary = { added: [], updated: [], kept: [] };
37
+ for (const [source, contract] of Object.entries(incoming.contracts)) {
38
+ (source in contracts ? summary.updated : summary.added).push(source);
39
+ contracts[source] = contract;
40
+ }
41
+ for (const source of Object.keys(existing.contracts)) {
42
+ if (!(source in incoming.contracts) && source.length > 0)
43
+ summary.kept.push(source);
44
+ }
45
+ return {
46
+ budget: { ...incoming, contracts },
47
+ summary: {
48
+ added: summary.added.sort(),
49
+ updated: summary.updated.sort(),
50
+ kept: summary.kept.sort(),
51
+ },
52
+ };
53
+ }
27
54
  export function writeBudget(path, budget) {
28
55
  writeFileSync(path, `${JSON.stringify(budget, null, 2)}\n`, "utf8");
29
56
  }
package/dist/cli.d.ts CHANGED
@@ -5,6 +5,7 @@ interface Options {
5
5
  ref?: string;
6
6
  json: boolean;
7
7
  strict: boolean;
8
+ replace: boolean;
8
9
  deep: boolean;
9
10
  estimate: boolean;
10
11
  noColor: boolean;
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { dirname, isAbsolute, relative, resolve } from "node:path";
3
3
  import { analyze } from "./analyze.js";
4
- import { DEFAULT_BUDGET_PATH, budgetFrom, budgetSources, check, readBudget, writeBudget, } from "./budget.js";
4
+ import { DEFAULT_BUDGET_PATH, budgetFrom, budgetSources, check, mergeBudgets, readBudget, writeBudget, } from "./budget.js";
5
5
  import { cacheKey, readCache, writeCache } from "./cache.js";
6
6
  import { setColorEnabled } from "./colors.js";
7
7
  import { compileSkipZk } from "./compile.js";
@@ -34,6 +34,7 @@ Options:
34
34
  --out <dir> Compile into a specific directory (kept)
35
35
  --budget <file> Budget path (default: ${DEFAULT_BUDGET_PATH})
36
36
  --strict check: fail on circuits missing from the budget
37
+ --replace save: overwrite the budget instead of merging
37
38
  --no-color Plain output (also honours NO_COLOR)
38
39
  --no-cache Ignore cached measurements for this run
39
40
  +VERSION Pin the Compact toolchain, e.g. +0.31.1
@@ -47,6 +48,7 @@ export function parseArgs(argv) {
47
48
  sources: [],
48
49
  json: false,
49
50
  strict: false,
51
+ replace: false,
50
52
  deep: false,
51
53
  estimate: true,
52
54
  noColor: false,
@@ -61,6 +63,8 @@ export function parseArgs(argv) {
61
63
  opts.json = true;
62
64
  else if (arg === "--strict")
63
65
  opts.strict = true;
66
+ else if (arg === "--replace")
67
+ opts.replace = true;
64
68
  else if (arg === "--deep")
65
69
  opts.deep = true;
66
70
  else if (arg === "--estimate")
@@ -262,12 +266,36 @@ export async function run(argv) {
262
266
  // stored relative to the budget file so the pair stays portable.
263
267
  const base = dirname(resolve(opts.budget));
264
268
  const line = `${run_.toolchain.version.split(".").slice(0, 2).join(".")}.x`;
265
- const budget = budgetFrom(run_.contracts.map((c) => ({ source: relative(base, resolve(c.source)), costs: c.costs })), line);
266
- writeBudget(opts.budget, budget);
269
+ const fresh = budgetFrom(run_.contracts.map((c) => ({ source: relative(base, resolve(c.source)), costs: c.costs })), line);
270
+ // Merge rather than overwrite. Saving one contract at a time is the normal
271
+ // way to set up a monorepo, and replacing the file each time would discard
272
+ // ceilings that are already committed, silently and while reporting success.
273
+ let existing;
274
+ if (!opts.replace) {
275
+ try {
276
+ existing = readBudget(opts.budget);
277
+ }
278
+ catch {
279
+ existing = undefined;
280
+ }
281
+ }
282
+ const merged = existing ? mergeBudgets(existing, fresh) : { budget: fresh, summary: undefined };
283
+ writeBudget(opts.budget, merged.budget);
267
284
  const circuits = run_.contracts.reduce((n, c) => n + c.costs.length, 0);
268
- process.stdout.write(`Wrote ${opts.budget}: ${run_.contracts.length} contract${run_.contracts.length === 1 ? "" : "s"}, ` +
269
- `${circuits} circuit${circuits === 1 ? "" : "s"}\n` +
270
- "Check it in, then run `nite-zk check` in CI.\n");
285
+ const written = resolve(opts.budget);
286
+ process.stdout.write(`Wrote ${written}\n` +
287
+ ` ${run_.contracts.length} contract${run_.contracts.length === 1 ? "" : "s"}, ` +
288
+ `${circuits} circuit${circuits === 1 ? "" : "s"}\n`);
289
+ const s_ = merged.summary;
290
+ if (s_) {
291
+ if (s_.added.length)
292
+ process.stdout.write(` added: ${s_.added.join(", ")}\n`);
293
+ if (s_.updated.length)
294
+ process.stdout.write(` updated: ${s_.updated.join(", ")}\n`);
295
+ if (s_.kept.length)
296
+ process.stdout.write(` kept: ${s_.kept.join(", ")}\n`);
297
+ }
298
+ process.stdout.write("Check it in, then run `nite-zk check` in CI.\n");
271
299
  return 0;
272
300
  }
273
301
  export async function main(argv) {
package/dist/report.js CHANGED
@@ -88,8 +88,11 @@ export function formatProfile(contracts, opts) {
88
88
  }
89
89
  if (opts.showEstimate) {
90
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."));
91
+ ? ` est. prove models prover work as 2^k, calibrated from an observed ` +
92
+ `${formatDuration(opts.calibration.observedMs)} proof at k=${opts.calibration.observedK}.`
93
+ : " est. prove models prover work as 2^k, on an uncalibrated default. " +
94
+ "Run `nite-zk calibrate` to anchor it to your prover."));
95
+ lines.push(gray(" It excludes network time, and assumes the proving key for that k is already on the prover."));
93
96
  }
94
97
  lines.push("");
95
98
  return lines.join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nite-framework/nite-zk-profiler",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "See what a Compact circuit costs to prove, without generating proving keys",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,7 +23,8 @@
23
23
  "zero-knowledge",
24
24
  "profiler",
25
25
  "proving-cost",
26
- "nite-zk-profiler"
26
+ "nite-zk-profiler",
27
+ "@nite-framework/nite-zk-profiler"
27
28
  ],
28
29
  "license": "MIT",
29
30
  "repository": {