chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
1 parent
719370c8f7
commit
6081e3e325
1743 files changed
+205459
-131704
No files matched your search
@@ -0,0 +1,201 @@
|
||||
/** Cold/full traversal complements the hot-access and replication benchmarks. */
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { setImmediate } from "node:timers/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { track } from "../src/delta/index.ts";
|
||||
|
||||
type Point = { x: number; y: number; pressure: number };
|
||||
type Drawing = { strokes: { id: number; color: string; width: number; points: Point[] }[] };
|
||||
type Mode = "raw" | "proxy" | "delta";
|
||||
const MiB = 1024 * 1024;
|
||||
async function gc(): Promise<void> {
|
||||
if (!global.gc) throw new Error("run with --expose-gc");
|
||||
// WeakRef targets survive their current job. Allow both target collection
|
||||
// and finalizer cleanup in subsequent jobs before sampling retained memory.
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await setImmediate();
|
||||
global.gc();
|
||||
}
|
||||
await setImmediate();
|
||||
}
|
||||
|
||||
function fixture(strokes: number, points: number): Drawing {
|
||||
return {
|
||||
strokes: Array.from({ length: strokes }, (_, id) => ({
|
||||
id,
|
||||
color: id % 2 ? "#405060" : "#102030",
|
||||
width: 2,
|
||||
points: Array.from({ length: points }, (_, p) => ({
|
||||
x: p * 3 + (id % 127),
|
||||
y: p * 7 + (id % 131),
|
||||
pressure: (p % 10) / 10,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
function membrane<T extends object>(input: T): T {
|
||||
const cache = new WeakMap<object, object>();
|
||||
const wrap = (value: object): object => {
|
||||
const existing = cache.get(value);
|
||||
if (existing) return existing;
|
||||
const proxy = new Proxy(value, {
|
||||
get(target, key, receiver) {
|
||||
const child: unknown = Reflect.get(target, key, receiver);
|
||||
return child !== null && typeof child === "object" ? wrap(child) : child;
|
||||
},
|
||||
});
|
||||
cache.set(value, proxy);
|
||||
return proxy;
|
||||
};
|
||||
return wrap(input) as T;
|
||||
}
|
||||
function setup(mode: Mode, input: Drawing): { state: Drawing; finish(): void } {
|
||||
if (mode === "delta") {
|
||||
const tracker = track(input);
|
||||
const change = tracker.beginChange();
|
||||
return {
|
||||
state: change.state,
|
||||
finish() {
|
||||
tracker.adopt(change.prepare());
|
||||
},
|
||||
};
|
||||
}
|
||||
return { state: mode === "proxy" ? membrane(input) : input, finish() {} };
|
||||
}
|
||||
// Keep temporary raw inputs out of the measuring frame.
|
||||
function prepare(mode: Mode, strokes: number, points: number) {
|
||||
return setup(mode, fixture(strokes, points));
|
||||
}
|
||||
function traverse(state: Drawing): number {
|
||||
let sum = 0;
|
||||
const strokes = state.strokes;
|
||||
for (let s = 0; s < strokes.length; s++) {
|
||||
const points = strokes[s].points;
|
||||
for (let p = 0; p < points.length; p++) {
|
||||
const point = points[p];
|
||||
sum += point.x + point.y + point.pressure;
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
function warm(mode: Mode): void {
|
||||
const t = prepare(mode, 10, 10);
|
||||
for (let i = 0; i < 10; i++) traverse(t.state);
|
||||
t.finish();
|
||||
}
|
||||
function sources() {
|
||||
return Object.fromEntries(
|
||||
[
|
||||
"../src/delta/index.ts",
|
||||
"../src/delta/tracker.ts",
|
||||
"../src/delta/draft.ts",
|
||||
"../src/delta/diff.ts",
|
||||
"../src/delta/revision-validator.ts",
|
||||
"../src/delta/apply-immutable-trusted.ts",
|
||||
"./delta-traversal.bench.ts",
|
||||
].map((path) => [
|
||||
path,
|
||||
createHash("sha256")
|
||||
.update(readFileSync(new URL(path, import.meta.url)))
|
||||
.digest("hex"),
|
||||
]),
|
||||
);
|
||||
}
|
||||
async function measure(mode: Mode, strokes: number, points: number, trial: number) {
|
||||
warm(mode);
|
||||
await gc();
|
||||
const empty = process.memoryUsage().heapUsed;
|
||||
const t = prepare(mode, strokes, points);
|
||||
await gc();
|
||||
const ready = process.memoryUsage().heapUsed;
|
||||
const start = performance.now();
|
||||
const checksum = traverse(t.state);
|
||||
const coldMs = performance.now() - start;
|
||||
const afterColdBeforeGc = process.memoryUsage().heapUsed;
|
||||
await gc();
|
||||
const afterCold = process.memoryUsage().heapUsed;
|
||||
const warmMs: number[] = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const begin = performance.now();
|
||||
const check = traverse(t.state);
|
||||
warmMs.push(performance.now() - begin);
|
||||
assert.equal(check, checksum);
|
||||
}
|
||||
let expected = 0;
|
||||
for (let s = 0; s < strokes; s++) {
|
||||
for (let p = 0; p < points; p++) expected += p * 10 + (s % 127) + (s % 131) + (p % 10) / 10;
|
||||
}
|
||||
assert.ok(Math.abs(expected - checksum) < Math.max(1, Math.abs(expected)) * 1e-9);
|
||||
await gc();
|
||||
const retained = process.memoryUsage().heapUsed;
|
||||
assert.equal(t.state.strokes.length, strokes);
|
||||
t.finish();
|
||||
return {
|
||||
mode,
|
||||
strokes,
|
||||
points,
|
||||
trial,
|
||||
coldMs,
|
||||
warmMs,
|
||||
checksum,
|
||||
readyMiB: (ready - empty) / MiB,
|
||||
afterColdMiB: (afterCold - empty) / MiB,
|
||||
retainedMiB: (retained - empty) / MiB,
|
||||
lazyProxyMiB: (afterCold - ready) / MiB,
|
||||
sampledColdPeakMiB: (afterColdBeforeGc - empty) / MiB,
|
||||
maxRssMiB: process.resourceUsage().maxRSS / 1024,
|
||||
sourceHashes: sources(),
|
||||
};
|
||||
}
|
||||
if (process.argv.includes("--worker")) {
|
||||
const c = JSON.parse(process.argv.at(-1)!) as { mode: Mode; strokes: number; points: number; trial: number };
|
||||
console.log(JSON.stringify(await measure(c.mode, c.strokes, c.points, c.trial)));
|
||||
} else {
|
||||
const quick = process.argv.includes("--quick");
|
||||
const outIndex = process.argv.indexOf("--out");
|
||||
const output = outIndex < 0 ? "/tmp/delta-traversal.json" : process.argv[outIndex + 1];
|
||||
const modeIndex = process.argv.indexOf("--modes");
|
||||
const selected = new Set(modeIndex < 0 ? ["raw", "proxy", "delta"] : process.argv[modeIndex + 1].split(","));
|
||||
for (const mode of selected) if (!["raw", "proxy", "delta"].includes(mode)) throw new Error(`unknown mode: ${mode}`);
|
||||
const hashes = sources();
|
||||
const results: Awaited<ReturnType<typeof measure>>[] = [];
|
||||
const failures: unknown[] = [];
|
||||
for (let trial = 0; trial < (quick ? 1 : 3); trial++) {
|
||||
const modes: Mode[] = trial % 2 ? ["delta", "proxy", "raw"] : ["raw", "proxy", "delta"];
|
||||
for (const mode of modes) {
|
||||
if (!selected.has(mode)) continue;
|
||||
assert.deepStrictEqual(sources(), hashes, "source changed during benchmark");
|
||||
const config = { mode, strokes: quick ? 200 : 20000, points: 100, trial };
|
||||
const child = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--expose-gc",
|
||||
"--max-old-space-size=8192",
|
||||
fileURLToPath(import.meta.url),
|
||||
"--worker",
|
||||
JSON.stringify(config),
|
||||
],
|
||||
{ encoding: "utf8", timeout: 180000, maxBuffer: 5 * MiB },
|
||||
);
|
||||
assert.deepStrictEqual(sources(), hashes, "source changed during benchmark");
|
||||
if (child.status !== 0)
|
||||
failures.push({ config, error: String(child.error ?? ""), stderr: child.stderr, stdout: child.stdout });
|
||||
else results.push(JSON.parse(child.stdout));
|
||||
writeFileSync(
|
||||
output,
|
||||
JSON.stringify(
|
||||
{ node: process.version, date: new Date().toISOString(), sourceHashes: hashes, results, failures },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
console.log(`${trial} ${mode}: ${child.status === 0 ? "ok" : "FAILED"}`);
|
||||
}
|
||||
}
|
||||
console.log(`Results: ${output}; failures: ${failures.length}`);
|
||||
if (failures.length) process.exitCode = 1;
|
||||
}
|
||||
Reference in new issue
Block a user