95 lines
3.5 KiB
TypeScript
95 lines
3.5 KiB
TypeScript
#!/usr/bin/env tsx
|
|
/* eslint-disable no-console */
|
|
import { readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { performance } from "node:perf_hooks";
|
|
import { spawnSync } from "node:child_process";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { i18nNamespaces } from "../packages/i18n/src/index.ts";
|
|
|
|
type Measurement = {
|
|
readonly locales: readonly string[];
|
|
readonly loadMs: number;
|
|
readonly rssDeltaMb: number;
|
|
readonly sourceMb: number;
|
|
};
|
|
|
|
const baselineLocales = ["de", "en"] as const;
|
|
const fullLocales = ["de", "en", "pl", "ru", "uk", "hu"] as const;
|
|
const maxAdditionalLoadMs = 250;
|
|
const maxAdditionalRssMb = 30;
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const childArgument = process.argv.find((argument) => argument.startsWith("--measure="));
|
|
|
|
if (childArgument) {
|
|
measureChild(childArgument.slice("--measure=".length).split(","));
|
|
} else {
|
|
const baseline = medianMeasurement(baselineLocales);
|
|
const full = medianMeasurement(fullLocales);
|
|
const additionalLoadMs = round(Math.max(0, full.loadMs - baseline.loadMs));
|
|
const additionalRssMb = round(Math.max(0, full.rssDeltaMb - baseline.rssDeltaMb));
|
|
const result = {
|
|
baseline,
|
|
full,
|
|
delta: { loadMs: additionalLoadMs, rssMb: additionalRssMb },
|
|
thresholds: { loadMs: maxAdditionalLoadMs, rssMb: maxAdditionalRssMb },
|
|
};
|
|
|
|
console.log(JSON.stringify(result, null, 2));
|
|
if (additionalLoadMs > maxAdditionalLoadMs || additionalRssMb > maxAdditionalRssMb) {
|
|
console.error("FAIL i18n catalog performance: switch to locale-scoped cached loading before release.");
|
|
process.exit(1);
|
|
}
|
|
console.log("PASS i18n catalog performance");
|
|
}
|
|
|
|
function measureChild(locales: readonly string[]): never {
|
|
global.gc?.();
|
|
const rssBefore = process.memoryUsage().rss;
|
|
const startedAt = performance.now();
|
|
let sourceBytes = 0;
|
|
const resources: Record<string, Record<string, unknown>> = {};
|
|
|
|
for (const locale of locales.flatMap((candidate) => [candidate, `${candidate}-formal`])) {
|
|
resources[locale] = {};
|
|
for (const namespace of i18nNamespaces) {
|
|
const source = readFileSync(path.join(root, "apps/web/public/locales", locale, `${namespace}.json`), "utf8");
|
|
sourceBytes += Buffer.byteLength(source);
|
|
resources[locale]![namespace] = JSON.parse(source) as unknown;
|
|
}
|
|
}
|
|
|
|
const loadMs = performance.now() - startedAt;
|
|
global.gc?.();
|
|
const rssAfter = process.memoryUsage().rss;
|
|
if (Object.keys(resources).length === 0) throw new Error("No i18n resources loaded.");
|
|
const result: Measurement = {
|
|
locales,
|
|
loadMs: round(loadMs),
|
|
rssDeltaMb: round((rssAfter - rssBefore) / 1024 / 1024),
|
|
sourceMb: round(sourceBytes / 1024 / 1024),
|
|
};
|
|
process.stdout.write(JSON.stringify(result));
|
|
process.exit(0);
|
|
}
|
|
|
|
function medianMeasurement(locales: readonly string[]): Measurement {
|
|
const samples = Array.from({ length: 3 }, () => runMeasurement(locales)).sort((left, right) => left.loadMs - right.loadMs);
|
|
return samples[1]!;
|
|
}
|
|
|
|
function runMeasurement(locales: readonly string[]): Measurement {
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
["--expose-gc", "--import", "tsx", fileURLToPath(import.meta.url), `--measure=${locales.join(",")}`],
|
|
{ cwd: root, encoding: "utf8" }
|
|
);
|
|
if (result.status !== 0) throw new Error(result.stderr || `i18n catalog measurement exited with ${result.status}`);
|
|
return JSON.parse(result.stdout) as Measurement;
|
|
}
|
|
|
|
function round(value: number): number {
|
|
return Math.round(value * 100) / 100;
|
|
}
|