170 lines
5.0 KiB
TypeScript
170 lines
5.0 KiB
TypeScript
/**
|
|
* KIS-TOiR Performance Monitor
|
|
*
|
|
* Lightweight instrumentation for the orchestrator. Tracks wall-clock time
|
|
* between named marks, accumulates metrics, prints human-readable summaries,
|
|
* and exports to JSON/CSV for trend analysis.
|
|
*
|
|
* Usage:
|
|
* import { monitor } from './performance-monitor';
|
|
* monitor.mark('start');
|
|
* // ... work ...
|
|
* monitor.measure('prisma');
|
|
* monitor.summary('Full Generation');
|
|
* fs.writeFileSync('generation-metrics.json', monitor.export('json'));
|
|
*/
|
|
|
|
import * as fs from "fs";
|
|
import * as path from "path";
|
|
|
|
export interface MetricEntry {
|
|
label: string;
|
|
duration: number; // milliseconds
|
|
timestamp: Date;
|
|
}
|
|
|
|
export type ExportFormat = "json" | "csv";
|
|
|
|
export class PerformanceMonitor {
|
|
private marks: Map<string, number> = new Map();
|
|
private metrics: MetricEntry[] = [];
|
|
private readonly startTime: number;
|
|
|
|
constructor() {
|
|
this.startTime = Date.now();
|
|
this.marks.set("start", this.startTime);
|
|
}
|
|
|
|
/**
|
|
* Record a named mark at the current time. A mark is an instant, not a
|
|
* duration. Use mark() to set a baseline, then measure() to record elapsed
|
|
* time from that baseline.
|
|
*/
|
|
mark(label: string): void {
|
|
this.marks.set(label, Date.now());
|
|
}
|
|
|
|
/**
|
|
* Record the elapsed time from the last `start` (or prior matching mark)
|
|
* until now. Returns the duration in milliseconds and stores it in the
|
|
* metrics collection. Pass `sinceMark` to measure from a specific mark
|
|
* rather than the most recent one.
|
|
*/
|
|
measure(label: string, sinceMark = "start"): number {
|
|
const origin = this.marks.get(sinceMark) ?? this.startTime;
|
|
const duration = Date.now() - origin;
|
|
this.metrics.push({
|
|
label,
|
|
duration,
|
|
timestamp: new Date(),
|
|
});
|
|
// eslint-disable-next-line no-console
|
|
console.log(`⏱️ ${label}: ${duration.toFixed(2)}ms`);
|
|
return duration;
|
|
}
|
|
|
|
/**
|
|
* Manually add a metric entry. Useful for tasks where you already have the
|
|
* duration (e.g., measured inside Promise.all bookkeeping).
|
|
*/
|
|
record(label: string, duration: number): void {
|
|
this.metrics.push({
|
|
label,
|
|
duration,
|
|
timestamp: new Date(),
|
|
});
|
|
// eslint-disable-next-line no-console
|
|
console.log(`⏱️ ${label}: ${duration.toFixed(2)}ms`);
|
|
}
|
|
|
|
/**
|
|
* Print a summary block. If `label` is provided, compares the max metric
|
|
* (assumed parallel total) against the sum of metrics (sequential baseline)
|
|
* and reports the speedup.
|
|
*/
|
|
summary(label = "Summary"): void {
|
|
const total = Date.now() - this.startTime;
|
|
const seqBaseline = this.metrics.reduce((acc, m) => acc + m.duration, 0);
|
|
const parallelActual = this.metrics.reduce(
|
|
(acc, m) => Math.max(acc, m.duration),
|
|
0
|
|
);
|
|
const improvement =
|
|
seqBaseline > 0
|
|
? ((seqBaseline - parallelActual) / seqBaseline) * 100
|
|
: 0;
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log(`\n📊 ${label}`);
|
|
// eslint-disable-next-line no-console
|
|
console.log(` Total wall-clock: ${total.toFixed(2)}ms`);
|
|
// eslint-disable-next-line no-console
|
|
console.log(` Sum of phases: ${seqBaseline.toFixed(2)}ms (sequential baseline)`);
|
|
if (this.metrics.length > 1) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
` Longest phase: ${parallelActual.toFixed(2)}ms (parallel lower bound)`
|
|
);
|
|
// eslint-disable-next-line no-console
|
|
console.log(` Speedup achieved: ${improvement.toFixed(1)}%`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Serialize metrics for disk storage / trend analysis.
|
|
*/
|
|
export(format: ExportFormat = "json"): string {
|
|
if (format === "csv") {
|
|
const header = "label,duration_ms,timestamp";
|
|
const rows = this.metrics.map(
|
|
(m) => `${m.label},${m.duration},${m.timestamp.toISOString()}`
|
|
);
|
|
return [header, ...rows].join("\n");
|
|
}
|
|
return JSON.stringify(
|
|
{
|
|
startedAt: new Date(this.startTime).toISOString(),
|
|
totalMs: Date.now() - this.startTime,
|
|
metrics: this.metrics,
|
|
},
|
|
null,
|
|
2
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Write metrics to a file alongside other generation outputs. Defaults to
|
|
* project-root `generation-metrics.json`.
|
|
*/
|
|
writeReport(filePath = "generation-metrics.json"): void {
|
|
const resolved = path.isAbsolute(filePath)
|
|
? filePath
|
|
: path.join(process.cwd(), filePath);
|
|
fs.writeFileSync(resolved, this.export("json"));
|
|
}
|
|
|
|
/**
|
|
* Return a shallow copy of the recorded metrics. Useful for tests and for
|
|
* attaching to GenerationOutput results.
|
|
*/
|
|
getMetrics(): MetricEntry[] {
|
|
return [...this.metrics];
|
|
}
|
|
|
|
/**
|
|
* Reset all marks and metrics. The monitor remains usable afterward.
|
|
*/
|
|
reset(): void {
|
|
this.marks.clear();
|
|
this.metrics.length = 0;
|
|
this.marks.set("start", Date.now());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shared singleton instance for convenience. Multiple independent runs can
|
|
* call `monitor.reset()` between them.
|
|
*/
|
|
export const monitor = new PerformanceMonitor();
|
|
export default monitor;
|