110 lines
3.3 KiB
TypeScript
110 lines
3.3 KiB
TypeScript
/**
|
||
* Shared paths, performance measurement, and optional JSON reports for vitest.
|
||
*/
|
||
|
||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
|
||
/** Directory of this file (`tests/`). */
|
||
export const testsDir = __dirname;
|
||
|
||
export const fixturesDir = path.join(testsDir, 'fixtures');
|
||
|
||
/** Default JSON output: `packages/xblox/reports/perf-latest.json` (gitignored). */
|
||
export const defaultPerfReportPath = path.join(testsDir, '..', 'reports', 'perf-latest.json');
|
||
|
||
export function fixture(...parts: string[]): string {
|
||
return path.join(fixturesDir, ...parts);
|
||
}
|
||
|
||
// ——— performance ———
|
||
|
||
export type PerfEntry = {
|
||
/** Logical label (e.g. suite step name). */
|
||
name: string;
|
||
/** Duration in milliseconds (`performance.now()` delta). */
|
||
ms: number;
|
||
/** ISO timestamp when the measurement finished. */
|
||
at: string;
|
||
};
|
||
|
||
/** When set to `1`, `PerfReporter.writeJsonFile()` writes to disk (see `npm run test:perf`). */
|
||
export function isPerfReportEnabled(): boolean {
|
||
return process.env.XBLOX_PERF_REPORT === '1';
|
||
}
|
||
|
||
/**
|
||
* Measure sync or async work; returns the function result and duration.
|
||
* Does not record to a report — use `PerfReporter.measure` for that.
|
||
*/
|
||
export async function measureMs<T>(fn: () => T | Promise<T>): Promise<{ result: T; ms: number }> {
|
||
const t0 = performance.now();
|
||
const result = await fn();
|
||
const ms = performance.now() - t0;
|
||
return { result, ms };
|
||
}
|
||
|
||
/**
|
||
* Collect timings for a test file or suite. Call `printSummary()` in `afterAll`,
|
||
* and optionally `writeJsonFile()` when `XBLOX_PERF_REPORT=1`.
|
||
*/
|
||
export class PerfReporter {
|
||
private readonly entries: PerfEntry[] = [];
|
||
|
||
get snapshot(): readonly PerfEntry[] {
|
||
return [...this.entries];
|
||
}
|
||
|
||
/** Run `fn`, record duration under `name`, return `fn`’s result. */
|
||
async measure<T>(name: string, fn: () => T | Promise<T>): Promise<T> {
|
||
const { result, ms } = await measureMs(fn);
|
||
this.entries.push({
|
||
name,
|
||
ms,
|
||
at: new Date().toISOString(),
|
||
});
|
||
return result;
|
||
}
|
||
|
||
/** Log a compact table to stdout (Vitest / CI). */
|
||
printSummary(title = 'xblox perf'): void {
|
||
if (this.entries.length === 0) {
|
||
return;
|
||
}
|
||
const rows = this.entries.map((e) => ({
|
||
name: e.name,
|
||
ms: Number(e.ms.toFixed(3)),
|
||
at: e.at,
|
||
}));
|
||
console.info(`\n[${title}]`);
|
||
console.table(rows);
|
||
const total = this.entries.reduce((s, e) => s + e.ms, 0);
|
||
console.info(`[${title}] total: ${total.toFixed(3)} ms\n`);
|
||
}
|
||
|
||
/**
|
||
* Write `reports/perf-latest.json` (or `path`). Safe to call from `afterAll`.
|
||
* Set env `XBLOX_PERF_REPORT=1` to enable writes; otherwise no-op (avoids churn in CI).
|
||
*/
|
||
writeJsonFile(filePath: string = defaultPerfReportPath): string | undefined {
|
||
if (!isPerfReportEnabled()) {
|
||
return undefined;
|
||
}
|
||
mkdirSync(path.dirname(filePath), { recursive: true });
|
||
const payload = {
|
||
generatedAt: new Date().toISOString(),
|
||
entries: this.entries.map((e) => ({
|
||
name: e.name,
|
||
ms: e.ms,
|
||
at: e.at,
|
||
})),
|
||
totalMs: this.entries.reduce((s, e) => s + e.ms, 0),
|
||
};
|
||
writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
||
return filePath;
|
||
}
|
||
}
|