mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Print how a --profile-use build's profile matched
`scriptc build --profile-use` prints one line on stderr with the number of functions the profile annotated, the number whose control flow changed since it was collected (mismatched), and the number it has no record of. When more than 10% of the functions the profile describes are mismatched, it also prints a warning to collect the profile again. The build still succeeds. Both the compiler library's build and the native compiler's build report the match. Cached builds report the match of the build that produced them: the native codegen and object caches store it beside the cached objects, and the executable cache stores it in its entry.
This commit is contained in:
@@ -170,7 +170,7 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
|
||||
<dd>On Linux, build an instrumented executable for profile-guided optimization. Each run writes a raw profile to the path pattern, where <code>%p</code> expands to the process ID; the <code>LLVM_PROFILE_FILE</code> environment variable overrides the pattern at run time. A relative pattern resolves from the current working directory. Instrumented programs run slower. Valid only for executable builds with <code>--optimization=release</code> or <code>speed</code>.</dd>
|
||||
|
||||
<dt><code>--profile-use <file.profdata></code></dt>
|
||||
<dd>Optimize the executable with a profile from <code>scriptc merge-profiles</code>: code is laid out, inlined, and split by how the profiled runs used it. Code the profile does not describe, or describes from an older version of the program, is optimized as without a profile, so a stale profile only loses part of its gain. A new or changed profile compiles every partition of the program again. Valid only for executable builds with <code>--optimization=release</code> or <code>speed</code>.</dd>
|
||||
<dd>Optimize the executable with a profile from <code>scriptc merge-profiles</code>: code is laid out, inlined, and split by how the profiled runs used it. Code the profile does not describe, or describes from an older version of the program, is optimized as without a profile, so a stale profile only loses part of its gain. A new or changed profile compiles every partition of the program again. <code>scriptc build</code> prints how the profile matched the program's functions on stderr; see <a href="#profile-guided-optimization">Profile-guided optimization</a>. Valid only for executable builds with <code>--optimization=release</code> or <code>speed</code>.</dd>
|
||||
|
||||
<dt><code>--npm-static <pkg[,pkg…]|auto></code></dt>
|
||||
<dd>Name npm packages to compile even if they don't ship their own type declarations (repeatable). A trailing <code>...</code>, as in <code>fs-extra...</code>, also names every package that package depends on. <code>auto</code> uses the default automatic selection. Packages that can't compile produce diagnostics. See <a href="/docs/dependencies#explicit-package-selection">npm Dependencies</a>.</dd>
|
||||
@@ -262,6 +262,25 @@ $ scriptc build app.ts --optimization=speed --profile-use app.profdata
|
||||
|
||||
Train on workloads like the ones the program will run: code the training never reached is optimized as without a profile. The profile can be committed and reused as the program changes. Functions whose control flow changed since the profile was collected lose their profile, and the rest keep theirs, so regenerate it when much of the program has changed. [Build parallelism](/docs/how-it-works#build-parallelism) describes what a profile adds to build time.
|
||||
|
||||
`scriptc build --profile-use` prints one line on stderr with how the profile matched the program's functions:
|
||||
|
||||
```
|
||||
scriptc: profile app.profdata: 9170 functions annotated, 0 mismatched, 28636 without a record
|
||||
```
|
||||
|
||||
- **annotated:** functions optimized with the profile's counts.
|
||||
- **mismatched:** functions whose control flow changed since the profile was collected. They are optimized without counts.
|
||||
- **without a record:** functions the profile does not describe: new functions, and functions the profiled runs never reached.
|
||||
- When the build reuses cached partitions whose counts were not recorded, the line ends with the number of those partitions.
|
||||
|
||||
When more than 10% of the functions the profile describes (annotated and mismatched) are mismatched, the build also prints a warning:
|
||||
|
||||
```
|
||||
scriptc: warning: 3739 of the 8947 functions app.profdata describes (41.8%) changed since it was collected and are optimized without their counts; collect the profile again
|
||||
```
|
||||
|
||||
The build still succeeds. Collecting the profile again with the current program and scriptc version restores the counts.
|
||||
|
||||
## Target CPU
|
||||
|
||||
By default, x86-64 executables run on any x86-64 CPU, so the compiler uses only the baseline instruction set (SSE2). `--target-cpu` compiles the program's code for a higher x86-64 microarchitecture level. Each level includes the ones below it:
|
||||
|
||||
@@ -63,8 +63,16 @@ test.skipIf(process.platform !== "linux")(
|
||||
expect(merge.stdout).toBe(`${merged}\n`);
|
||||
|
||||
const optimized = join(dir, "optimized");
|
||||
await scriptc(["build", entry, "--profile-use", merged, "-o", optimized], env);
|
||||
const fresh = await scriptc(["build", entry, "--profile-use", merged, "-o", optimized], env);
|
||||
expect((await execFileAsync(optimized, ["1000"])).stdout).toBe("total 167499\n");
|
||||
// The build reports how the profile matched the program's functions.
|
||||
expect(fresh.stderr).toMatch(
|
||||
/^scriptc: profile .*: [1-9]\d* functions annotated, 0 mismatched, \d+ without a record\n$/,
|
||||
);
|
||||
expect(fresh.stderr).toContain(`scriptc: profile ${merged}: `);
|
||||
// A build from the cache reports the same match.
|
||||
const again = await scriptc(["build", entry, "--profile-use", merged, "-o", optimized], env);
|
||||
expect(again.stderr).toBe(fresh.stderr);
|
||||
|
||||
// A sparse profile leaves out the runtime functions the runs never
|
||||
// reached.
|
||||
@@ -77,10 +85,15 @@ test.skipIf(process.platform !== "linux")(
|
||||
await scriptc(["build", entry, "--profile-use", sparse, "-o", optimized], env);
|
||||
expect((await execFileAsync(optimized, ["1000"])).stdout).toBe("total 167499\n");
|
||||
|
||||
// A profile from an older version of the program still builds it.
|
||||
// A profile from an older version of the program still builds it. The
|
||||
// edited function no longer matches its counts, and the build warns.
|
||||
await writeFile(entry, PROGRAM + 'console.log("edited");\n');
|
||||
await scriptc(["build", entry, "--profile-use", merged, "-o", optimized], env);
|
||||
const stale = await scriptc(["build", entry, "--profile-use", merged, "-o", optimized], env);
|
||||
expect((await execFileAsync(optimized, ["1000"])).stdout).toBe("total 167499\nedited\n");
|
||||
expect(stale.stderr).toMatch(/ [1-9]\d* mismatched, /);
|
||||
expect(stale.stderr).toMatch(
|
||||
/\nscriptc: warning: [1-9]\d* of the \d+ functions .* describes \([\d.]+%\) changed since it was collected and are optimized without their counts; collect the profile again\n$/,
|
||||
);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { chmod, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
@@ -26,6 +26,7 @@ async function fakePackage(
|
||||
emptyOutput?: boolean;
|
||||
missingOutput?: boolean;
|
||||
changePackageDuringEmit?: boolean;
|
||||
profileReport?: string;
|
||||
} = {},
|
||||
) {
|
||||
const root = await mkdtemp(join(tmpdir(), "scriptc-native-helper-test-"));
|
||||
@@ -72,6 +73,7 @@ ${
|
||||
? ":"
|
||||
: 'for each in $outputs; do cp "$input" "$each"; done'
|
||||
}
|
||||
${options.profileReport === undefined ? "" : `printf '%s\\n' '${options.profileReport}'`}
|
||||
`,
|
||||
);
|
||||
await chmod(bin, 0o755);
|
||||
@@ -196,6 +198,41 @@ test("profiles reach the helper and key the native cache by their bytes", async
|
||||
expect(calls[2]).toContain("--profile-use /profiles/a.profdata");
|
||||
});
|
||||
|
||||
test("a cached artifact keeps the profile match of the build that produced it", async () => {
|
||||
const pkg = await fakePackage({
|
||||
profileReport:
|
||||
'{"ok":true,"profile":{"annotated":120,"mismatched":7,"missing":40,"unknown_partitions":0}}',
|
||||
});
|
||||
const used = {
|
||||
...request(pkg.root, pkg.packageJson, join(pkg.root, "used.o")),
|
||||
profile: { use: { path: "/profiles/a.profdata", digest: "a".repeat(64) } },
|
||||
};
|
||||
const match = { annotated: 120, mismatched: 7, missing: 40, unknownPartitions: 0 };
|
||||
expect((await emitNativeArtifact(used)).profileMatch).toEqual(match);
|
||||
const again = await emitNativeArtifact({ ...used, outputPath: join(pkg.root, "again.o") });
|
||||
expect(again.profileMatch).toEqual(match);
|
||||
expect((await readFile(pkg.log, "utf8")).trim().split("\n")).toHaveLength(1);
|
||||
// Without the cached match, the cached partition's counts are unknown.
|
||||
const cache = join(pkg.root, "cache", "native-codegen-v1");
|
||||
const [shard] = await readdir(cache);
|
||||
for (const name of await readdir(join(cache, shard!)))
|
||||
if (name.includes(".profile-match")) await rm(join(cache, shard!, name));
|
||||
const unknown = await emitNativeArtifact({ ...used, outputPath: join(pkg.root, "unknown.o") });
|
||||
expect(unknown.profileMatch).toEqual({
|
||||
annotated: 0,
|
||||
mismatched: 0,
|
||||
missing: 0,
|
||||
unknownPartitions: 1,
|
||||
});
|
||||
// Builds without a profile carry no match, from the helper or the cache.
|
||||
const plain = request(pkg.root, pkg.packageJson, join(pkg.root, "plain.o"));
|
||||
expect((await emitNativeArtifact(plain)).profileMatch).toBeUndefined();
|
||||
expect(
|
||||
(await emitNativeArtifact({ ...plain, outputPath: join(pkg.root, "plain-again.o") }))
|
||||
.profileMatch,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test("chunked LLVM input preserves emitted bytes and the native cache identity", async () => {
|
||||
const pkg = await fakePackage();
|
||||
const first = request(pkg.root, pkg.packageJson, join(pkg.root, "chunks.o"));
|
||||
|
||||
@@ -2,7 +2,17 @@ import { execFile } from "node:child_process";
|
||||
import { createHash, type Hash } from "node:crypto";
|
||||
import { closeSync, constants, openSync, readSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { access, chmod, copyFile, mkdir, readFile, rename, rm, stat } from "node:fs/promises";
|
||||
import {
|
||||
access,
|
||||
chmod,
|
||||
copyFile,
|
||||
mkdir,
|
||||
readFile,
|
||||
rename,
|
||||
rm,
|
||||
stat,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import {
|
||||
@@ -436,7 +446,10 @@ export async function emitNativeArtifact(
|
||||
dependencies: helper.dependencies,
|
||||
outputPaths: outputs,
|
||||
} satisfies NativeCodegenArtifact;
|
||||
if (await installCachedPartitions(cached, outputs)) return artifact;
|
||||
if (await installCachedPartitions(cached, outputs)) {
|
||||
if (options.profile?.use === undefined) return artifact;
|
||||
return { ...artifact, profileMatch: await cachedProfileMatch(cached[0]!, outputs.length) };
|
||||
}
|
||||
// Partitioned programs also reuse individual partition artifacts, so an
|
||||
// edit recompiles only the partitions it affects. The directory names the
|
||||
// exact helper build and target; the helper keys entries by content.
|
||||
@@ -459,6 +472,7 @@ export async function emitNativeArtifact(
|
||||
// The helper reads a private copy: the caller's .ll may be shared with a
|
||||
// concurrent build of another entry that has the same output stem.
|
||||
const input = privateSiblingPath(options.outputPath, "native-input");
|
||||
const matchStage = privateSiblingPath(options.outputPath, "native-profile-match");
|
||||
try {
|
||||
if (isLlvmFile(options.llvm)) await copyFile(options.llvm.file, input);
|
||||
else await writeLlvmSource(input, options.llvm, 0o600);
|
||||
@@ -493,6 +507,8 @@ export async function emitNativeArtifact(
|
||||
}
|
||||
await chmod(stage, artifactMode());
|
||||
}
|
||||
const profileMatch =
|
||||
options.profile?.use === undefined ? null : parseNativeProfileMatch(stdout);
|
||||
// Cache publication is an optimization boundary. The helper has already
|
||||
// produced a valid caller artifact, so a read-only/full cache must not
|
||||
// discard it or turn an otherwise successful build into an exception.
|
||||
@@ -500,23 +516,68 @@ export async function emitNativeArtifact(
|
||||
root !== null &&
|
||||
(await nativeArtifactDependenciesStillMatch(helper.dependencies).catch(() => false))
|
||||
) {
|
||||
// The match goes in first: a reader that finds every partition finds it too.
|
||||
if (profileMatch !== null) {
|
||||
await writeFile(
|
||||
matchStage,
|
||||
`${JSON.stringify({ profile: helperProfileMatch(profileMatch) })}\n`,
|
||||
{
|
||||
mode: 0o600,
|
||||
},
|
||||
)
|
||||
.then(() => publishCachedFile(matchStage, cachedProfileMatchPath(cached[0]!)))
|
||||
.catch(() => undefined);
|
||||
}
|
||||
for (let index = 0; index < stages.length; index++)
|
||||
await publishCachedFile(stages[index]!, cached[index]!).catch(() => undefined);
|
||||
}
|
||||
for (let index = 0; index < stages.length; index++)
|
||||
await rename(stages[index]!, outputs[index]!);
|
||||
await pruneBuildCache(root);
|
||||
const profileMatch =
|
||||
options.profile?.use === undefined ? null : parseNativeProfileMatch(stdout);
|
||||
return profileMatch === null ? artifact : { ...artifact, profileMatch };
|
||||
} finally {
|
||||
await Promise.all([
|
||||
...stages.map((stage) => rm(stage, { force: true }).catch(() => undefined)),
|
||||
rm(input, { force: true }).catch(() => undefined),
|
||||
rm(matchStage, { force: true }).catch(() => undefined),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/** The cache entry beside a cached artifact's first partition that holds the
|
||||
* profile match of the build that produced it. */
|
||||
function cachedProfileMatchPath(firstPartition: string): string {
|
||||
return `${firstPartition}.profile-match`;
|
||||
}
|
||||
|
||||
/** The match in the helper's own JSON form, as parseNativeProfileMatch reads it. */
|
||||
function helperProfileMatch(match: NativeProfileMatch): Record<string, number> {
|
||||
return {
|
||||
annotated: match.annotated,
|
||||
mismatched: match.mismatched,
|
||||
missing: match.missing,
|
||||
unknown_partitions: match.unknownPartitions,
|
||||
};
|
||||
}
|
||||
|
||||
/** The profile match of a cached artifact. Without a valid entry, every
|
||||
* partition's counts are unknown. */
|
||||
async function cachedProfileMatch(
|
||||
firstPartition: string,
|
||||
partitions: number,
|
||||
): Promise<NativeProfileMatch> {
|
||||
const path = cachedProfileMatchPath(firstPartition);
|
||||
const text = (await validCachedFile(path)) ? await readFile(path, "utf8").catch(() => "") : "";
|
||||
return (
|
||||
parseNativeProfileMatch(text) ?? {
|
||||
annotated: 0,
|
||||
mismatched: 0,
|
||||
missing: 0,
|
||||
unknownPartitions: partitions,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Install a cached artifact only when every partition is present and valid. */
|
||||
async function installCachedPartitions(
|
||||
cached: readonly (string | null)[],
|
||||
|
||||
@@ -3,6 +3,7 @@ import { executableLinkInputs } from "./link-plan-core.js";
|
||||
import {
|
||||
parseNativeProfileMatch,
|
||||
nativeProfileHelperArgs,
|
||||
renderNativeProfileMatch,
|
||||
nativeProfileKey,
|
||||
nativeProfileRefusal,
|
||||
profileGenerateLinkerArgs,
|
||||
@@ -84,3 +85,49 @@ test("the helper's profile match is read from its output", () => {
|
||||
expect(parseNativeProfileMatch('{"ok":true}\n')).toBeNull();
|
||||
expect(parseNativeProfileMatch('{"ok":true,"profile":{"annotated":-1}}\n')).toBeNull();
|
||||
});
|
||||
|
||||
test("the build reports the match, and warns when the profile is stale", () => {
|
||||
expect(
|
||||
renderNativeProfileMatch("app.profdata", {
|
||||
annotated: 9170,
|
||||
mismatched: 0,
|
||||
missing: 28636,
|
||||
unknownPartitions: 0,
|
||||
}),
|
||||
).toBe(
|
||||
"scriptc: profile app.profdata: 9170 functions annotated, 0 mismatched, 28636 without a record\n",
|
||||
);
|
||||
// 10% of the described functions changed: at the threshold, no warning.
|
||||
expect(
|
||||
renderNativeProfileMatch("app.profdata", {
|
||||
annotated: 90,
|
||||
mismatched: 10,
|
||||
missing: 5,
|
||||
unknownPartitions: 1,
|
||||
}),
|
||||
).toBe(
|
||||
"scriptc: profile app.profdata: 90 functions annotated, 10 mismatched, 5 without a record, 1 partition without counts\n",
|
||||
);
|
||||
expect(
|
||||
renderNativeProfileMatch("app.profdata", {
|
||||
annotated: 5208,
|
||||
mismatched: 3739,
|
||||
missing: 28859,
|
||||
unknownPartitions: 2,
|
||||
}),
|
||||
).toBe(
|
||||
"scriptc: profile app.profdata: 5208 functions annotated, 3739 mismatched, 28859 without a record, 2 partitions without counts\n" +
|
||||
"scriptc: warning: 3739 of the 8947 functions app.profdata describes (41.8%) changed since it was collected and are optimized without their counts; collect the profile again\n",
|
||||
);
|
||||
// Nothing described: no share to warn about.
|
||||
expect(
|
||||
renderNativeProfileMatch("app.profdata", {
|
||||
annotated: 0,
|
||||
mismatched: 0,
|
||||
missing: 12,
|
||||
unknownPartitions: 0,
|
||||
}),
|
||||
).toBe(
|
||||
"scriptc: profile app.profdata: 0 functions annotated, 0 mismatched, 12 without a record\n",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -83,6 +83,32 @@ export function parseNativeProfileMatch(stdout: string): NativeProfileMatch | nu
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The share of the functions a profile describes whose control flow changed
|
||||
* since it was collected, above which `scriptc build` warns that the profile
|
||||
* is stale. A freshly collected profile has none. */
|
||||
export const STALE_PROFILE_SHARE = 0.1;
|
||||
|
||||
/** `scriptc build --profile-use`'s report on stderr: one line with the match,
|
||||
* and a warning line when more than STALE_PROFILE_SHARE of the functions the
|
||||
* profile describes changed since it was collected. */
|
||||
export function renderNativeProfileMatch(profile: string, match: NativeProfileMatch): string {
|
||||
const unknown =
|
||||
match.unknownPartitions === 0
|
||||
? ""
|
||||
: `, ${match.unknownPartitions} partition${match.unknownPartitions === 1 ? "" : "s"} without counts`;
|
||||
const lines = [
|
||||
`scriptc: profile ${profile}: ${match.annotated} functions annotated, ${match.mismatched} mismatched, ${match.missing} without a record${unknown}`,
|
||||
];
|
||||
const described = match.annotated + match.mismatched;
|
||||
if (described > 0 && match.mismatched / described > STALE_PROFILE_SHARE) {
|
||||
const percent = ((match.mismatched / described) * 100).toFixed(1);
|
||||
lines.push(
|
||||
`scriptc: warning: ${match.mismatched} of the ${described} functions ${profile} describes (${percent}%) changed since it was collected and are optimized without their counts; collect the profile again`,
|
||||
);
|
||||
}
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
/** Cache-key part; empty without a profile so other keys are unchanged. */
|
||||
export function nativeProfileKey(profile: NativeProfileOptions | undefined): string {
|
||||
if (profile?.generate !== undefined) return `profile-generate:${profile.generate}`;
|
||||
|
||||
@@ -8,7 +8,12 @@ import {
|
||||
validateNativeCodegenVersion,
|
||||
type NativeCodegenOutputKind,
|
||||
} from "./native-codegen-core.js";
|
||||
import { nativeProfileHelperArgs, type NativeProfileOptions } from "./native-profile.js";
|
||||
import {
|
||||
nativeProfileHelperArgs,
|
||||
parseNativeProfileMatch,
|
||||
type NativeProfileMatch,
|
||||
type NativeProfileOptions,
|
||||
} from "./native-profile.js";
|
||||
|
||||
export function runNativeTool(
|
||||
executable: string,
|
||||
@@ -48,10 +53,10 @@ export function emitNativeObject(options: {
|
||||
partitionCache?: string;
|
||||
/** Profile-guided optimization: instrumentation or an indexed profile. */
|
||||
profile?: NativeProfileOptions;
|
||||
}): string[] {
|
||||
}): { outputs: string[]; profileMatch: NativeProfileMatch | null } {
|
||||
verifyNativeHelper(options);
|
||||
const outputs = nativePartitionPaths(options.outputPath, options.partitions ?? 1);
|
||||
runNativeTool(options.executable, [
|
||||
const stdout = runNativeTool(options.executable, [
|
||||
"emit",
|
||||
"--input",
|
||||
options.inputPath,
|
||||
@@ -74,7 +79,10 @@ export function emitNativeObject(options: {
|
||||
...nativeProfileHelperArgs(options.profile),
|
||||
]);
|
||||
for (const output of outputs) requireNativeArtifact(output);
|
||||
return outputs;
|
||||
return {
|
||||
outputs,
|
||||
profileMatch: options.profile?.use === undefined ? null : parseNativeProfileMatch(stdout),
|
||||
};
|
||||
}
|
||||
|
||||
/** Merges raw or indexed IR profiles into one indexed profile. */
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import { isTypePatchCode } from "../diagnostics/diagnostic.js";
|
||||
import { isPerformanceCode, renderPerformanceWarnings } from "../diagnostics/dynamic-tier.js";
|
||||
import { isNativeOptimization } from "../backend/optimization.js";
|
||||
import { renderNativeProfileMatch } from "../backend/native-profile.js";
|
||||
import {
|
||||
hostTargetCpu,
|
||||
isTargetCpuOption,
|
||||
@@ -712,6 +713,15 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
}
|
||||
}
|
||||
if (result.artifact.kind === "exe") {
|
||||
// How the profile matched the program's functions; `run` keeps stderr
|
||||
// for the program's own output.
|
||||
const profileUse = values["profile-use"];
|
||||
if (
|
||||
command === "build" &&
|
||||
profileUse !== undefined &&
|
||||
result.artifact.profileMatch !== undefined
|
||||
)
|
||||
process.stderr.write(renderNativeProfileMatch(profileUse, result.artifact.profileMatch));
|
||||
if (!values["keep-llvm"]) rmSync(result.artifact.translationUnitPath, { force: true });
|
||||
} else if (result.artifact.kind === "obj") {
|
||||
nativeLinkInfo = result.artifact.nativeLinkInfo;
|
||||
|
||||
@@ -86,8 +86,10 @@ Options:
|
||||
code is laid out, inlined and split by how the profiled
|
||||
runs used it. Code the profile does not describe, or
|
||||
describes from an older version of the program, is
|
||||
optimized as without one (release or speed; --emit=exe
|
||||
only)
|
||||
optimized as without one. build prints how the profile
|
||||
matched on stderr, and warns when more than 10% of the
|
||||
functions it describes changed (release or speed;
|
||||
--emit=exe only)
|
||||
--keep-llvm keep generated LLVM IR beside the executable (default)
|
||||
--no-keep-llvm delete generated LLVM IR after compiling
|
||||
--emit-ir also write IR beside an executable or library archive;
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "node:fs/promises";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import type { NativeProfileMatch } from "../backend/native-profile.js";
|
||||
import type { FrontendInputSnapshot } from "../frontend/input-tracker.js";
|
||||
import { frontendInputsStillMatch, validFrontendInputSnapshot } from "../frontend/input-tracker.js";
|
||||
import { compilerReleaseVersion } from "../library/sidecar.js";
|
||||
@@ -129,6 +130,8 @@ interface EarlyExecutableCacheStamp {
|
||||
native: EarlyExecutableNativeFeatures;
|
||||
/** The build's performance warnings (SC61xx), which a cache hit repeats. */
|
||||
performance?: ScrDiagnostic[];
|
||||
/** How a --profile-use build's profile matched, which a cache hit reports. */
|
||||
profileMatch?: NativeProfileMatch;
|
||||
integrity: string;
|
||||
}
|
||||
|
||||
@@ -213,12 +216,23 @@ export interface EarlyExecutableCacheHit {
|
||||
frontend: FrontendInputSnapshot;
|
||||
/** The performance warnings the build reported (SC61xx), repeated on a hit. */
|
||||
performance?: ScrDiagnostic[];
|
||||
/** How a --profile-use build's profile matched, reported again on a hit. */
|
||||
profileMatch?: NativeProfileMatch;
|
||||
}
|
||||
|
||||
export interface EarlyExecutableCachePublish extends EarlyExecutableCacheHit {
|
||||
nativeDependencies?: NativeArtifactDependency[];
|
||||
}
|
||||
|
||||
function isProfileMatch(value: unknown): value is NativeProfileMatch {
|
||||
if (typeof value !== "object" || value === null) return false;
|
||||
const match = value as Record<string, unknown>;
|
||||
return ["annotated", "mismatched", "missing", "unknownPartitions"].every((key) => {
|
||||
const count = match[key];
|
||||
return typeof count === "number" && Number.isInteger(count) && count >= 0;
|
||||
});
|
||||
}
|
||||
|
||||
const BOOLEAN_NATIVE_KEYS = [
|
||||
"regex",
|
||||
"copying",
|
||||
@@ -671,6 +685,7 @@ async function readExecutableCache(
|
||||
...(Array.isArray(stamp.performance) && stamp.performance.length > 0
|
||||
? { performance: stamp.performance }
|
||||
: {}),
|
||||
...(isProfileMatch(stamp.profileMatch) ? { profileMatch: stamp.profileMatch } : {}),
|
||||
...(ir === null ? {} : { irPath: paths.irPath }),
|
||||
...(llvmSourcePath === undefined ? {} : { llvmSourcePath }),
|
||||
...(irSourcePath === undefined ? {} : { irSourcePath }),
|
||||
@@ -861,6 +876,7 @@ export async function publishEarlyExecutableCache(
|
||||
...(result.performance === undefined || result.performance.length === 0
|
||||
? {}
|
||||
: { performance: result.performance }),
|
||||
...(result.profileMatch === undefined ? {} : { profileMatch: result.profileMatch }),
|
||||
};
|
||||
const stamp: EarlyExecutableCacheStamp = { ...unsigned, integrity: stampIntegrity(unsigned) };
|
||||
await writeFile(join(stage, "stamp.json"), `${JSON.stringify(stamp)}\n`, { mode: 0o600 });
|
||||
|
||||
@@ -529,6 +529,14 @@ function performanceWarnings(sink: CompileWarningSink | undefined): {
|
||||
return performance.length === 0 ? {} : { performance };
|
||||
}
|
||||
|
||||
/** The profile match of a --profile-use build's program object, for its
|
||||
* artifact and its executable cache entry, which a cache hit reports again. */
|
||||
function profileMatchField(object: NativeProgramObject | null): {
|
||||
profileMatch?: NativeProfileMatch;
|
||||
} {
|
||||
return object?.profileMatch === undefined ? {} : { profileMatch: object.profileMatch };
|
||||
}
|
||||
|
||||
function withCompileWarnings<T extends CompileRequestResult>(
|
||||
result: T,
|
||||
sink: CompileWarningSink,
|
||||
@@ -1675,6 +1683,7 @@ async function compileTracked(
|
||||
path: opts.outPath,
|
||||
translationUnitPath: earlyHit.llvmPath,
|
||||
backend: earlyHit.native.backend,
|
||||
...(earlyHit.profileMatch === undefined ? {} : { profileMatch: earlyHit.profileMatch }),
|
||||
},
|
||||
binaryPath: opts.outPath,
|
||||
llvmPath: earlyHit.llvmPath,
|
||||
@@ -1752,6 +1761,7 @@ async function compileTracked(
|
||||
executableRestored: true,
|
||||
nativeDependencies: dependencies,
|
||||
frontend: earlyHit.frontend,
|
||||
...profileMatchField(nativeProgramObject),
|
||||
});
|
||||
},
|
||||
nativeProgramObject?.partitionPaths,
|
||||
@@ -1797,6 +1807,7 @@ async function compileTracked(
|
||||
path: opts.outPath,
|
||||
translationUnitPath: earlyHit.llvmPath,
|
||||
backend: earlyHit.native.backend,
|
||||
...profileMatchField(nativeProgramObject),
|
||||
},
|
||||
binaryPath: opts.outPath,
|
||||
llvmPath: earlyHit.llvmPath,
|
||||
@@ -1888,6 +1899,7 @@ async function compileTracked(
|
||||
frontend: frontendInputs.snapshot(),
|
||||
...(irSourcePath === undefined ? {} : { irPath: irSourcePath }),
|
||||
...performanceWarnings(warningSink),
|
||||
...profileMatchField(nativeProgramObject),
|
||||
});
|
||||
publishedExecutable = true;
|
||||
},
|
||||
@@ -1907,6 +1919,7 @@ async function compileTracked(
|
||||
frontend: frontendInputs.snapshot(),
|
||||
...(irSourcePath === undefined ? {} : { irPath: irSourcePath }),
|
||||
...performanceWarnings(warningSink),
|
||||
...profileMatchField(nativeProgramObject),
|
||||
}).catch(() => undefined);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -1935,7 +1948,6 @@ async function compileTracked(
|
||||
}
|
||||
await pruneBuildCache(cacheRoot);
|
||||
timing("complete");
|
||||
const profileMatch = nativeProgramObject?.profileMatch;
|
||||
return {
|
||||
ok: true,
|
||||
artifact: {
|
||||
@@ -1943,7 +1955,7 @@ async function compileTracked(
|
||||
path: opts.outPath,
|
||||
translationUnitPath: llvmPath,
|
||||
backend,
|
||||
...(profileMatch === undefined ? {} : { profileMatch }),
|
||||
...profileMatchField(nativeProgramObject),
|
||||
},
|
||||
binaryPath: opts.outPath,
|
||||
llvmPath,
|
||||
|
||||
@@ -107,7 +107,7 @@ test.each([
|
||||
});
|
||||
vi.mocked(emitNativeObject).mockImplementation((options) => {
|
||||
writeFileSync(options.outputPath, "object");
|
||||
return [options.outputPath];
|
||||
return { outputs: [options.outputPath], profileMatch: null };
|
||||
});
|
||||
const cache = new NativeCache(join(directory, "cache"));
|
||||
const key = contentDigest("executable link");
|
||||
@@ -250,7 +250,7 @@ test.each([false, true])(
|
||||
expect(emitted).toContain("define i32 @main");
|
||||
if (fail) throw new Error("codegen failed");
|
||||
writeFileSync(options.outputPath, "new object");
|
||||
return [options.outputPath];
|
||||
return { outputs: [options.outputPath], profileMatch: null };
|
||||
});
|
||||
const target = MACOS_ARM64_TARGET;
|
||||
const toolchain: NativeToolchain = {
|
||||
@@ -341,7 +341,7 @@ test.skipIf(process.platform === "win32").each([false, true])(
|
||||
});
|
||||
vi.mocked(emitNativeObject).mockImplementation((options) => {
|
||||
writeFileSync(options.outputPath, "object");
|
||||
return [options.outputPath];
|
||||
return { outputs: [options.outputPath], profileMatch: null };
|
||||
});
|
||||
vi.mocked(openNativeExecutableCache).mockReturnValue(null);
|
||||
const links: string[][] = [];
|
||||
@@ -378,3 +378,84 @@ test.skipIf(process.platform === "win32").each([false, true])(
|
||||
if (lldFails) expect(links[1]).not.toContain("-fuse-ld=lld");
|
||||
},
|
||||
);
|
||||
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"a --profile-use executable reports the helper's profile match",
|
||||
() => {
|
||||
const directory = mkdtempSync(join("/tmp", "scriptc-native-profile-"));
|
||||
directories.push(directory);
|
||||
const clang = join(directory, "clang");
|
||||
writeFileSync(clang, "#!/bin/sh\n");
|
||||
chmodSync(clang, 0o755);
|
||||
const entry = join(directory, "entry.ts");
|
||||
const output = join(directory, "program");
|
||||
const profile = join(directory, "app.profdata");
|
||||
writeFileSync(profile, "indexed profile");
|
||||
const module: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: entry,
|
||||
entry: "%main",
|
||||
functions: [
|
||||
{
|
||||
name: "%main",
|
||||
params: [],
|
||||
locals: [],
|
||||
body: [],
|
||||
returnType: VOID,
|
||||
loc: { file: entry, start: 0, end: 0 },
|
||||
},
|
||||
],
|
||||
};
|
||||
vi.mocked(runNativeFrontend).mockReturnValue({
|
||||
preflight: [],
|
||||
entryText: () => "",
|
||||
entryExports: () => new Map(),
|
||||
entryContract: () => {
|
||||
throw new Error("unexpected contract request");
|
||||
},
|
||||
sourceTexts: () => new Map(),
|
||||
npmStatic: [],
|
||||
npmImportSites: new Map(),
|
||||
dispose: () => {},
|
||||
lower: () => ({
|
||||
module,
|
||||
diagnostics: [],
|
||||
runtimeFences: [],
|
||||
stats: { statementsTotal: 0, statementsFailed: 0, functionsSkipped: 0 },
|
||||
}),
|
||||
});
|
||||
const match = { annotated: 120, mismatched: 7, missing: 40, unknownPartitions: 0 };
|
||||
vi.mocked(emitNativeObject).mockImplementation((options) => {
|
||||
expect(options.profile?.use?.path).toBe(profile);
|
||||
writeFileSync(options.outputPath, "object");
|
||||
return { outputs: [options.outputPath], profileMatch: match };
|
||||
});
|
||||
vi.mocked(openNativeExecutableCache).mockReturnValue(null);
|
||||
vi.mocked(execFileSync).mockImplementation((_command, args) => {
|
||||
const list = args as string[];
|
||||
writeFileSync(list[list.indexOf("-o") + 1]!, "executable");
|
||||
return "";
|
||||
});
|
||||
const target = LINUX_X64_GNU_TARGET;
|
||||
const compiler = new NativeCompiler({
|
||||
compilerVersion: "0.0.0",
|
||||
target,
|
||||
helper: target.helper,
|
||||
ts7Executable: "unused",
|
||||
helperExecutable: "unused",
|
||||
helperPackageRoot: "unused",
|
||||
runtimePackRoot: "unused",
|
||||
linker: clang,
|
||||
linkerArgs: [],
|
||||
dsymutil: "unused",
|
||||
});
|
||||
const result = compiler.compile(entry, {
|
||||
outDir: directory,
|
||||
outPath: output,
|
||||
backend: "llvm",
|
||||
optimization: "speed",
|
||||
profileUse: profile,
|
||||
});
|
||||
expect(result).toMatchObject({ ok: true, artifact: { kind: "exe", profileMatch: match } });
|
||||
},
|
||||
);
|
||||
|
||||
@@ -63,6 +63,8 @@ import { contentDigest, fileDigest, partsDigest, type NativeCache } from "./cach
|
||||
import {
|
||||
nativeProfileKey,
|
||||
nativeProfileRefusal,
|
||||
parseNativeProfileMatch,
|
||||
type NativeProfileMatch,
|
||||
type NativeProfileOptions,
|
||||
} from "../backend/native-profile.js";
|
||||
import { splitLlvmLibraryProgram } from "../backend/llvm/split.js";
|
||||
@@ -78,6 +80,18 @@ function partitionCacheKey(key: string, index: number): string {
|
||||
return index === 0 ? key : contentDigest(`${key}:${index}`);
|
||||
}
|
||||
|
||||
/** The object cache entry that holds a cached program's profile match. */
|
||||
function profileMatchCacheKey(key: string): string {
|
||||
return contentDigest(`${key}:profile-match`);
|
||||
}
|
||||
|
||||
/** A program's native objects, and how a --profile-use build's profile
|
||||
* matched its functions. */
|
||||
interface NativeObjects {
|
||||
paths: string[];
|
||||
profileMatch?: NativeProfileMatch;
|
||||
}
|
||||
|
||||
function failure(error: unknown, entry: string, sources: Map<string, string>): CompileFailure {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -186,7 +200,7 @@ export class NativeCompiler {
|
||||
partitions = 1,
|
||||
profile?: NativeProfileOptions,
|
||||
llvmDigest: () => string = () => fileDigest(input),
|
||||
): string[] {
|
||||
): NativeObjects {
|
||||
const toolchain = this.toolchain;
|
||||
const outputs = nativePartitionPaths(output, partitions);
|
||||
const helperOptions = {
|
||||
@@ -223,7 +237,20 @@ export class NativeCompiler {
|
||||
verifyNativeHelper(helperOptions);
|
||||
if (nativeFileIdentity(toolchain.helperExecutable) === identity) {
|
||||
cached.forEach((bytes, index) => writeFileSync(outputs[index]!, bytes!));
|
||||
return outputs;
|
||||
if (profile?.use === undefined) return { paths: outputs };
|
||||
// Without the cached match, every partition's counts are unknown.
|
||||
const match = this.cache.read("object", profileMatchCacheKey(key));
|
||||
return {
|
||||
paths: outputs,
|
||||
profileMatch: (match === null
|
||||
? null
|
||||
: parseNativeProfileMatch(match.toString("utf8"))) ?? {
|
||||
annotated: 0,
|
||||
mismatched: 0,
|
||||
missing: 0,
|
||||
unknownPartitions: outputs.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
key = null;
|
||||
}
|
||||
@@ -235,7 +262,7 @@ export class NativeCompiler {
|
||||
this.cache === null || identity === null || partitions < 2
|
||||
? null
|
||||
: this.cache.partitionDirectory(identity);
|
||||
emitNativeObject({
|
||||
const { profileMatch } = emitNativeObject({
|
||||
executable: toolchain.helperExecutable,
|
||||
packageRoot: toolchain.helperPackageRoot,
|
||||
compilerVersion: toolchain.compilerVersion,
|
||||
@@ -252,15 +279,30 @@ export class NativeCompiler {
|
||||
});
|
||||
if (this.cache !== null && key !== null) {
|
||||
try {
|
||||
if (nativeFileIdentity(toolchain.helperExecutable) === identity)
|
||||
if (nativeFileIdentity(toolchain.helperExecutable) === identity) {
|
||||
// The match goes in first: a reader that finds every partition finds it too.
|
||||
if (profileMatch !== null)
|
||||
this.cache.write(
|
||||
"object",
|
||||
profileMatchCacheKey(key),
|
||||
`${JSON.stringify({
|
||||
profile: {
|
||||
annotated: profileMatch.annotated,
|
||||
mismatched: profileMatch.mismatched,
|
||||
missing: profileMatch.missing,
|
||||
unknown_partitions: profileMatch.unknownPartitions,
|
||||
},
|
||||
})}\n`,
|
||||
);
|
||||
outputs.forEach((path, index) =>
|
||||
this.cache!.write("object", partitionCacheKey(key!, index), readFileSync(path)),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
/* An updated helper or unavailable cache only prevents reuse. */
|
||||
}
|
||||
}
|
||||
return outputs;
|
||||
return profileMatch === null ? { paths: outputs } : { paths: outputs, profileMatch };
|
||||
}
|
||||
|
||||
private emitProgramObject(
|
||||
@@ -272,7 +314,7 @@ export class NativeCompiler {
|
||||
library: boolean,
|
||||
profile?: NativeProfileOptions,
|
||||
llvmDigest?: () => string,
|
||||
): string[] {
|
||||
): NativeObjects {
|
||||
// Library archives merge dev shards relocatably; executables link the
|
||||
// helper's program partitions directly.
|
||||
const llvm =
|
||||
@@ -303,7 +345,7 @@ export class NativeCompiler {
|
||||
}
|
||||
const merged = localizeNativeLibrary(this.toolchain, stage, objects, [], split.publicSymbols);
|
||||
renameSync(merged, output);
|
||||
return [output];
|
||||
return { paths: [output] };
|
||||
}
|
||||
|
||||
compile(entry: string, options: CompileRequestOptions): CompileRequestResult {
|
||||
@@ -427,6 +469,9 @@ export class NativeCompiler {
|
||||
const kept = executableCache?.keep(output) ?? false;
|
||||
const restored = kept || (executableCache?.restore(stagedOutput) ?? false);
|
||||
timing(restored ? "executable-cache-hit" : "executable-cache-miss");
|
||||
// The executable cache leaves out --profile-use builds, so this is set
|
||||
// whenever the build has a profile to report.
|
||||
let profileMatch: NativeProfileMatch | undefined;
|
||||
if (!restored) {
|
||||
// A missing or invalid runtime pack fails before code generation.
|
||||
if (outputKind === "exe") selectPack();
|
||||
@@ -440,8 +485,8 @@ export class NativeCompiler {
|
||||
object,
|
||||
]);
|
||||
requireNativeArtifact(object);
|
||||
} else if (outputKind === "exe")
|
||||
programObjects = this.emitProgramObject(
|
||||
} else if (outputKind === "exe") {
|
||||
const objects = this.emitProgramObject(
|
||||
llvmInput,
|
||||
object,
|
||||
entry,
|
||||
@@ -451,7 +496,9 @@ export class NativeCompiler {
|
||||
profile,
|
||||
digestLlvm,
|
||||
);
|
||||
else
|
||||
programObjects = objects.paths;
|
||||
profileMatch = objects.profileMatch;
|
||||
} else
|
||||
this.emitObject(
|
||||
llvmInput,
|
||||
object,
|
||||
@@ -578,7 +625,13 @@ export class NativeCompiler {
|
||||
rmSync(output + ".dSYM", { recursive: true, force: true });
|
||||
return {
|
||||
ok: true,
|
||||
artifact: { kind: "exe", path: output, translationUnitPath: llvmPath, backend: "llvm" },
|
||||
artifact: {
|
||||
kind: "exe",
|
||||
path: output,
|
||||
translationUnitPath: llvmPath,
|
||||
backend: "llvm",
|
||||
...(profileMatch === undefined ? {} : { profileMatch }),
|
||||
},
|
||||
binaryPath: output,
|
||||
llvmPath,
|
||||
backend: "llvm",
|
||||
|
||||
Reference in New Issue
Block a user