mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
perf(cli): load each command's code only when that command runs (#4656)
* perf(cli): load each command's code only when that command runs * fix(cli): honest bench percentiles, and no unused registry entry in the split build * fix(cli): the startup bench is a package script and stops on a failed run * style(cli): format the bench's failed-run check * fix(cli): a notice module that fails to load cannot fail the command
This commit is contained in:
@@ -27,7 +27,8 @@
|
||||
"build:copy": "node scripts/build-copy.mjs",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"gen:skills-manifest": "tsx scripts/gen-skills-manifest.ts",
|
||||
"gen:agent-dirs": "tsx scripts/sync-agent-dirs.ts"
|
||||
"gen:agent-dirs": "tsx scripts/sync-agent-dirs.ts",
|
||||
"bench:startup": "node scripts/bench-startup.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^2.0.5",
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env node
|
||||
// Times whole CLI runs (spawn to exit) and prints a markdown table of median and p90.
|
||||
// Usage: node scripts/bench-startup.mjs <project-dir> [--runs 10] [--cli bin/hyperframes.mjs] [--no-telemetry]
|
||||
// Runs with CI=1 (no update check); telemetry stays on unless --no-telemetry, so each run sends real events.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
const { values, positionals } = parseArgs({
|
||||
allowPositionals: true,
|
||||
options: {
|
||||
runs: { type: "string", default: "10" },
|
||||
cli: { type: "string" },
|
||||
"no-telemetry": { type: "boolean", default: false },
|
||||
},
|
||||
});
|
||||
const project = resolve(positionals[0] ?? ".");
|
||||
const runs = Number(values.runs);
|
||||
if (!Number.isInteger(runs) || runs < 1) throw new Error("--runs must be a positive integer");
|
||||
const cli = resolve(
|
||||
values.cli ?? fileURLToPath(new URL("../bin/hyperframes.mjs", import.meta.url)),
|
||||
);
|
||||
const env = { ...process.env, CI: "1" };
|
||||
if (values["no-telemetry"]) env.HYPERFRAMES_NO_TELEMETRY = "1";
|
||||
|
||||
const commands = [["--help"], ["lint", "--help"], ["lint"], ["compositions"], ["info"]];
|
||||
// Nearest rank: p90 of 10 runs is the 9th fastest, not the slowest.
|
||||
const percentile = (sorted, p) => sorted[Math.ceil(p * sorted.length) - 1];
|
||||
|
||||
function timeRun(command) {
|
||||
const start = process.hrtime.bigint();
|
||||
const result = spawnSync(process.execPath, [cli, ...command], {
|
||||
cwd: project,
|
||||
env,
|
||||
stdio: "ignore",
|
||||
});
|
||||
const seconds = Number(process.hrtime.bigint() - start) / 1e9;
|
||||
// A failed run times nothing worth reporting.
|
||||
if (result.status !== 0)
|
||||
throw result.error ?? new Error(`${command.join(" ")} exited ${result.status}`);
|
||||
return seconds;
|
||||
}
|
||||
|
||||
console.log(`| command | median (s) | p90 (s) |\n|---|---|---|`);
|
||||
for (const command of commands) {
|
||||
timeRun(command); // warm-up: fills the OS file cache, not counted
|
||||
const times = Array.from({ length: runs }, () => timeRun(command)).sort((a, b) => a - b);
|
||||
console.log(
|
||||
`| ${command.join(" ")} | ${percentile(times, 0.5).toFixed(2)} | ${percentile(times, 0.9).toFixed(2)} |`,
|
||||
);
|
||||
}
|
||||
@@ -102,10 +102,6 @@ async function main() {
|
||||
join(mediaEngine, "resolve.mjs"),
|
||||
join(DIST, "skills", "media-use", "scripts", "resolve.mjs"),
|
||||
);
|
||||
mkdirSync(join(DIST, "skills", "registry"), { recursive: true });
|
||||
copyDirContents(join(DIST, "registry"), join(DIST, "skills", "registry"));
|
||||
mkdirSync(join(DIST, "skills", "media-use", "registry"), { recursive: true });
|
||||
copyDirContents(join(DIST, "registry"), join(DIST, "skills", "media-use", "registry"));
|
||||
|
||||
const dockerfile = join(CLI_ROOT, "src", "docker", "Dockerfile.render");
|
||||
if (existsSync(dockerfile)) {
|
||||
|
||||
+17
-13
@@ -26,7 +26,7 @@ for (const stream of [process.stdout, process.stderr]) {
|
||||
// The shaderTransitionWorkerPool lives in the producer package and resolves
|
||||
// its worker entry by probing for a sibling `.js` file next to
|
||||
// `import.meta.url`. When this CLI is bundled by tsup, the producer code is
|
||||
// inlined into `cli.js`, but `import.meta.url` resolves to the producer's
|
||||
// bundled into chunks beside cli.js, but `import.meta.url` resolves to the producer's
|
||||
// own dist path (NOT cli.js) on some module-graph layouts — so the sibling
|
||||
// probe lands in a directory that does not contain the bundled worker.
|
||||
// We emit the worker entry next to cli.js (see tsup.config.ts) and tell
|
||||
@@ -279,20 +279,24 @@ if (
|
||||
// we don't over-print.
|
||||
import("./utils/autoUpdate.js").then((mod) => mod.reportCompletedUpdate()).catch(() => {});
|
||||
|
||||
import("./utils/updateCheck.js").then(async (mod) => {
|
||||
_printUpdateNotice = mod.printUpdateNotice;
|
||||
_printStalePinNotice = mod.printStalePinNotice;
|
||||
const result = mod.cachedUpdateCheck();
|
||||
if (result.updateAvailable) {
|
||||
const auto = await import("./utils/autoUpdate.js").catch(() => null);
|
||||
auto?.scheduleBackgroundInstall(result.latest, result.current);
|
||||
}
|
||||
});
|
||||
import("./utils/updateCheck.js")
|
||||
.then(async (mod) => {
|
||||
_printUpdateNotice = mod.printUpdateNotice;
|
||||
_printStalePinNotice = mod.printStalePinNotice;
|
||||
const result = mod.cachedUpdateCheck();
|
||||
if (result.updateAvailable) {
|
||||
const auto = await import("./utils/autoUpdate.js").catch(() => null);
|
||||
auto?.scheduleBackgroundInstall(result.latest, result.current);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// Skills freshness nudge — same gating as the CLI self-update notice.
|
||||
import("./utils/skillsUpdateCheck.js").then((mod) => {
|
||||
_printSkillsUpdateNotice = mod.printSkillsUpdateNotice;
|
||||
});
|
||||
import("./utils/skillsUpdateCheck.js")
|
||||
.then((mod) => {
|
||||
_printSkillsUpdateNotice = mod.printSkillsUpdateNotice;
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// The notices read the caches; a detached child refreshes them for the next run.
|
||||
import("./utils/backgroundChecks.js").then((mod) => mod.launchBackgroundChecks()).catch(() => {});
|
||||
|
||||
@@ -19,14 +19,13 @@ export default defineConfig({
|
||||
backgroundChecksWorker: "src/backgroundChecksWorker.ts",
|
||||
sherpaWorker: "src/whisper/sherpaWorker.ts",
|
||||
shaderTransitionWorker: "../producer/src/services/shaderTransitionWorker.ts",
|
||||
"registry/localSemantic": "src/registry/localSemantic.ts",
|
||||
},
|
||||
format: ["esm"],
|
||||
outDir: "dist",
|
||||
target: "node22",
|
||||
platform: "node",
|
||||
bundle: true,
|
||||
splitting: false,
|
||||
splitting: true,
|
||||
sourcemap: false,
|
||||
clean: true,
|
||||
banner: {
|
||||
|
||||
Reference in New Issue
Block a user