mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user