mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
A build now names the functions of the program's own files that compile
to the dynamic tier and likely run often, so a slow program with a little
untyped code says where the time goes:
warning SC6101: 3 functions that likely run often use untyped values, which runs slower than typed code:
advance (nbody.js:22): untyped JavaScript file, in a loop
...
- The functions come from the tier assignment lowering already makes
(Lowerer.dynamicFunctions). Without a profile, a function counts as
likely hot when a dynamic operation runs inside one of its loops, when
a loop calls it, or when it calls itself (lowering/dynamic-tier.ts).
- A function is named only when its source says why its values are
untyped: an `any` (for a for...of variable, the value it iterates), an
untyped JSON.parse result, or an untyped JavaScript file. Generated
%dyn.* helpers, module top levels and functions that are dynamic for an
operation without a static lowering are left out.
- At most five functions are named, most likely hot first, with the
total; dynamic-tier code in npm packages is counted per package on one
line. All of the warning's text is in diagnostics/dynamic-tier.ts.
- SC6101 opens the SC61xx band, performance notes, with a new
"performance" category in the diagnostics envelope; SC60xx stay the
divergences.
- scriptc build prints it after the divergence warnings, and the
executable cache stores it with the entry, so a cached build (including
the CLI's routed fast path) repeats it. scriptc coverage reports it, and
a comment-only edit no longer replays a verdict that carries it, since
its text holds line numbers.
- --no-dynamic-tier-warning leaves it out of build and coverage;
coverage --fail-on=dynamic fails on it.
- Docs: coverage.mdx (Dynamic tier, Gating builds and CI, the envelope
categories) and cli.mdx.
121 lines
4.7 KiB
TypeScript
121 lines
4.7 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { createRequire } from "node:module";
|
|
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { expect, test } from "vitest";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const require = createRequire(import.meta.url);
|
|
const repoRoot = join(import.meta.dirname, "../../..");
|
|
const cliEntry = join(repoRoot, "packages/cli/src/main.ts");
|
|
const tsxLoader = join(dirname(require.resolve("tsx/package.json")), "dist/loader.mjs");
|
|
const fixtures = join(repoRoot, "tests/coverage-fixtures/dynamic-tier");
|
|
|
|
const HEADER =
|
|
"warning SC6101: 3 functions that likely run often use untyped values, which runs slower than typed code:";
|
|
const CLOSING =
|
|
"The program compiled. Adding types to these functions lets scriptc compile them to faster code.";
|
|
|
|
async function cli(
|
|
args: string[],
|
|
cacheRoot: string,
|
|
): Promise<{ code: number; stdout: string; stderr: string }> {
|
|
try {
|
|
const { stdout, stderr } = await execFileAsync(
|
|
process.execPath,
|
|
["--import", tsxLoader, cliEntry, ...args],
|
|
{
|
|
env: { ...process.env, SCRIPTC_CACHE_DIR: cacheRoot, SCRIPTC_TIMING: "1" },
|
|
maxBuffer: 4 * 1024 * 1024,
|
|
},
|
|
);
|
|
return { code: 0, stdout, stderr };
|
|
} catch (err) {
|
|
const e = err as { code?: number; stdout?: string; stderr?: string };
|
|
return { code: e.code ?? 1, stdout: e.stdout ?? "", stderr: e.stderr ?? "" };
|
|
}
|
|
}
|
|
|
|
test("a build names its likely-hot untyped functions, also when it comes from the cache", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "scriptc-dynamic-tier-warning-"));
|
|
const cacheRoot = join(dir, "cache");
|
|
const outPath = join(dir, process.platform === "win32" ? "program.exe" : "program");
|
|
try {
|
|
await mkdir(cacheRoot, { mode: 0o700 });
|
|
const build = (extra: string[] = []) =>
|
|
cli(["build", join(fixtures, "hot.js"), "-o", outPath, ...extra], cacheRoot);
|
|
|
|
const first = await build();
|
|
expect(first.code).toBe(0);
|
|
expect(first.stderr).toContain("scriptc lowering");
|
|
expect(first.stderr).toContain(
|
|
[
|
|
HEADER,
|
|
" centroid (hot.js:7): untyped JavaScript file, in a loop",
|
|
" spread (hot.js:23): untyped JavaScript file, in a loop",
|
|
" distance (hot.js:17): untyped JavaScript file, called in a loop",
|
|
" hint: add types to compile these statically; see scriptc coverage --explain-dynamic",
|
|
"",
|
|
CLOSING,
|
|
].join("\n"),
|
|
);
|
|
|
|
// The cache hit skips lowering and repeats the warning.
|
|
const cached = await build();
|
|
expect(cached.code).toBe(0);
|
|
expect(cached.stderr).not.toContain("scriptc lowering");
|
|
expect(cached.stderr).toContain(HEADER);
|
|
expect(cached.stderr).toContain(CLOSING);
|
|
|
|
const silenced = await build(["--no-dynamic-tier-warning"]);
|
|
expect(silenced.code).toBe(0);
|
|
expect(silenced.stderr).not.toContain("SC6101");
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
}, 300_000);
|
|
|
|
test("untyped setup code builds without the warning", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "scriptc-dynamic-tier-quiet-"));
|
|
try {
|
|
await mkdir(join(dir, "cache"), { mode: 0o700 });
|
|
const result = await cli(
|
|
["build", join(fixtures, "quiet.ts"), "-o", join(dir, "program")],
|
|
join(dir, "cache"),
|
|
);
|
|
expect(result.code).toBe(0);
|
|
expect(result.stderr).not.toContain("SC6101");
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
}, 300_000);
|
|
|
|
test("coverage --fail-on=dynamic fails on the warning, and --no-dynamic-tier-warning cannot hide it", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "scriptc-dynamic-tier-fail-on-"));
|
|
try {
|
|
await mkdir(join(dir, "cache"), { mode: 0o700 });
|
|
const coverage = (file: string, extra: string[]) =>
|
|
cli(["coverage", join(fixtures, file), ...extra], join(dir, "cache"));
|
|
|
|
const hot = await coverage("hot.js", ["--fail-on=dynamic"]);
|
|
expect(hot.code).toBe(1);
|
|
expect(hot.stdout).toContain("untyped and likely hot");
|
|
expect((await coverage("hot.js", ["--fail-on=divergences"])).code).toBe(0);
|
|
expect((await coverage("quiet.ts", ["--fail-on=dynamic"])).code).toBe(0);
|
|
|
|
const silenced = await coverage("hot.js", ["--no-dynamic-tier-warning"]);
|
|
expect(silenced.code).toBe(0);
|
|
expect(silenced.stdout).not.toContain("untyped and likely hot");
|
|
|
|
const conflict = await coverage("hot.js", ["--fail-on=dynamic", "--no-dynamic-tier-warning"]);
|
|
expect(conflict.code).toBe(1);
|
|
expect(conflict.stderr).toContain(
|
|
"--fail-on=dynamic checks the warning --no-dynamic-tier-warning leaves out",
|
|
);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
}, 300_000);
|