@decaf-ts/utils
Version:
module management utils for decaf-ts
426 lines • 17.5 kB
JavaScript
;
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