GitNexus/gitnexus/bench/v8-sidecar/measure.mjs
Gergő Magyar 72edf40087
perf(store): V8 sidecars plus hardlinked ParsedFile restore (#3099)
* perf(store): add best-effort V8 sidecars beside canonical JSON caches

Warm ParsedFile and parse-cache loads skip JSON.parse when a sidecar is present. JSON remains authoritative: envelope validation plus v8.deserialize decide the hit, and any failure falls back without reparsing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(store): require generation bind or sidecar drop before cache overwrite

A same-length JSON rewrite could accept a leftover V8 sidecar if both
generation rotation and unlink failed. Refuse the new generation unless
at least one of those invalidations succeeds; skip publishing a sidecar
when only the drop succeeded.

detect_changes --scope all: 7 files, risk low, no affected processes.
tsc --noEmit clean; 115/115 relevant unit tests; cache-related
integration tests pass. parse-impl-env-reads worker-ready timeout is
pre-existing (same 5 failures with this change set stashed). ESLint 0
errors; remaining warnings are pre-existing and not on changed lines.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(autofix): apply prettier + eslint fixes via /autofix command

* refactor(store): share V8 overwrite invalidation across persist paths

The bind-or-drop gate lived in five writers. One helper keeps the
protocol in a single place and lets bind/drop run together on the
async path.

detect_changes --scope all: 3 files, risk low, no affected processes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(store): hardlink durable ParsedFile shards into the run store

Warm restore of parsedfile-cache into parsedfile-store now publishes all four shard files via fs.link, falling back to copy-into-tmp + rename so a leftover dest hardlink can never be written through. JSON remains the canonical cache; V8 sidecars ride the same path.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(store): load immutable V8 shards in place, drop JSON fallback

Warm analyze was still paying JSON.parse plus a restore copy. One .v8 envelope per shard and SCHEMA_BUMP 81 make a miss re-extract instead of serving a stale JSON twin.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(store): validate durable V8 warm-cache restores

Reject incomplete or corrupt durable generations and snapshot valid shards before skipping parse workers, preserving ParsedFiles when persistence fails.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(store): drop unused durable load path

Load ParsedFiles only from the run-store snapshot and share one checksummed payload reader so inspect and deserialize stay consistent.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-30 20:50:20 +00:00

91 lines
3 KiB
JavaScript

#!/usr/bin/env node
/**
* Optional V8 sidecar warm-load bench (#3089).
*
* Not part of `npm test`. Measures repeated warm loads of the `.v8` ParsedFile
* shards already on disk through the production loader. Replay of identical
* shards is throughput-only — it is not unique-object scale.
*
* Copies the store into a temporary workspace first. The source cache is
* never mutated.
*
* Usage (from gitnexus/):
* node --expose-gc --import tsx bench/v8-sidecar/measure.mjs <storagePath>
*/
import { cp, mkdtemp, readdir, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { performance } from 'node:perf_hooks';
import { loadParsedFilesForPaths } from '../../src/storage/parsedfile-store.ts';
import { inspectV8Cache } from '../../src/storage/v8-sidecar.ts';
const srcStorage = process.argv[2];
if (!srcStorage) {
console.error('usage: node --expose-gc --import tsx bench/v8-sidecar/measure.mjs <storagePath>');
process.exit(2);
}
const srcStoreDir = path.join(srcStorage, 'parsedfile-store');
const benchRoot = await mkdtemp(path.join(tmpdir(), 'gnx-v8-bench-'));
const storeDir = path.join(benchRoot, 'parsedfile-store');
const PATH_SOURCE_SHARDS = 8;
const RUNS = 3;
try {
await cp(srcStoreDir, storeDir, { recursive: true });
const names = (await readdir(storeDir))
.filter((f) => f.endsWith('.v8') && !f.includes('.v8.'))
.sort();
if (names.length === 0) {
throw new Error(
`no .v8 ParsedFile shards in ${srcStoreDir} — run an analyze that populates the store first`,
);
}
const want = new Set();
let sourceShards = 0;
for (const name of names) {
const inspected = await inspectV8Cache(path.join(storeDir, name));
if (!inspected) continue;
sourceShards++;
for (const filePath of inspected.paths) want.add(filePath);
if (sourceShards >= PATH_SOURCE_SHARDS) break;
}
if (want.size === 0) {
throw new Error(
`no file paths readable from ${names.length} shard(s) in ${srcStoreDir} — shards may be from another Node/V8 runtime, so re-analyze with this runtime`,
);
}
const rss = () => Math.round(process.memoryUsage().rss / 1024 / 1024);
const heap = () => Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
const run = async (label) => {
if (typeof globalThis.gc === 'function') globalThis.gc();
const t0 = performance.now();
const loaded = await loadParsedFilesForPaths(benchRoot, want);
const ms = Math.round(performance.now() - t0);
if (loaded.size !== want.size) {
throw new Error(`incomplete V8 load: requested ${want.size} paths but loaded ${loaded.size}`);
}
if (typeof globalThis.gc === 'function') globalThis.gc();
console.log(
JSON.stringify({
label,
shards: names.length,
wantPaths: want.size,
files: loaded.size,
ms,
rssMiB: rss(),
heapUsedMiB: heap(),
}),
);
};
for (let i = 1; i <= RUNS; i++) {
await run(`v8-load-${i}`);
}
} finally {
await rm(benchRoot, { recursive: true, force: true });
}