perf(web): skip unchanged incremental web builds
This commit is contained in:
@@ -23,6 +23,9 @@
|
||||
- Emscripten 环境、无人值守启动器和原生构建入口统一使用自适应并行度,避免在高配机器上固定 `-j8` 或 `-j2`。
|
||||
- `web/tsconfig.json` 开启增量图和 `.cache/tsconfig.tsbuildinfo`,使独立
|
||||
`typecheck` 与后续 `build` 共享 TypeScript 解析结果。
|
||||
- `npm run typecheck` 和 `npm run build` 使用源码/配置指纹。指纹未变化且
|
||||
构件存在时输出 `web-typecheck-reused`/`web-build-reused`,不重复启动
|
||||
TypeScript 或 Vite;任一输入变化都会自动回到完整增量构建路径。
|
||||
- 调度器状态显示为 WASM 构件增加 `bytes + mtime_ns` 指纹缓存;未变化时不再重复读取并计算 SHA-256。
|
||||
- 状态页从既有收据计算并显示当前阶段中位数最高的观测瓶颈,便于下一轮只调整真正耗时的阶段。
|
||||
|
||||
|
||||
23
tools/web/incremental-web-build.mjs
Normal file
23
tools/web/incremental-web-build.mjs
Normal file
@@ -0,0 +1,23 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import { fingerprint, readState, root, writeState } from "./web-build-fingerprint.mjs";
|
||||
|
||||
const currentFingerprint = fingerprint();
|
||||
const state = readState("web-build-success.json");
|
||||
const index = `${root}/web/dist/index.html`;
|
||||
if (state?.fingerprint === currentFingerprint && fs.existsSync(index)) {
|
||||
process.stdout.write(`web-build-reused fingerprint=${currentFingerprint}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const typecheck = spawnSync(process.execPath, ["tools/web/run-incremental-typecheck.mjs"], {
|
||||
cwd: root,
|
||||
stdio: "inherit",
|
||||
});
|
||||
if (typecheck.status !== 0) process.exit(typecheck.status ?? 1);
|
||||
const vite = spawnSync(process.execPath, ["./node_modules/vite/bin/vite.js", "build", "--config", "app/vite.config.ts"], {
|
||||
cwd: `${root}/web`,
|
||||
stdio: "inherit",
|
||||
});
|
||||
if (vite.status !== 0) process.exit(vite.status ?? 1);
|
||||
writeState("web-build-success.json", { schemaVersion: 1, fingerprint: currentFingerprint });
|
||||
18
tools/web/run-incremental-typecheck.mjs
Normal file
18
tools/web/run-incremental-typecheck.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import { fingerprint, readState, root, statePath, writeState } from "./web-build-fingerprint.mjs";
|
||||
|
||||
const currentFingerprint = fingerprint();
|
||||
const state = readState("typecheck-success.json");
|
||||
const buildInfo = statePath("tsconfig.tsbuildinfo");
|
||||
if (state?.fingerprint === currentFingerprint && fs.existsSync(buildInfo)) {
|
||||
process.stdout.write(`web-typecheck-reused fingerprint=${currentFingerprint}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const result = spawnSync(process.execPath, ["./node_modules/typescript/bin/tsc", "-p", "tsconfig.json"], {
|
||||
cwd: root + "/web",
|
||||
stdio: "inherit",
|
||||
});
|
||||
if (result.status !== 0) process.exit(result.status ?? 1);
|
||||
writeState("typecheck-success.json", { schemaVersion: 1, fingerprint: currentFingerprint });
|
||||
61
tools/web/web-build-fingerprint.mjs
Normal file
61
tools/web/web-build-fingerprint.mjs
Normal file
@@ -0,0 +1,61 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const webRoot = path.join(root, "web");
|
||||
|
||||
function walk(directory, output) {
|
||||
if (!fs.existsSync(directory)) return;
|
||||
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||||
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".cache" || entry.name === ".vite") continue;
|
||||
const file = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) walk(file, output);
|
||||
else if (entry.isFile()) output.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
export function inputFiles() {
|
||||
const files = [
|
||||
path.join(webRoot, "package.json"),
|
||||
path.join(webRoot, "package-lock.json"),
|
||||
path.join(webRoot, "tsconfig.json"),
|
||||
path.join(webRoot, "app/vite.config.ts"),
|
||||
];
|
||||
walk(path.join(webRoot, "app/src"), files);
|
||||
walk(path.join(webRoot, "protocol"), files);
|
||||
walk(path.join(webRoot, "app/public"), files);
|
||||
return [...new Set(files)].filter((file) => fs.existsSync(file)).sort();
|
||||
}
|
||||
|
||||
export function fingerprint() {
|
||||
const hash = crypto.createHash("sha256");
|
||||
for (const file of inputFiles()) {
|
||||
const stat = fs.statSync(file);
|
||||
hash.update(path.relative(root, file));
|
||||
hash.update("\0");
|
||||
hash.update(String(stat.size));
|
||||
hash.update("\0");
|
||||
hash.update(String(stat.mtimeNs));
|
||||
hash.update("\n");
|
||||
}
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
export function statePath(name) {
|
||||
return path.join(webRoot, ".cache", name);
|
||||
}
|
||||
|
||||
export function readState(name) {
|
||||
try { return JSON.parse(fs.readFileSync(statePath(name), "utf8")); }
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
export function writeState(name, value) {
|
||||
const file = statePath(name);
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
const temporary = `${file}.tmp-${process.pid}`;
|
||||
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`);
|
||||
fs.renameSync(temporary, file);
|
||||
}
|
||||
@@ -5,8 +5,8 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --config app/vite.config.ts",
|
||||
"build": "tsc -p tsconfig.json && vite build --config app/vite.config.ts",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"build": "node ../tools/web/incremental-web-build.mjs",
|
||||
"typecheck": "node ../tools/web/run-incremental-typecheck.mjs",
|
||||
"lint": "eslint app",
|
||||
"check:local-deps": "bash ../tools/web/check-local-deps.sh",
|
||||
"test": "node --test tests/**/*.test.mjs",
|
||||
|
||||
Reference in New Issue
Block a user