UNPKG

@decaf-ts/utils

Version:

module management utils for decaf-ts

426 lines 17.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PerformanceRunner = exports.defaultCanvasOptions = void 0; const path_1 = __importDefault(require("path")); const promises_1 = require("fs/promises"); const logging_1 = require("@decaf-ts/logging"); exports.defaultCanvasOptions = { width: 1200, height: 260, padding: 32, backgroundColor: "#0f172a", headerFont: "bold 18px 'Segoe UI', sans-serif", rowFont: "14px 'Segoe UI', sans-serif", headerColor: "#f8fafc", rowColor: "#cbd5f5", }; const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const perfDebugEnabled = process.env.PERF_VERBOSE === "true"; const debugLog = (...args) => { if (perfDebugEnabled) { console.debug(...args); } }; const formatTable = (title, headers, rows) => { const columnWidths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0))); const formatRow = (values) => "| " + values .map((value, index) => value.padEnd(columnWidths[index])) .join(" | ") + " |"; const headerLine = formatRow(headers); const divider = "| " + columnWidths.map((width) => "-".repeat(width)).join(" | ") + " |"; const body = rows.map(formatRow).join("\n"); return `${title}\n${headerLine}\n${divider}\n${body}`; }; const ensureDirectory = async (targetPath) => { await (0, promises_1.mkdir)(path_1.default.dirname(targetPath), { recursive: true }); }; const renderMetricsTableToCanvas = async (headers, rows, options, outputPath) => { const config = { ...exports.defaultCanvasOptions, ...options }; // @ts-expect-error canvas import let createCanvas; try { // @ts-expect-error because we allow optional dependency const canvasModule = await Promise.resolve().then(() => __importStar(require("canvas"))); createCanvas = canvasModule.createCanvas; } catch (error) { console.warn("[PerfRunner] Canvas module not available, skipping chart.", error); return; } const canvas = createCanvas(config.width, config.height); const ctx = canvas.getContext("2d"); ctx.fillStyle = config.backgroundColor; ctx.fillRect(0, 0, config.width, config.height); ctx.font = config.headerFont; ctx.fillStyle = config.headerColor; const columnWidth = (config.width - config.padding * 2) / headers.length; const headerY = config.padding + 24; headers.forEach((header, index) => { ctx.fillText(header, config.padding + columnWidth * index, headerY); }); ctx.font = config.rowFont; ctx.fillStyle = config.rowColor; const rowHeight = 24; rows.forEach((row, rowIndex) => { const y = headerY + 12 + rowHeight * (rowIndex + 1); row.forEach((cell, cellIndex) => { ctx.fillText(cell, config.padding + columnWidth * cellIndex, y); }); }); await ensureDirectory(outputPath); await (0, promises_1.writeFile)(outputPath, canvas.toBuffer("image/png")); console.log(`Stored performance chart at ${outputPath}`); }; class PerformanceRunner { constructor(scenario) { this.scenario = scenario; } async run() { if (this.scenario.initialize) { await this.scenario.initialize(); } let phaseCounter = 0; const queue = this.scenario.phases.map((phase) => ({ phase, phaseNumber: ++phaseCounter })); const results = []; while (queue.length) { const item = queue.shift(); const phase = item.phase; const context = this.mergeContext(phase); console.info(`[PerfRunner] Starting phase #${item.phaseNumber} "${phase.name}"`, { iterations: phase.config.iterations, mode: phase.config.mode, concurrency: phase.config.concurrency, burst: phase.config.burst, }); const result = await this.runPhase(phase, context); console.info(`[PerfRunner] Completed phase #${item.phaseNumber} "${phase.name}"`, { total: result.aggregated.totalDurationMs.toFixed(2), avg: result.aggregated.averageMs.toFixed(2), failureCount: result.aggregated.failureCount, }); results.push(result); if (phase.generator) { const historySnapshot = [...results]; const metadata = { phaseNumber: item.phaseNumber, phaseName: phase.name, iterationCount: phase.config.iterations, burstSegments: result.segmentCount, segmentCount: result.segmentCount, mode: phase.config.mode, history: historySnapshot, }; const generatorResult = await phase.generator({ result, history: historySnapshot, metadata, }); if (generatorResult) { const nextPhase = ("config" in generatorResult ? { name: generatorResult.name ?? `${phase.name} → gen`, config: generatorResult.config, generator: phase.generator, } : { name: `${phase.name} → gen`, config: generatorResult, generator: phase.generator, }); queue.push({ phase: nextPhase, phaseNumber: ++phaseCounter, }); } } } await this.logSummary(results); if (this.scenario.failOnError !== false) { const totalFailures = results.reduce((acc, r) => acc + r.aggregated.failureCount, 0); if (totalFailures > 0) { throw new Error(`Scenario "${this.scenario.name}" recorded ${totalFailures} failures across ${results.length} phases`); } } return results; } async runPhase(phase, context) { await this.runWarmup(phase, context); const segments = this.buildSegmentIndices(phase.config.iterations, phase.config.burst); const collected = []; const phaseStart = Date.now(); for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex += 1) { const indices = segments[segmentIndex]; if (!indices.length) { continue; } debugLog(`[PerfRunner] Phase "${phase.name}" executing segment ${segmentIndex + 1}/${segments.length} (iterations ${indices[0]}-${indices[indices.length - 1]})`); const metrics = await this.executeSegment(this.scenario.handler, phase.config, context, indices); collected.push(...metrics); const burstInterval = phase.config.burst?.intervalMs; if (burstInterval && segmentIndex < segments.length - 1) { await delay(burstInterval); } } const wallClockMs = Date.now() - phaseStart; const sorted = [...collected].sort((a, b) => a.iteration - b.iteration); const aggregated = this.aggregateMetrics(sorted); if (phase.config.pauseAfterMs) { await delay(phase.config.pauseAfterMs); } return { phase, config: phase.config, iterationMetrics: sorted, aggregated, context, segmentCount: segments.length, wallClockMs, }; } async runWarmup(phase, context) { const warmup = phase.config.warmup; if (!warmup?.iterations) { return; } const handler = warmup.handler ?? this.scenario.handler; for (let index = 0; index < warmup.iterations; index += 1) { await handler({ iteration: index, config: phase.config, loadFactor: this.computeLoadFactor(phase.config, index), context, }); if (warmup.delayBetweenIterationsMs && index < warmup.iterations - 1) { await delay(warmup.delayBetweenIterationsMs); } } } computeLoadFactor(config, iteration) { const base = config.loadStart ?? 1; const step = config.loadStep ?? 0; const multiplier = config.loadMultiplier ?? 1; return (base + step * iteration) * multiplier; } buildSegmentIndices(iterations, burst) { const total = Math.max(0, iterations); if (total === 0) { return []; } const chunk = burst?.size && burst.size > 0 ? Math.min(burst.size, total) : total; const segments = []; for (let cursor = 0; cursor < total; cursor += chunk) { const end = Math.min(total, cursor + chunk); segments.push(Array.from({ length: end - cursor }, (_, idx) => cursor + idx)); } return segments; } mergeContext(phase) { const baseContext = this.scenario.baseContext ?? {}; const phaseContext = phase.config.context ?? {}; return this.mergeContexts(baseContext, phaseContext); } mergeContexts(a, b) { return Object.assign({}, a, b); } async executeSegment(handler, config, context, indices) { if (!indices.length) { return []; } if (config.mode === "concurrent") { return this.collectConcurrent(handler, config, context, indices); } return this.collectSequential(handler, config, context, indices); } async collectSequential(handler, config, context, indices) { const metrics = []; const delayBetween = config.delayBetweenIterationsMs; for (let idx = 0; idx < indices.length; idx += 1) { metrics.push(await this.runIteration(handler, config, context, indices[idx])); if (delayBetween && idx < indices.length - 1) { await delay(delayBetween); } } return metrics; } async collectConcurrent(handler, config, context, indices) { const concurrency = Math.max(1, Math.min(config.concurrency ?? indices.length, indices.length)); const metrics = []; let pointer = 0; const worker = async () => { while (pointer < indices.length) { const iteration = indices[pointer]; pointer += 1; metrics.push(await this.runIteration(handler, config, context, iteration)); } }; await Promise.all(Array.from({ length: concurrency }, () => worker())); return metrics; } async runIteration(handler, config, context, iteration) { const loadFactor = this.computeLoadFactor(config, iteration); const stopwatch = new logging_1.StopWatch(true); let success = true; let meta; debugLog(`[PerfRunner] Iteration ${iteration} (phase=${config.metadata?.phaseName ?? "n/a"}, load=${loadFactor.toFixed(2)}) starting`); try { const result = await handler({ iteration, config, loadFactor, context }); if (typeof result?.success === "boolean") { success = result.success; } meta = result?.meta; } catch (error) { success = false; meta = { error: error instanceof Error ? error.message : String(error) }; } const durationMs = stopwatch.stop(); debugLog(`[PerfRunner] Iteration ${iteration} complete in ${durationMs.toFixed(2)}ms, success=${success}`); return { iteration, durationMs, success, meta, loadFactor }; } aggregateMetrics(metrics) { if (metrics.length === 0) { return { totalDurationMs: 0, minMs: 0, maxMs: 0, averageMs: 0, successCount: 0, failureCount: 0, loadStart: 0, loadEnd: 0, }; } const totalDurationMs = metrics.reduce((acc, metric) => acc + metric.durationMs, 0); const minMs = Math.min(...metrics.map((metric) => metric.durationMs)); const maxMs = Math.max(...metrics.map((metric) => metric.durationMs)); const averageMs = totalDurationMs / metrics.length; const successCount = metrics.filter((metric) => metric.success).length; const failureCount = metrics.length - successCount; const loadStart = metrics[0]?.loadFactor ?? 0; const loadEnd = metrics[metrics.length - 1]?.loadFactor ?? loadStart; return { totalDurationMs, minMs, maxMs, averageMs, successCount, failureCount, loadStart, loadEnd, }; } logPhaseTable(result) { const headers = [ "Phase", "Mode", "Iterations", "Wall ms", "RPS", "Avg ms", "Min ms", "Max ms", "Success", "Failures", "Load range", ]; const rps = (result.config.iterations / (result.wallClockMs / 1000)).toFixed(1); const row = [ result.phase.name, result.config.mode, result.config.iterations.toString(), result.wallClockMs.toFixed(0), rps, result.aggregated.averageMs.toFixed(2), result.aggregated.minMs.toFixed(2), result.aggregated.maxMs.toFixed(2), result.aggregated.successCount.toString(), result.aggregated.failureCount.toString(), `${result.aggregated.loadStart.toFixed(2)}${result.aggregated.loadEnd.toFixed(2)}`, ]; console.log(formatTable(`Phase result: ${result.phase.name}`, headers, [row])); } async logSummary(results) { if (!results.length) { return; } const headers = [ "Phase", "Mode", "Iterations", "Wall ms", "RPS", "Avg ms", "Min ms", "Max ms", "Success", "Failures", "Load range", ]; const rows = results.map((result) => { const rps = (result.config.iterations / (result.wallClockMs / 1000)).toFixed(1); return [ result.phase.name, result.config.mode, result.config.iterations.toString(), result.wallClockMs.toFixed(0), rps, result.aggregated.averageMs.toFixed(2), result.aggregated.minMs.toFixed(2), result.aggregated.maxMs.toFixed(2), result.aggregated.successCount.toString(), result.aggregated.failureCount.toString(), `${result.aggregated.loadStart.toFixed(2)}${result.aggregated.loadEnd.toFixed(2)}`, ]; }); console.log(formatTable(`Performance summary for ${this.scenario.name}`, headers, rows)); if (this.shouldRenderCanvas()) { const chartPath = this.scenario.canvasOutputPath ?? path_1.default.join(process.cwd(), "workdocs", "reports", "performance-runner.png"); await renderMetricsTableToCanvas(headers, rows, this.scenario.canvasOptions ?? exports.defaultCanvasOptions, chartPath); } } shouldRenderCanvas() { return this.scenario.enableCanvas ?? Boolean(this.scenario.canvasOptions); } } exports.PerformanceRunner = PerformanceRunner; //# sourceMappingURL=performanceRunner.js.map //# sourceMappingURL=performanceRunner.cjs.map