Count class-value constructions in the census, and key caches on both diagnostics

- The heap census also counts instances a class value constructs: the
  construct thunk that `newValue` calls counts them after its allocation,
  as `new` does at its own.
- A build with SCRIPTC_HEAP_CENSUS set is its own executable cache entry
  (`heapCensus` in the early cache's key and route key), and one with
  SCRIPTC_REPR_REPORT set does not read the early cache, since a hit
  emits no module and so writes no report. The CLI's startup route defers
  both to the full compiler.
- Test: a warm cache still builds the census executable, counting
  `new` and class-value constructions, and still writes the report.
This commit is contained in:
Chris Tate
2026-10-27 01:54:48 -05:00
parent 8b256639f4
commit 60d5dc8ab1
9 changed files with 100 additions and 9 deletions
+5 -1
View File
@@ -74,7 +74,11 @@ async function tryFastPath(): Promise<number | null> {
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;
@@ -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<string> =>
(
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,
);
@@ -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)`,
@@ -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
@@ -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. */
@@ -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 {
@@ -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;
@@ -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
? "<npm-static-off>"
: options.npmStatic === "auto"
@@ -370,6 +373,7 @@ function routeKey(options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment"
if (options.functionLayout !== undefined) hash.update(`layout:${options.functionLayout}\0`);
if (options.checker !== undefined) hash.update(`checker:${options.checker}\0`);
if (options.youngNursery) hash.update("young-nursery\0");
if (options.heapCensus) hash.update("heap-census\0");
hash
.update(
options.npmStatic === null
+5 -1
View File
@@ -161,6 +161,8 @@ import { clearResolveCaches } from "./frontend/resolve.js";
import { loadFfiProfile, type FfiProfile } from "./ffi/ffi-manifest.js";
import { executableLinkFeatures } from "./backend/executable-features.js";
import { youngNurseryRequested } from "./backend/young-nursery.js";
import { heapCensusRequested } from "./backend/llvm/heap-census.js";
import { representationReportPath } from "./backend/llvm/repr-report.js";
import {
type FrontendInputSnapshot,
FrontendInputTracker,
@@ -1694,6 +1696,7 @@ async function compileTracked(
...(functionLayoutDigest === undefined ? {} : { functionLayout: functionLayoutDigest }),
...checkerField(),
...(youngNurseryRequested() ? { youngNursery: true as const } : {}),
...(heapCensusRequested() ? { heapCensus: true as const } : {}),
npmStatic: opts.npmStatic ?? null,
...(shapeOverlayEnabled() ? { shapeOverlay: true as const } : {}),
ffiProfile:
@@ -1737,8 +1740,9 @@ async function compileTracked(
implementationDependencies: implementation.dependencies,
};
}
// A cache hit emits no module, so it would write no representation report.
const earlyHit =
earlyCacheOptions === null
earlyCacheOptions === null || representationReportPath() !== null
? null
: await readEarlyExecutableCache(cacheRoot, earlyCacheOptions);
if (earlyHit !== null) {