rebase generation
This commit is contained in:
169
tools/performance-monitor.ts
Normal file
169
tools/performance-monitor.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user