Merge remote-tracking branch 'origin/perf/static-objects' into HEAD

# Conflicts:
#	packages/compiler/src/frontend/pipeline.ts
#	packages/compiler/src/frontend/shape-inference/overlay.ts
#	tests/harness/npm.test.ts
This commit is contained in:
Malte Ubl
2026-10-25 13:38:56 -07:00
820 changed files with 41438 additions and 2453 deletions
+10
View File
@@ -180,6 +180,16 @@ function generateInput(kind) {
["sku-" + i, "category-" + (i % 17), "product " + i, i % 40, 199 + (i % 5000)].join("\t"),
).join("\n") + "\n",
);
} else if (kind === "line-reader") {
// About 67 MB of access-log lines; every 50th path is not ASCII.
writeFileSync(
path,
Array.from(
{ length: 600_000 },
(_, i) =>
`10.${i % 251}.${i % 7}.${i % 13} - - [24/Oct/2026:${String(i % 24).padStart(2, "0")}:${String(i % 60).padStart(2, "0")}:00 +0000] "${i % 9 === 0 ? "POST" : "GET"} /${i % 50 === 0 ? "caf\u00e9" : "api"}/items/${i % 997}?page=${i % 31} HTTP/1.1" ${i % 23 === 0 ? 404 : 200} ${(i * 7919) % 50000} "scriptc-bench/1.0"`,
).join("\n") + "\n",
);
} else if (kind === "cli-config") {
writeFileSync(path, JSON.stringify(deployManifest(), null, 2) + "\n");
} else throw new Error(`unknown input kind ${kind}`);
+32 -5
View File
@@ -17,9 +17,16 @@ const sanitize = process.env["SCRIPTC_SAN"] === "1";
// SCRIPTC_NATIVE_PROFILE_GENERATE=<pattern> builds a compiler that writes a
// raw profile per run (scripts/refresh-cli-profile.mjs). Optimized builds
// otherwise use the committed profile, or the indexed profile that
// SCRIPTC_NATIVE_PROFILE_USE names; an empty SCRIPTC_NATIVE_PROFILE_USE
// builds without one.
// SCRIPTC_NATIVE_PROFILE_USE names (gzip-compressed when the name ends in
// .gz, as refresh-cli-profile.mjs writes it); an empty
// SCRIPTC_NATIVE_PROFILE_USE builds without one.
const profileGenerate = process.env["SCRIPTC_NATIVE_PROFILE_GENERATE"] || undefined;
// Below this share of matching functions, the build warns that the profile
// has lost most of its gain. A profile refreshed on the same commit matched
// 92.7% of the native compiler's profiled functions, one refreshed 1.5 days
// and about 200 commits earlier 87.2%, and that one kept about half of the
// fresh profile's gain on a cold tsc-ts build.
const PROFILE_MATCH_WARNING = 0.9;
const committedProfile = join(root, "packages/compiler/native/cli.profdata.gz");
const requestedProfile = process.env["SCRIPTC_NATIVE_PROFILE_USE"];
const useProfile =
@@ -45,10 +52,11 @@ const seed = join(dirname(output), ".scriptc", basename(output) + "-seed");
for (const dir of [bin, lib, seed]) mkdirSync(dir, { recursive: true });
let profileUse: string | undefined;
if (useProfile) {
profileUse = requestedProfile;
if (profileUse === undefined) {
profileUse = requestedProfile ?? committedProfile;
if (profileUse.endsWith(".gz")) {
const compressed = profileUse;
profileUse = join(seed, "cli.profdata");
writeFileSync(profileUse, gunzipSync(readFileSync(committedProfile)));
writeFileSync(profileUse, gunzipSync(readFileSync(compressed)));
}
}
const compiler = process.env["SCRIPTC_CC"] ?? target.defaultLinker;
@@ -127,6 +135,25 @@ if (!result.ok)
)
.join("\n"),
);
// How well the profile still describes the compiler: a function whose
// control flow changed since the profile was collected is optimized as
// without one. The share falls as compiler code changes, and with it the
// profile's gain (refresh it with scripts/refresh-cli-profile.mjs).
const match = useProfile && "artifact" in result ? result.artifact.profileMatch : undefined;
if (match !== undefined) {
const profiled = match.annotated + match.mismatched;
const share = profiled === 0 ? 0 : match.annotated / profiled;
console.log(
`native compiler profile: ${match.annotated} of ${profiled} profiled functions match (${(100 * share).toFixed(1)}%); ${match.missing} functions have no profile` +
(match.unknownPartitions > 0
? `; ${match.unknownPartitions} partitions reused without counts`
: ""),
);
if (match.unknownPartitions === 0 && share < PROFILE_MATCH_WARNING)
console.warn(
`warning: the native compiler profile matches ${(100 * share).toFixed(1)}% of the functions it describes; run scripts/refresh-cli-profile.mjs`,
);
}
const compilerVersion = (
JSON.parse(readFileSync(join(root, "packages/compiler/package.json"), "utf8")) as {
version: string;
+13 -6
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env node
/* Refreshes packages/compiler/native/cli.profdata.gz, the profile that release
* builds of the native compiler optimize with (scripts/build-native-cli.mts).
/* Refreshes packages/compiler/native/cli.profdata.gz, the profile that builds
* of the native compiler optimize with (scripts/build-native-cli.mts). The
* release workflow's cli-profile job runs it with --output on the release
* commit, and release builds use that profile instead of the committed one.
*
* Builds an instrumented native compiler for this Linux host, runs it on the
* training builds below, and merges the raw profiles it writes into one
@@ -26,7 +28,7 @@ import {
writeFileSync,
} from "node:fs";
import { cpus, tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util";
import { constants, gzipSync } from "node:zlib";
@@ -55,6 +57,9 @@ const tscTsPackage = join(tscTs, "packages/tsc-ts");
if (!existsSync(join(tscTsPackage, "src/main.ts")))
throw new Error(`${tscTs} is not a tsc-ts checkout with installed dependencies`);
const jobs = Number(values.jobs);
// The output's directory is created up front, before the instrumented build.
const profileOutput = resolve(values.output);
mkdirSync(dirname(profileOutput), { recursive: true });
const work = resolve(values.work ?? mkdtempSync(join(tmpdir(), "scriptc-cli-profile-")));
const raw = join(work, "raw");
const builds = join(work, "builds");
@@ -196,10 +201,12 @@ const merged = spawnSync(scriptc, ["merge-profiles", "--sparse", "-o", indexed,
});
if (merged.status !== 0) throw new Error("merge-profiles failed");
// The indexed profile compresses to about an eighth of its size.
const output = resolve(values.output);
writeFileSync(output, gzipSync(readFileSync(indexed), { level: constants.Z_BEST_COMPRESSION }));
writeFileSync(
profileOutput,
gzipSync(readFileSync(indexed), { level: constants.Z_BEST_COMPRESSION }),
);
console.log(
`wrote ${output} (${statSync(output).size} bytes; ${statSync(indexed).size} uncompressed)`,
`wrote ${profileOutput} (${statSync(profileOutput).size} bytes; ${statSync(indexed).size} uncompressed)`,
);
// A work directory the caller named stays for inspection.
if (values.work === undefined) rmSync(work, { recursive: true, force: true });
+5 -1
View File
@@ -395,7 +395,9 @@ export function dynamicTierSymbols(names) {
* bench/channel-messages and bench/worker-messages, added later, link them
* through worker messages:
* node:worker_threads values are dyn objects. bench/json-unions, added
* later too, decodes JSON.parse trees. bench/dynamic-methods and
* later too, decodes JSON.parse trees. bench/throw-chains, also added
* later, tests caught values with `instanceof`, as bench/validate-errors
* does. bench/dynamic-methods and
* bench/dynamic-properties are untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and
* tests/harness/bench-dynamic-tier.test.ts enforce that, and the test also
* fails when a listed workload stops linking them, so remove a workload here
@@ -416,7 +418,9 @@ export const DYNAMIC_WORKLOADS = new Set([
"bench/typed-array-scan-workers",
"bench/log-summary",
"bench/inventory-report",
"bench/line-reader",
"bench/json-unions",
"bench/throw-chains",
]);
/** Names of the functions (text and weak symbols) in `nm` output. Mach-O