/** * 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 = 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;