diff --git a/packages/cli/src/bootstrap.ts b/packages/cli/src/bootstrap.ts index f6efec134..0c09c4435 100644 --- a/packages/cli/src/bootstrap.ts +++ b/packages/cli/src/bootstrap.ts @@ -74,7 +74,11 @@ async function tryFastPath(): Promise { values["target-cpu"] !== undefined || (values["external-types"] ?? []).length > 0 || // The full CLI validates the Node island policy flags. - hasNodeIslandFlags(values) + hasNodeIslandFlags(values) || + // The full compiler writes the representation report and keys the heap + // census build (backend/llvm/repr-report.ts, heap-census.ts). + (process.env["SCRIPTC_REPR_REPORT"] ?? "") !== "" || + (process.env["SCRIPTC_HEAP_CENSUS"] ?? "") !== "" ) return null; const backend = values.backend; diff --git a/packages/cli/test/executable-cache.test.ts b/packages/cli/test/executable-cache.test.ts index ee8027765..eef8e97a0 100644 --- a/packages/cli/test/executable-cache.test.ts +++ b/packages/cli/test/executable-cache.test.ts @@ -198,3 +198,57 @@ test.skipIf(process.platform === "win32")( }, 120_000, ); + +test.skipIf(process.platform !== "linux")( + "a warm executable cache still builds the heap census and writes the representation report", + async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-cli-diagnostic-cache-")); + const cacheRoot = join(dir, "cache"); + const entry = join(dir, "main.ts"); + const outPath = join(dir, "program"); + try { + await mkdir(cacheRoot, { mode: 0o700 }); + // `new` and construction through a class value both count. + await writeFile( + entry, + [ + "class Item { constructor(readonly id: number) {} }", + "const kind: typeof Item = Item;", + "const items = [new Item(1), new kind(2), new kind(3)];", + "console.log(items.length);", + "", + ].join("\n"), + ); + const build = async (env: NodeJS.ProcessEnv = {}): Promise => + ( + await execFileAsync( + process.execPath, + ["--import", tsxLoader, cliEntry, "build", entry, "-o", outPath], + { + env: { ...process.env, SCRIPTC_CACHE_DIR: cacheRoot, SCRIPTC_TIMING: "1", ...env }, + maxBuffer: 4 * 1024 * 1024, + }, + ) + ).stderr; + expect(await build()).toContain("scriptc lowering"); + expect(await build()).not.toContain("scriptc lowering"); + + // The census build is its own cache entry. + const census = join(dir, "census.tsv"); + await build({ SCRIPTC_HEAP_CENSUS: "1" }); + const { stdout } = await execFileAsync(outPath, [], { + env: { ...process.env, SCRIPTC_HEAP_CENSUS: census }, + }); + expect(stdout).toBe("3\n"); + expect(await readFile(census, "utf8")).toMatch(/^new\tItem\t\t3\t[1-9]\d*$/m); + + // A report is written even when the executable is cached. + const report = join(dir, "repr.tsv"); + await build({ SCRIPTC_REPR_REPORT: report }); + expect(await readFile(report, "utf8")).toMatch(/^field\tItem\tid\t/m); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, + 600_000, +); diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 695acc273..94cfcaf25 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -1712,6 +1712,8 @@ export function emitClassObjDefs( `define internal ptr @${mangleCtorThunk(className)}(ptr %class${paramDecls ? ", " + paramDecls : ""}) ${FN_ATTRS} { ; construct thunk ${className}`, `entry:`, ` %o = call ptr @${mangleClassNew(className)}()`, + // Construction through a class value: the census counts it here. + ...(host.heapCensus?.newLines(className) ?? []), ...(meta.def.localCaptures !== undefined ? [ ` %class.owned = call ptr @scr_classobj_retain_v(ptr %class)`, diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 9278613da..0a20e1b1a 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -2366,6 +2366,7 @@ export class LlEmitter { virtualEntry: (implFn) => this.virtualEntry(implFn), programLifetime: this.programLifetime, regionAliasTag: (word) => this.memoryAliasAttachment(`region.${word}`), + heapCensus: this.heapCensus, }; // An instance of a class that extends Array is an array: a slot of its // element array type can hold it, and the collector visits it there as diff --git a/packages/compiler/src/backend/llvm/heap-census.ts b/packages/compiler/src/backend/llvm/heap-census.ts index 91b9839cd..b55e9341b 100644 --- a/packages/compiler/src/backend/llvm/heap-census.ts +++ b/packages/compiler/src/backend/llvm/heap-census.ts @@ -1,6 +1,7 @@ /* The heap census. A program built with SCRIPTC_HEAP_CENSUS set counts, for * ranking representation work: - * - the instances each `new` of a class creates, with the class's size; + * - the instances of each class that `new` or a class value constructs, + * with the class's size; * - for each array-typed class field, its stores (with the stored array's * length in buckets and the total of those lengths), the stores whose * value is an array literal written at the store, and its accesses; @@ -17,10 +18,16 @@ import { classStructSym } from "./classes.js"; import { llvmBytes, wtf8Bytes } from "../literals.js"; import type { NullableUnions } from "./nullable-unions.js"; +/** Whether this build counts the heap census: SCRIPTC_HEAP_CENSUS is set + * and nonempty. The executable cache keys on it. */ +export function heapCensusRequested(env: NodeJS.ProcessEnv = process.env): boolean { + const flag = env["SCRIPTC_HEAP_CENSUS"]; + return flag !== undefined && flag !== ""; +} + /** The census of the module being built, or null when it is off. */ export function heapCensusOf(mod: IrModule, unions: NullableUnions): HeapCensus | null { - const flag = process.env["SCRIPTC_HEAP_CENSUS"]; - if (flag === undefined || flag === "" || mod.lib !== undefined) return null; + if (!heapCensusRequested() || mod.lib !== undefined) return null; return new HeapCensus(mod, unions); } @@ -58,8 +65,14 @@ export class HeapCensus { /** Counts one instance of `className`. */ countNew(line: (text: string) => void, className: string): void { + for (const text of this.newLines(className)) line(text); + } + + /** The lines that count one instance of `className`: a `new`, or a + * construct thunk for class values (`newValue`). */ + newLines(className: string): string[] { const index = this.instances.get(className); - if (index !== undefined) line(this.increment(index, "1")); + return index === undefined ? [] : [this.increment(index, "1")]; } /** Counts an access of a field; true when the field is counted. */ diff --git a/packages/compiler/src/backend/llvm/repr-report.ts b/packages/compiler/src/backend/llvm/repr-report.ts index 82104277c..8d6e48f30 100644 --- a/packages/compiler/src/backend/llvm/repr-report.ts +++ b/packages/compiler/src/backend/llvm/repr-report.ts @@ -12,12 +12,18 @@ import { INLINE_UNION_FIELD } from "./inline-unions.js"; export const REPR_REPORT_HEADER = "kind\towner\tname\ttype\tstorage\treason"; +/** The file SCRIPTC_REPR_REPORT names, or null. A build that writes the + * report skips the executable cache, whose hit would emit no module. */ +export function representationReportPath(env: NodeJS.ProcessEnv = process.env): string | null { + const path = env["SCRIPTC_REPR_REPORT"]; + return path === undefined || path === "" ? null : path; +} + /** Writes the report of `emitter`'s module when SCRIPTC_REPR_REPORT names a * file. */ export function writeRepresentationReport(emitter: LlEmitter): void { - const path = process.env["SCRIPTC_REPR_REPORT"]; - if (path === undefined || path === "") return; - writeFileSync(path, representationReport(emitter)); + const path = representationReportPath(); + if (path !== null) writeFileSync(path, representationReport(emitter)); } export function representationReport(emitter: LlEmitter): string { diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index b5b1e735a..e48ab8e8c 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -2,6 +2,7 @@ * Containers store erased adapters; shape helpers retain, release and trace * their fields. Every allocation has a size_t reference count at offset zero * and participates in the runtime allocation audit. */ +import type { HeapCensus } from "./heap-census.js"; import type { IrClassDef, IrModule, IrRecordShape, IrType } from "../../ir/ir.js"; import { isIdentityCollectionKey, @@ -78,6 +79,8 @@ const CYC_MAX_TYPES = 32768; /** What the tables need from the emitter: the extern-declaration ledger * and the module-wide cycle/shape indexes. */ export interface ShapeHost { + /** The counters of a build instrumented for the heap census (heap-census.ts). */ + readonly heapCensus?: HeapCensus | null; /** Chunk emitters only (emit-journal.ts): EmitJournal's allocated, built, * opened, used and entry. */ journalAllocated?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined; diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 6124a6edf..8f90a9d0c 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -168,6 +168,8 @@ export interface EarlyExecutableCacheOptions { checker?: string; /** Omitted unless SCRIPTC_YOUNG_NURSERY=1 (backend/young-nursery.ts). */ youngNursery?: true; + /** Omitted unless SCRIPTC_HEAP_CENSUS is set (backend/llvm/heap-census.ts). */ + heapCensus?: true; npmStatic: readonly string[] | "auto" | null; /** Omitted unless shape inference's overlay compiles the packages * (frontend/shape-inference). */ @@ -324,6 +326,7 @@ function cacheKey(options: EarlyExecutableCacheOptions): string { ...(options.functionLayout !== undefined ? [`layout:${options.functionLayout}`] : []), ...(options.checker !== undefined ? [`checker:${options.checker}`] : []), ...(options.youngNursery ? ["young-nursery"] : []), + ...(options.heapCensus ? ["heap-census"] : []), options.npmStatic === null ? "" : options.npmStatic === "auto" @@ -370,6 +373,7 @@ function routeKey(options: Omit