109 lines
2.5 KiB
TypeScript
109 lines
2.5 KiB
TypeScript
import * as fsp from 'fs/promises';
|
|
import * as path from 'path';
|
|
import type { PerfDiagEntry } from './diagnostics.models';
|
|
import {
|
|
formatPerfDiagLine,
|
|
pushRingBuffer,
|
|
resolveDiagnosticsFilePath
|
|
} from './diagnostics.rules';
|
|
|
|
const DEFAULT_RING_CAPACITY = 120;
|
|
const FLUSH_DEBOUNCE_MS = 250;
|
|
|
|
export interface PerfDiagWriterOptions {
|
|
userDataPath: string;
|
|
sessionId: string;
|
|
ringCapacity?: number;
|
|
}
|
|
|
|
export class PerfDiagWriter {
|
|
private readonly filePath: string;
|
|
private readonly ringCapacity: number;
|
|
private readonly pendingLines: string[] = [];
|
|
private ring: PerfDiagEntry[] = [];
|
|
private flushTimer: NodeJS.Timeout | null = null;
|
|
private flushInFlight: Promise<void> | null = null;
|
|
private disabled = false;
|
|
|
|
constructor(options: PerfDiagWriterOptions) {
|
|
this.filePath = resolveDiagnosticsFilePath(options.userDataPath, options.sessionId);
|
|
this.ringCapacity = options.ringCapacity ?? DEFAULT_RING_CAPACITY;
|
|
}
|
|
|
|
get snapshotFilePath(): string {
|
|
return this.filePath;
|
|
}
|
|
|
|
get bufferedEntries(): readonly PerfDiagEntry[] {
|
|
return this.ring;
|
|
}
|
|
|
|
append(entry: PerfDiagEntry): void {
|
|
if (this.disabled) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.ring = pushRingBuffer(this.ring, entry, this.ringCapacity);
|
|
this.pendingLines.push(`${formatPerfDiagLine(entry)}\n`);
|
|
this.scheduleFlush();
|
|
} catch {
|
|
this.disabled = true;
|
|
}
|
|
}
|
|
|
|
async flush(): Promise<void> {
|
|
if (this.disabled || this.pendingLines.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if (this.flushInFlight) {
|
|
await this.flushInFlight;
|
|
return;
|
|
}
|
|
|
|
const lines = this.pendingLines.splice(0, this.pendingLines.length);
|
|
|
|
this.flushInFlight = this.writeLines(lines)
|
|
.catch(() => {
|
|
this.disabled = true;
|
|
})
|
|
.finally(() => {
|
|
this.flushInFlight = null;
|
|
});
|
|
|
|
await this.flushInFlight;
|
|
}
|
|
|
|
async flushSnapshot(label: string): Promise<void> {
|
|
this.append({
|
|
collectedAt: Date.now(),
|
|
source: 'main',
|
|
type: 'session',
|
|
payload: {
|
|
event: label,
|
|
filePath: this.filePath,
|
|
entries: this.ring
|
|
}
|
|
});
|
|
|
|
await this.flush();
|
|
}
|
|
|
|
private scheduleFlush(): void {
|
|
if (this.flushTimer) {
|
|
return;
|
|
}
|
|
|
|
this.flushTimer = setTimeout(() => {
|
|
this.flushTimer = null;
|
|
void this.flush();
|
|
}, FLUSH_DEBOUNCE_MS);
|
|
}
|
|
|
|
private async writeLines(lines: string[]): Promise<void> {
|
|
await fsp.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
await fsp.appendFile(this.filePath, lines.join(''), 'utf8');
|
|
}
|
|
}
|