Files
healer-man/scripts/asset-pipeline/audit-environment-performance.mjs
T
2026-08-22 15:53:43 -04:00

122 lines
4.2 KiB
JavaScript

#!/usr/bin/env node
import { access, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { analyzeGlbFile } from "./glb-performance.mjs";
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const baselineFile = path.join(
projectRoot,
"scripts/asset-pipeline/environment-performance-baseline.json",
);
const defaultRoots = [
path.join(projectRoot, "public/assets/game/manastorm"),
path.join(projectRoot, "src/assets/game/dungeons"),
];
const defaultThresholds = {
maxEstimatedDrawCalls: 1_500,
maxUnbatchedDrawCallSavings: 250,
};
async function exists(file) {
try {
await access(file);
return true;
} catch {
return false;
}
}
async function collectVisualGlbs(root, files = []) {
if (!await exists(root)) return files;
const entry = await stat(root);
if (entry.isFile()) {
if (/(?:^|[-_])visual\.glb$/i.test(path.basename(root))) files.push(root);
return files;
}
for (const child of await readdir(root, { withFileTypes: true })) {
const target = path.join(root, child.name);
if (child.isDirectory()) await collectVisualGlbs(target, files);
else if (/(?:^|[-_])visual\.glb$/i.test(child.name)) files.push(target);
}
return files;
}
function projectPath(file) {
return path.relative(projectRoot, file).split(path.sep).join("/");
}
function exceeds(report, limits) {
return report.estimatedDrawCalls > limits.maxEstimatedDrawCalls
|| report.unbatchedDrawCallSavings > limits.maxUnbatchedDrawCallSavings;
}
const args = process.argv.slice(2);
const writeBaseline = args.includes("--write-baseline");
const roots = args.filter((argument) => argument !== "--write-baseline")
.map((argument) => path.resolve(projectRoot, argument));
const files = [...new Set((await Promise.all(
(roots.length ? roots : defaultRoots).map((root) => collectVisualGlbs(root)),
)).flat())].sort();
const reports = await Promise.all(files.map(async (file) => ({
file: projectPath(file),
byteLength: (await stat(file)).size,
...(await analyzeGlbFile(file)),
})));
if (writeBaseline) {
const baseline = {
schemaVersion: 1,
thresholds: defaultThresholds,
exceptions: Object.fromEntries(
reports
.filter((report) => exceeds(report, defaultThresholds))
.map((report) => [report.file, {
maxEstimatedDrawCalls: Math.max(defaultThresholds.maxEstimatedDrawCalls, report.estimatedDrawCalls),
maxUnbatchedDrawCallSavings: Math.max(
defaultThresholds.maxUnbatchedDrawCallSavings,
report.unbatchedDrawCallSavings,
),
}]),
),
};
await mkdir(path.dirname(baselineFile), { recursive: true });
await writeFile(baselineFile, `${JSON.stringify(baseline, null, 2)}\n`, "utf8");
console.log(`Wrote ${Object.keys(baseline.exceptions).length} performance exceptions to ${projectPath(baselineFile)}.`);
process.exit(0);
}
if (!await exists(baselineFile)) {
throw new Error(`Missing ${projectPath(baselineFile)}. Run this script with --write-baseline.`);
}
const baseline = JSON.parse(await readFile(baselineFile, "utf8"));
const thresholds = { ...defaultThresholds, ...baseline.thresholds };
const regressions = reports.filter((report) => {
const limits = { ...thresholds, ...baseline.exceptions?.[report.file] };
return exceeds(report, limits);
});
const debt = reports.filter((report) => exceeds(report, thresholds));
const top = [...reports]
.sort((left, right) => right.estimatedDrawCalls - left.estimatedDrawCalls)
.slice(0, 12)
.map((report) => ({
asset: report.file,
calls: report.estimatedDrawCalls,
avoidable: report.unbatchedDrawCallSavings,
gpuBatches: report.gpuInstanceBatches,
nodes: report.nodes,
}));
console.table(top);
console.log(JSON.stringify({
status: regressions.length ? "blocked" : "green",
assets: reports.length,
knownBudgetExceptions: debt.length,
regressions: regressions.map((report) => ({
file: report.file,
estimatedDrawCalls: report.estimatedDrawCalls,
unbatchedDrawCallSavings: report.unbatchedDrawCallSavings,
})),
}, null, 2));
if (regressions.length) process.exitCode = 1;