Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions src/schema/emit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ const ANALYZER_NAME = "codeanalyzer-typescript";
const MAX_IMPLEMENTED = 4;

/**
* Structural internal-field strip on the WIRE CLONE: module cache trio + callable join fields.
* Structural (walks the tree shape) rather than key-name-based, for two load-bearing reasons:
* the artifact layer's `content_hash` is WIRE payload (a name-keyed replacer would eat it), and
* a `JSON.stringify` deep-copy roundtrip builds one multi-GB string at vscode-L4 scale and OOMs
* (measured). `structuredClone` + targeted deletes never materializes a string.
* Structural internal-field strip on the WIRE COPY's per-module clones: module cache trio +
* callable join fields. Structural (walks the tree shape) rather than key-name-based, for two
* load-bearing reasons: the artifact layer's `content_hash` is WIRE payload (a name-keyed replacer
* would eat it), and a `JSON.stringify` deep-copy roundtrip builds one multi-GB string at
* vscode-L4 scale and OOMs (measured). Per-module `structuredClone` + targeted deletes never
* materializes a string and never serializes more than one module at a time (#180).
*/
function stripInternal(root: TSApplication): void {
const stripCallable = (c: Record<string, unknown>): void => {
Expand Down Expand Up @@ -156,9 +157,17 @@ export function finalizeAnalysis(
analyzer: { name: ANALYZER_NAME, version: ANALYZER_VERSION },
application: root,
};
// The wire copy: deep, detached from the live tree, internals stripped STRUCTURALLY —
// structuredClone instead of a stringify roundtrip (the string form OOMs at vscode-L4 scale).
const application = structuredClone(envelope) as TSAnalysis;
// The wire copy: detached from the live tree, internals stripped STRUCTURALLY. The strip only
// ever touches modules (and what hangs off them), so the clone is per MODULE (#180): one
// structuredClone of the whole envelope hits Bun's serialization ceiling on a large repository
// (a TypeError at 13k files on 1.3.0; a hard abort with no exception at ~2 GiB on 1.3.14 —
// measured), while the largest single module is megabytes. Everything else on the root
// (call_graph, param_in/out, artifacts, config edges, entrypoint report) is untouched by the
// strip and shared by reference. A stringify roundtrip is out for the same reason: the string
// form OOMs at vscode-L4 scale.
const symbol_table: TSApplication["symbol_table"] = {};
for (const [key, mod] of Object.entries(root.symbol_table)) symbol_table[key] = structuredClone(mod);
const application: TSAnalysis = { ...envelope, application: { ...root, symbol_table } };
stripInternal(application.application);
return { application, internal: app, ...(pg ? { program_graphs: pg } : {}), idBySig, collisions, dangling };
}
72 changes: 72 additions & 0 deletions test/finalize-clone.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* #180 — finalizeAnalysis must never structuredClone the whole envelope.
*
* Bun's structuredClone has a hard serialization ceiling (measured on 1.3.14: a ~2.2 GiB object
* aborts the process with no exception; a 13k-file repository hit a TypeError at 1.3.0). The
* envelope is only cloned so the wire copy can be internal-field-stripped without touching the
* live tree that `result.internal` hands back — and the strip only ever touches modules. So the
* clone is per MODULE (the largest is megabytes, never gigabytes); everything else on the root is
* shared by reference. This pins both halves: the wire is detached from and stripped relative to
* the internal tree, and the strip never reached into the internal tree.
*/
import { describe, expect, spyOn, test } from "bun:test";
import * as path from "node:path";
import { analyze } from "../src/core";
import type { AnalysisOptions } from "../src/options";
import type { TSAnalysis } from "../src/schema";

const FIXTURE = path.resolve(import.meta.dir, "fixtures/sample-app");
const opts = { input: FIXTURE, appName: "sa", analysisLevel: 2, noBuild: true, emit: "json", eager: true } as unknown as AnalysisOptions;

describe("#180 finalize clones per module", () => {
test("structuredClone is called once per module and never on the envelope or the root", async () => {
const spy = spyOn(globalThis, "structuredClone");
try {
const r = await analyze(opts);
const modules = Object.keys(r.internal.symbol_table).length;
const cloned = spy.mock.calls.map((c) => c[0] as Record<string, unknown>);
expect(cloned.length).toBe(modules);
for (const arg of cloned) {
expect(arg.kind).toBe("module");
expect(arg.schema_version).toBeUndefined();
expect(arg.symbol_table).toBeUndefined();
}
} finally {
spy.mockRestore();
}
});

test("every wire module is a distinct object from its internal twin, stripped, with the twin intact", async () => {
const r = await analyze(opts);
const wire = (r.application as TSAnalysis).application;
const keys = Object.keys(wire.symbol_table);
expect(keys.length).toBeGreaterThan(0);
for (const k of keys) {
const w = wire.symbol_table[k] as unknown as Record<string, unknown>;
const i = r.internal.symbol_table[k] as unknown as Record<string, unknown>;
expect(w).not.toBe(i);
// the strip landed on the wire...
expect(w.call_sites).toBeUndefined();
expect(w.last_modified).toBeUndefined();
expect(w.file_size).toBeUndefined();
expect(w.content_hash).toBe(i.content_hash); // #118: content_hash stays on the wire
// ...and only on the wire: the live tree still carries the per-run join fields
expect(Array.isArray(i.call_sites)).toBe(true);
expect(typeof i.last_modified).toBe("number");
}
// a module with callables: the callable-level strip detached too
const mod = wire.symbol_table["src/services.ts"]!;
const fn = Object.values(mod.types)[0]!.callables ? Object.values(Object.values(mod.types)[0]!.callables!)[0] : undefined;
expect(fn).toBeDefined();
expect((fn as unknown as Record<string, unknown>).call_sites).toBeUndefined();
expect((fn as unknown as Record<string, unknown>).abs_path).toBeUndefined();
});

test("the wire survives a JSON roundtrip identical to itself (no shared-reference surprise)", async () => {
const r = await analyze(opts);
const once = JSON.stringify(r.application);
// mutating the internal tree after finalize must not leak into an already-finalized wire
delete (r.internal.symbol_table["src/index.ts"] as unknown as Record<string, unknown>).functions;
expect(JSON.stringify(r.application)).toBe(once);
});
});
Loading