@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/diff.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, relative, resolve } from "node:path";
|
|
5
|
+
import { ProfilerError } from "./errors.js";
|
|
6
|
+
function git(args, cwd) {
|
|
7
|
+
const res = spawnSync("git", args, { encoding: "utf8", cwd });
|
|
8
|
+
return { status: res.status, out: `${res.stdout ?? ""}`.trim(), err: `${res.stderr ?? ""}`.trim() };
|
|
9
|
+
}
|
|
10
|
+
/** Repository root, so ref paths can be resolved the way git sees them. */
|
|
11
|
+
export function repoRoot() {
|
|
12
|
+
const res = git(["rev-parse", "--show-toplevel"]);
|
|
13
|
+
if (res.status !== 0) {
|
|
14
|
+
throw new ProfilerError("Not a git repository", "`nite-zk diff` compares against a git ref, so it needs to run inside a repository.");
|
|
15
|
+
}
|
|
16
|
+
return res.out;
|
|
17
|
+
}
|
|
18
|
+
function assertRefExists(ref) {
|
|
19
|
+
if (git(["rev-parse", "--verify", `${ref}^{commit}`]).status !== 0) {
|
|
20
|
+
throw new ProfilerError(`Unknown git ref: ${ref}`, "Pass a branch, tag or commit that exists, for example `nite-zk diff main`.");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Materialise a ref into a temporary directory.
|
|
25
|
+
*
|
|
26
|
+
* `git archive` is used rather than a worktree or a checkout, because it never
|
|
27
|
+
* touches the working tree or the index. Profiling a branch must not disturb
|
|
28
|
+
* uncommitted work.
|
|
29
|
+
*/
|
|
30
|
+
export async function materialise(ref) {
|
|
31
|
+
assertRefExists(ref);
|
|
32
|
+
const dir = mkdtempSync(join(tmpdir(), "nite-zk-diff-"));
|
|
33
|
+
const cleanup = () => rmSync(dir, { recursive: true, force: true });
|
|
34
|
+
const status = await new Promise((resolvePromise) => {
|
|
35
|
+
const archive = spawn("git", ["archive", "--format=tar", ref]);
|
|
36
|
+
const untar = spawn("tar", ["-x", "-C", dir]);
|
|
37
|
+
archive.stdout.pipe(untar.stdin);
|
|
38
|
+
let failed = "";
|
|
39
|
+
archive.stderr.on("data", (d) => (failed += d));
|
|
40
|
+
untar.on("close", (code) => resolvePromise(code));
|
|
41
|
+
archive.on("error", () => resolvePromise(null));
|
|
42
|
+
});
|
|
43
|
+
if (status !== 0) {
|
|
44
|
+
cleanup();
|
|
45
|
+
throw new ProfilerError(`Could not export ${ref}`, "git archive failed. Check the ref is reachable from this repository.");
|
|
46
|
+
}
|
|
47
|
+
return { dir, cleanup };
|
|
48
|
+
}
|
|
49
|
+
/** Where a working tree path lives inside the exported ref. */
|
|
50
|
+
export function pathWithinRef(source, root, refDir) {
|
|
51
|
+
return join(refDir, relative(root, resolve(source)));
|
|
52
|
+
}
|
|
53
|
+
export function diffCosts(ref, before, after) {
|
|
54
|
+
const beforeK = new Map(before.map((c) => [c.circuit, c.k]));
|
|
55
|
+
const afterK = new Map(after.map((c) => [c.circuit, c.k]));
|
|
56
|
+
const names = [...new Set([...beforeK.keys(), ...afterK.keys()])].sort();
|
|
57
|
+
const rows = names.map((circuit) => ({
|
|
58
|
+
circuit,
|
|
59
|
+
before: beforeK.get(circuit),
|
|
60
|
+
after: afterK.get(circuit),
|
|
61
|
+
}));
|
|
62
|
+
const regressed = rows.some((r) => r.before !== undefined && r.after !== undefined && r.after > r.before);
|
|
63
|
+
return { ref, rows, regressed };
|
|
64
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proving cost model.
|
|
3
|
+
*
|
|
4
|
+
* A Halo2 proof is dominated by FFTs and multi scalar multiplications over the
|
|
5
|
+
* full 2^k domain, so the work is proportional to 2^k up to a log factor that
|
|
6
|
+
* is small across the range circuits actually occupy. That gives
|
|
7
|
+
*
|
|
8
|
+
* time = msPerDomainRow * 2^k
|
|
9
|
+
*
|
|
10
|
+
* with a single machine dependent constant.
|
|
11
|
+
*
|
|
12
|
+
* The default constant is anchored on measured key generation, which performs
|
|
13
|
+
* comparable domain work on the same machine: 10547ms at k=15, so
|
|
14
|
+
* 10547 / 2^15 = 0.322 ms per domain row. Proving is not key generation, so
|
|
15
|
+
* treat the default as an order of magnitude, and calibrate to make it real.
|
|
16
|
+
*/
|
|
17
|
+
export declare const DEFAULT_MS_PER_DOMAIN_ROW = 0.322;
|
|
18
|
+
export interface Calibration {
|
|
19
|
+
msPerDomainRow: number;
|
|
20
|
+
/** What the number came from, so a stale calibration can be recognised. */
|
|
21
|
+
observedMs: number;
|
|
22
|
+
observedK: number;
|
|
23
|
+
recordedAt: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ProvingEstimate {
|
|
26
|
+
ms: number;
|
|
27
|
+
calibrated: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function estimateProvingMs(k: number, calibration?: Calibration): ProvingEstimate;
|
|
30
|
+
/** Turn one observed proof into the machine constant. */
|
|
31
|
+
export declare function calibrationFrom(observedMs: number, observedK: number): Calibration;
|
|
32
|
+
export declare function calibrationPath(budgetPath: string): string;
|
|
33
|
+
export declare function readCalibration(budgetPath: string): Calibration | undefined;
|
|
34
|
+
export declare function writeCalibration(budgetPath: string, calibration: Calibration): void;
|
|
35
|
+
/** Human readable duration, for estimates that span milliseconds to minutes. */
|
|
36
|
+
export declare function formatDuration(ms: number): string;
|
|
37
|
+
/** Human readable byte size. */
|
|
38
|
+
export declare function formatBytes(bytes: number): string;
|
package/dist/estimate.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
/*
|
|
4
|
+
* There is deliberately no key size prediction here.
|
|
5
|
+
*
|
|
6
|
+
* Key size looked predictable from k: three unrelated circuits at k=13 agreed
|
|
7
|
+
* within 0.11%, and a synthetic k=15 circuit matched a production one to 0.08%.
|
|
8
|
+
* It does not hold. At k=16 a production circuit produced 19,524,757 bytes and
|
|
9
|
+
* a synthetic one 38,513,181, a factor of two apart, with identical verifier
|
|
10
|
+
* key sizes.
|
|
11
|
+
*
|
|
12
|
+
* The likely cause is the extended evaluation domain, which Halo2 sizes by
|
|
13
|
+
* maximum gate degree. That is not reported by mock-compile, so from k alone
|
|
14
|
+
* there is no way to tell which case a circuit is in. A figure that is exact
|
|
15
|
+
* at one k and 2x wrong at the next is worse than no figure, so key size comes
|
|
16
|
+
* only from --deep, where it is measured.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Proving cost model.
|
|
20
|
+
*
|
|
21
|
+
* A Halo2 proof is dominated by FFTs and multi scalar multiplications over the
|
|
22
|
+
* full 2^k domain, so the work is proportional to 2^k up to a log factor that
|
|
23
|
+
* is small across the range circuits actually occupy. That gives
|
|
24
|
+
*
|
|
25
|
+
* time = msPerDomainRow * 2^k
|
|
26
|
+
*
|
|
27
|
+
* with a single machine dependent constant.
|
|
28
|
+
*
|
|
29
|
+
* The default constant is anchored on measured key generation, which performs
|
|
30
|
+
* comparable domain work on the same machine: 10547ms at k=15, so
|
|
31
|
+
* 10547 / 2^15 = 0.322 ms per domain row. Proving is not key generation, so
|
|
32
|
+
* treat the default as an order of magnitude, and calibrate to make it real.
|
|
33
|
+
*/
|
|
34
|
+
export const DEFAULT_MS_PER_DOMAIN_ROW = 0.322;
|
|
35
|
+
export function estimateProvingMs(k, calibration) {
|
|
36
|
+
const rate = calibration?.msPerDomainRow ?? DEFAULT_MS_PER_DOMAIN_ROW;
|
|
37
|
+
return { ms: rate * 2 ** k, calibrated: calibration !== undefined };
|
|
38
|
+
}
|
|
39
|
+
/** Turn one observed proof into the machine constant. */
|
|
40
|
+
export function calibrationFrom(observedMs, observedK) {
|
|
41
|
+
return {
|
|
42
|
+
msPerDomainRow: observedMs / 2 ** observedK,
|
|
43
|
+
observedMs,
|
|
44
|
+
observedK,
|
|
45
|
+
recordedAt: new Date().toISOString(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function calibrationPath(budgetPath) {
|
|
49
|
+
return join(dirname(budgetPath), ".nite-zk-calibration.json");
|
|
50
|
+
}
|
|
51
|
+
export function readCalibration(budgetPath) {
|
|
52
|
+
const file = calibrationPath(budgetPath);
|
|
53
|
+
if (!existsSync(file))
|
|
54
|
+
return undefined;
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
57
|
+
return typeof parsed.msPerDomainRow === "number" && parsed.msPerDomainRow > 0
|
|
58
|
+
? parsed
|
|
59
|
+
: undefined;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function writeCalibration(budgetPath, calibration) {
|
|
66
|
+
const file = calibrationPath(budgetPath);
|
|
67
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
68
|
+
writeFileSync(file, `${JSON.stringify(calibration, null, 2)}\n`, "utf8");
|
|
69
|
+
}
|
|
70
|
+
/** Human readable duration, for estimates that span milliseconds to minutes. */
|
|
71
|
+
export function formatDuration(ms) {
|
|
72
|
+
if (ms < 1000)
|
|
73
|
+
return `${Math.round(ms)}ms`;
|
|
74
|
+
if (ms < 60_000)
|
|
75
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
76
|
+
return `${Math.floor(ms / 60_000)}m${Math.round((ms % 60_000) / 1000)}s`;
|
|
77
|
+
}
|
|
78
|
+
/** Human readable byte size. */
|
|
79
|
+
export function formatBytes(bytes) {
|
|
80
|
+
if (bytes < 1024)
|
|
81
|
+
return `${bytes} B`;
|
|
82
|
+
if (bytes < 1024 * 1024)
|
|
83
|
+
return `${(bytes / 1024).toFixed(0)} KB`;
|
|
84
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
85
|
+
}
|
package/dist/measure.d.ts
CHANGED
|
@@ -13,11 +13,20 @@ export interface Measurement {
|
|
|
13
13
|
* report that reads as a successful one.
|
|
14
14
|
*/
|
|
15
15
|
export declare function parseReport(text: string): Measurement[];
|
|
16
|
+
/** Parse the single file form, whose report names the path rather than the circuit. */
|
|
17
|
+
export declare function parseSingle(text: string, circuit: string): Measurement;
|
|
18
|
+
/** Measure every circuit in a single sequential `zkir` invocation. */
|
|
19
|
+
export declare function measure(zkirDir: string, toolchain: Toolchain, source: string): Measurement[];
|
|
16
20
|
/**
|
|
17
|
-
* Measure every circuit
|
|
21
|
+
* Measure every circuit, several at a time.
|
|
22
|
+
*
|
|
23
|
+
* `mock-compile-many` walks the directory sequentially, and its cost is
|
|
24
|
+
* dominated by the largest circuits: on a nine circuit contract it spent 17s,
|
|
25
|
+
* of which one circuit accounted for 5.5s. Running the single file form
|
|
26
|
+
* concurrently cuts that to roughly 9s on eight cores, and each process stays
|
|
27
|
+
* around 58 MB, so the concurrency is bounded by cores rather than memory.
|
|
18
28
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* would leave circuit names to be recovered from filenames.
|
|
29
|
+
* The single file report names the path instead of the circuit, but the file is
|
|
30
|
+
* `<circuit>.zkir`, so the name comes from the filename.
|
|
22
31
|
*/
|
|
23
|
-
export declare function
|
|
32
|
+
export declare function measureParallel(zkirDir: string, toolchain: Toolchain, source: string, onProgress?: (done: number, total: number) => void, concurrency?: number): Promise<Measurement[]>;
|
package/dist/measure.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
3
|
+
import { availableParallelism } from "node:os";
|
|
4
|
+
import { basename, join } from "node:path";
|
|
3
5
|
import { NoProvableCircuitsError, ProfilerError } from "./errors.js";
|
|
4
6
|
/** ` circuit "name" (k=9, rows=305)` */
|
|
5
7
|
const CIRCUIT_LINE = /^\s*circuit\s+"([^"]+)"\s+\(k=(\d+),\s*rows=(\d+)\)\s*$/;
|
|
6
8
|
/** `Mock compiling 2 circuits:` */
|
|
7
9
|
const HEADER_LINE = /^Mock compiling (\d+) circuits?:/m;
|
|
10
|
+
/** Single file form: `Mock compiling circuit "/abs/path.zkir" (k=9, rows=305)` */
|
|
11
|
+
const SINGLE_LINE = /\(k=(\d+),\s*rows=(\d+)\)/;
|
|
8
12
|
/**
|
|
9
13
|
* Parse a `zkir mock-compile-many` report.
|
|
10
14
|
*
|
|
@@ -36,17 +40,25 @@ export function parseReport(text) {
|
|
|
36
40
|
}
|
|
37
41
|
return measurements;
|
|
38
42
|
}
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
* would leave circuit names to be recovered from filenames.
|
|
45
|
-
*/
|
|
46
|
-
export function measure(zkirDir, toolchain, source) {
|
|
47
|
-
if (!existsSync(zkirDir)) {
|
|
48
|
-
throw new NoProvableCircuitsError(source);
|
|
43
|
+
/** Parse the single file form, whose report names the path rather than the circuit. */
|
|
44
|
+
export function parseSingle(text, circuit) {
|
|
45
|
+
const m = text.match(SINGLE_LINE);
|
|
46
|
+
if (!m) {
|
|
47
|
+
throw new ProfilerError(`Could not parse the zkir report for ${circuit}`, text.trim() || "(no output)");
|
|
49
48
|
}
|
|
49
|
+
return { circuit, k: Number(m[1]), rows: Number(m[2]) };
|
|
50
|
+
}
|
|
51
|
+
function zkirFiles(zkirDir, source) {
|
|
52
|
+
if (!existsSync(zkirDir))
|
|
53
|
+
throw new NoProvableCircuitsError(source);
|
|
54
|
+
const files = readdirSync(zkirDir).filter((f) => f.endsWith(".zkir"));
|
|
55
|
+
if (files.length === 0)
|
|
56
|
+
throw new NoProvableCircuitsError(source);
|
|
57
|
+
return files;
|
|
58
|
+
}
|
|
59
|
+
/** Measure every circuit in a single sequential `zkir` invocation. */
|
|
60
|
+
export function measure(zkirDir, toolchain, source) {
|
|
61
|
+
zkirFiles(zkirDir, source);
|
|
50
62
|
const res = spawnSync(toolchain.zkirPath, ["mock-compile-many", zkirDir], {
|
|
51
63
|
encoding: "utf8",
|
|
52
64
|
});
|
|
@@ -59,9 +71,52 @@ export function measure(zkirDir, toolchain, source) {
|
|
|
59
71
|
if (res.status !== 0) {
|
|
60
72
|
throw new ProfilerError("zkir mock-compile-many failed", output.trim() || `zkir exited with status ${res.status}`);
|
|
61
73
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
return parseReport(output).sort((a, b) => a.circuit.localeCompare(b.circuit));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Measure every circuit, several at a time.
|
|
78
|
+
*
|
|
79
|
+
* `mock-compile-many` walks the directory sequentially, and its cost is
|
|
80
|
+
* dominated by the largest circuits: on a nine circuit contract it spent 17s,
|
|
81
|
+
* of which one circuit accounted for 5.5s. Running the single file form
|
|
82
|
+
* concurrently cuts that to roughly 9s on eight cores, and each process stays
|
|
83
|
+
* around 58 MB, so the concurrency is bounded by cores rather than memory.
|
|
84
|
+
*
|
|
85
|
+
* The single file report names the path instead of the circuit, but the file is
|
|
86
|
+
* `<circuit>.zkir`, so the name comes from the filename.
|
|
87
|
+
*/
|
|
88
|
+
export async function measureParallel(zkirDir, toolchain, source, onProgress, concurrency = Math.max(1, availableParallelism())) {
|
|
89
|
+
const files = zkirFiles(zkirDir, source);
|
|
90
|
+
const results = [];
|
|
91
|
+
let done = 0;
|
|
92
|
+
let next = 0;
|
|
93
|
+
onProgress?.(0, files.length);
|
|
94
|
+
const runOne = (file) => new Promise((resolvePromise, reject) => {
|
|
95
|
+
const child = spawn(toolchain.zkirPath, ["mock-compile", join(zkirDir, file)]);
|
|
96
|
+
let out = "";
|
|
97
|
+
child.stderr.on("data", (d) => (out += d));
|
|
98
|
+
child.stdout.on("data", (d) => (out += d));
|
|
99
|
+
child.on("error", (e) => reject(new ProfilerError(`Could not run ${toolchain.zkirPath}`, String(e))));
|
|
100
|
+
child.on("close", (code) => {
|
|
101
|
+
if (code !== 0) {
|
|
102
|
+
reject(new ProfilerError(`zkir mock-compile failed for ${basename(file, ".zkir")}`, out.trim() || `zkir exited with status ${code}`));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const m = parseSingle(out, basename(file, ".zkir"));
|
|
107
|
+
onProgress?.(++done, files.length);
|
|
108
|
+
resolvePromise(m);
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
reject(e);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
const worker = async () => {
|
|
116
|
+
while (next < files.length) {
|
|
117
|
+
results.push(await runOne(files[next++]));
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, files.length) }, () => worker()));
|
|
121
|
+
return results.sort((a, b) => a.circuit.localeCompare(b.circuit));
|
|
67
122
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A spinner for the wait, which on a large contract is ten seconds or more.
|
|
3
|
+
*
|
|
4
|
+
* Everything goes to stderr so that `nite-zk profile --json > out.json` stays
|
|
5
|
+
* clean, and it disables itself when stderr is not a terminal so CI logs do not
|
|
6
|
+
* fill with redraw frames.
|
|
7
|
+
*/
|
|
8
|
+
export declare class Progress {
|
|
9
|
+
private timer;
|
|
10
|
+
private frame;
|
|
11
|
+
private label;
|
|
12
|
+
private readonly active;
|
|
13
|
+
constructor(enabled?: boolean);
|
|
14
|
+
start(label: string): void;
|
|
15
|
+
update(label: string): void;
|
|
16
|
+
private render;
|
|
17
|
+
/** Clear the line so the report starts on clean output. */
|
|
18
|
+
stop(): void;
|
|
19
|
+
}
|
package/dist/progress.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { dim } from "./colors.js";
|
|
2
|
+
const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
3
|
+
/**
|
|
4
|
+
* A spinner for the wait, which on a large contract is ten seconds or more.
|
|
5
|
+
*
|
|
6
|
+
* Everything goes to stderr so that `nite-zk profile --json > out.json` stays
|
|
7
|
+
* clean, and it disables itself when stderr is not a terminal so CI logs do not
|
|
8
|
+
* fill with redraw frames.
|
|
9
|
+
*/
|
|
10
|
+
export class Progress {
|
|
11
|
+
timer;
|
|
12
|
+
frame = 0;
|
|
13
|
+
label = "";
|
|
14
|
+
active;
|
|
15
|
+
constructor(enabled = true) {
|
|
16
|
+
this.active =
|
|
17
|
+
enabled &&
|
|
18
|
+
process.stderr.isTTY === true &&
|
|
19
|
+
(process.env.NO_COLOR === undefined || process.env.NO_COLOR === "");
|
|
20
|
+
}
|
|
21
|
+
start(label) {
|
|
22
|
+
this.label = label;
|
|
23
|
+
if (!this.active)
|
|
24
|
+
return;
|
|
25
|
+
// Draw once up front, so the first phase is visible immediately rather than
|
|
26
|
+
// after the first tick.
|
|
27
|
+
this.render();
|
|
28
|
+
this.timer = setInterval(() => this.render(), 80);
|
|
29
|
+
this.timer.unref?.();
|
|
30
|
+
}
|
|
31
|
+
update(label) {
|
|
32
|
+
this.label = label;
|
|
33
|
+
// Redraw straight away so a phase change shows even if the next tick is
|
|
34
|
+
// still 80ms out.
|
|
35
|
+
this.render();
|
|
36
|
+
}
|
|
37
|
+
render() {
|
|
38
|
+
if (!this.active)
|
|
39
|
+
return;
|
|
40
|
+
const spin = FRAMES[this.frame++ % FRAMES.length];
|
|
41
|
+
process.stderr.write(`\r\x1b[K${dim(`${spin} ${this.label}`)}`);
|
|
42
|
+
}
|
|
43
|
+
/** Clear the line so the report starts on clean output. */
|
|
44
|
+
stop() {
|
|
45
|
+
if (this.timer)
|
|
46
|
+
clearInterval(this.timer);
|
|
47
|
+
this.timer = undefined;
|
|
48
|
+
if (this.active)
|
|
49
|
+
process.stderr.write("\r\x1b[K");
|
|
50
|
+
}
|
|
51
|
+
}
|
package/dist/report.d.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type
|
|
1
|
+
import type { CheckResult, ContractCosts } from "./budget.ts";
|
|
2
|
+
import { type DeepMeasurement } from "./deep.ts";
|
|
3
|
+
import type { DiffResult } from "./diff.ts";
|
|
4
|
+
import { type Calibration } from "./estimate.ts";
|
|
3
5
|
import type { Toolchain } from "./toolchain.ts";
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
|
|
6
|
+
export type DeepByCircuit = Map<string, DeepMeasurement>;
|
|
7
|
+
export interface ProfileOptions {
|
|
8
|
+
toolchain: Toolchain;
|
|
9
|
+
elapsedMs: number;
|
|
10
|
+
deep?: DeepByCircuit;
|
|
11
|
+
cached?: boolean;
|
|
12
|
+
calibration?: Calibration;
|
|
13
|
+
showEstimate?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Per circuit cost table, one block per contract. */
|
|
16
|
+
export declare function formatProfile(contracts: ContractCosts[], opts: ProfileOptions): string;
|
|
17
|
+
/** Budget comparison, grouped by contract when there is more than one. */
|
|
7
18
|
export declare function formatCheck(result: CheckResult): string;
|
|
8
|
-
|
|
19
|
+
/** Comparison against a git ref. */
|
|
20
|
+
export declare function formatDiff(result: DiffResult): string;
|
|
21
|
+
export declare function profileJson(contracts: ContractCosts[], opts: ProfileOptions): string;
|
|
9
22
|
export declare function checkJson(result: CheckResult): string;
|
|
23
|
+
export declare function diffJson(result: DiffResult): string;
|