Files
2026-09-22 08:31:01 +02:00

187 lines
7.9 KiB
JavaScript

import { writeFileSync } from 'node:fs';
import chalk from 'chalk';
function fmtMs(ms) {
if (ms == null || Number.isNaN(ms)) return '-';
if (ms < 1) return `${ms.toFixed(2)} ms`;
if (ms < 10) return `${ms.toFixed(2)} ms`;
return `${ms.toFixed(1)} ms`;
}
function padRight(str, len) {
const s = String(str);
return s + ' '.repeat(Math.max(0, len - s.length));
}
function padLeft(str, len) {
const s = String(str);
return ' '.repeat(Math.max(0, len - s.length)) + s;
}
export class BenchmarkReporter {
/**
* Prints the single-driver timing summary to stdout.
*/
static printDriverReport(driverName, stats, totalElapsedMs) {
console.log();
console.log(chalk.bold.cyan(`========================================================================================`));
console.log(chalk.bold.cyan(` DATABASE DRIVER TIMING REPORT: ${driverName.toUpperCase()}`));
console.log(chalk.bold.cyan(`========================================================================================`));
const totalOps = stats.reduce((acc, s) => acc + s.count, 0);
const totalErrors = stats.reduce((acc, s) => acc + s.errors, 0);
const header = `${padRight('API Method', 38)} | ${padLeft('Calls', 6)} | ${padLeft('Avg (ms)', 10)} | ${padLeft('Min (ms)', 10)} | ${padLeft('Max (ms)', 10)} | ${padLeft('P50 (ms)', 10)} | ${padLeft('P95 (ms)', 10)}`;
console.log(chalk.gray('-'.repeat(header.length)));
console.log(chalk.bold(header));
console.log(chalk.gray('-'.repeat(header.length)));
for (const item of stats) {
const avgStr = fmtMs(item.avgMs);
const minStr = fmtMs(item.minMs);
const maxStr = fmtMs(item.maxMs);
const p50Str = fmtMs(item.p50Ms);
const p95Str = fmtMs(item.p95Ms);
let line = `${padRight(item.name, 38)} | ${padLeft(item.count, 6)} | ${padLeft(avgStr, 10)} | ${padLeft(minStr, 10)} | ${padLeft(maxStr, 10)} | ${padLeft(p50Str, 10)} | ${padLeft(p95Str, 10)}`;
if (item.errors > 0) {
line = chalk.red(line + ` [${item.errors} ERR]`);
}
console.log(line);
}
console.log(chalk.gray('-'.repeat(header.length)));
console.log(
chalk.green(
`Total Operations: ${totalOps} | Total Time: ${(totalElapsedMs / 1000).toFixed(2)}s | APIs Covered: ${stats.length} | Errors: ${totalErrors}`,
),
);
console.log();
}
/**
* Prints the side-by-side comparison between MSSQL and SQLite.
*/
static printComparisonReport(mssqlStats, sqliteStats, mssqlElapsedMs, sqliteElapsedMs) {
console.log();
console.log(chalk.bold.magenta(`========================================================================================================`));
console.log(chalk.bold.magenta(` COMPARATIVE BENCHMARK: MSSQL vs BETTER-SQLITE3 (Timings in ms)`));
console.log(chalk.bold.magenta(`========================================================================================================`));
const mssqlMap = new Map(mssqlStats.map((s) => [s.name, s]));
const sqliteMap = new Map(sqliteStats.map((s) => [s.name, s]));
const allNames = Array.from(new Set([...mssqlMap.keys(), ...sqliteMap.keys()])).sort();
const header = `${padRight('API Method', 36)} | ${padLeft('Calls', 6)} | ${padLeft('MSSQL Avg', 11)} | ${padLeft('MSSQL P95', 11)} | ${padLeft('SQLite Avg', 11)} | ${padLeft('SQLite P95', 11)} | ${padLeft('Ratio (M/S)', 12)}`;
console.log(chalk.gray('-'.repeat(header.length)));
console.log(chalk.bold(header));
console.log(chalk.gray('-'.repeat(header.length)));
for (const name of allNames) {
const m = mssqlMap.get(name);
const s = sqliteMap.get(name);
const calls = m?.count || s?.count || 0;
const mAvg = m ? fmtMs(m.avgMs) : '-';
const mP95 = m ? fmtMs(m.p95Ms) : '-';
const sAvg = s ? fmtMs(s.avgMs) : '-';
const sP95 = s ? fmtMs(s.p95Ms) : '-';
let ratioStr = '-';
if (m && s && s.avgMs > 0) {
const ratio = m.avgMs / s.avgMs;
ratioStr = `${ratio.toFixed(1)}x`;
if (ratio > 1.5) {
ratioStr = chalk.yellow(ratioStr + ' (SQLite faster)');
} else if (ratio < 0.8) {
ratioStr = chalk.cyan(ratioStr + ' (MSSQL faster)');
}
}
console.log(
`${padRight(name, 36)} | ${padLeft(calls, 6)} | ${padLeft(mAvg, 11)} | ${padLeft(mP95, 11)} | ${padLeft(sAvg, 11)} | ${padLeft(sP95, 11)} | ${padLeft(ratioStr, 12)}`,
);
}
console.log(chalk.gray('-'.repeat(header.length)));
console.log(
chalk.bold(
`Total Runtime: MSSQL = ${(mssqlElapsedMs / 1000).toFixed(2)}s vs SQLite = ${(sqliteElapsedMs / 1000).toFixed(2)}s`,
),
);
console.log();
}
/**
* Generates a GitHub-flavored Markdown report.
*/
static generateMarkdownReport({ mssqlStats, sqliteStats, mssqlElapsedMs, sqliteElapsedMs, options = {} }) {
const lines = [];
lines.push('# Database Drivers API Timing & Benchmark Report');
lines.push('');
lines.push(`- **Date**: ${new Date().toISOString()}`);
lines.push(`- **Scale**: ${options.scale || 'small'}`);
lines.push(`- **Read Iterations on Filled DB**: ${options.iterations || 5}`);
lines.push('');
if (mssqlStats && sqliteStats) {
lines.push('## Comparative Summary');
lines.push('');
lines.push(`| Metric | MSSQL | better-sqlite3 | Delta |`);
lines.push(`| :--- | :---: | :---: | :---: |`);
const mssqlTotalOps = mssqlStats.reduce((acc, s) => acc + s.count, 0);
const sqliteTotalOps = sqliteStats.reduce((acc, s) => acc + s.count, 0);
lines.push(`| Total API Calls | ${mssqlTotalOps} | ${sqliteTotalOps} | - |`);
lines.push(`| Total Runtime | ${(mssqlElapsedMs / 1000).toFixed(2)}s | ${(sqliteElapsedMs / 1000).toFixed(2)}s | ${(mssqlElapsedMs / sqliteElapsedMs).toFixed(1)}x |`);
lines.push('');
lines.push('## API Timings Comparison (in milliseconds)');
lines.push('');
lines.push('| API Method | Calls | MSSQL Avg (ms) | MSSQL P95 (ms) | SQLite Avg (ms) | SQLite P95 (ms) | Ratio (MSSQL / SQLite) |');
lines.push('| :--- | :---: | :---: | :---: | :---: | :---: | :---: |');
const mssqlMap = new Map(mssqlStats.map((s) => [s.name, s]));
const sqliteMap = new Map(sqliteStats.map((s) => [s.name, s]));
const allNames = Array.from(new Set([...mssqlMap.keys(), ...sqliteMap.keys()])).sort();
for (const name of allNames) {
const m = mssqlMap.get(name);
const s = sqliteMap.get(name);
const calls = m?.count || s?.count || 0;
const mAvg = m ? m.avgMs.toFixed(2) : '-';
const mP95 = m ? m.p95Ms.toFixed(2) : '-';
const sAvg = s ? s.avgMs.toFixed(2) : '-';
const sP95 = s ? s.p95Ms.toFixed(2) : '-';
const ratio = m && s && s.avgMs > 0 ? (m.avgMs / s.avgMs).toFixed(2) + 'x' : '-';
lines.push(`| \`${name}\` | ${calls} | ${mAvg} | ${mP95} | ${sAvg} | ${sP95} | ${ratio} |`);
}
lines.push('');
}
const drivers = [];
if (mssqlStats) drivers.push({ name: 'MSSQL (MssqlProvider)', stats: mssqlStats, elapsed: mssqlElapsedMs });
if (sqliteStats) drivers.push({ name: 'SQLite (BetterSqlite3Provider)', stats: sqliteStats, elapsed: sqliteElapsedMs });
for (const driver of drivers) {
lines.push(`## ${driver.name} Detailed Breakdown`);
lines.push('');
lines.push('| API Method | Calls | Avg (ms) | Min (ms) | Max (ms) | P50 (ms) | P95 (ms) | Errors |');
lines.push('| :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |');
for (const item of driver.stats) {
lines.push(
`| \`${item.name}\` | ${item.count} | ${item.avgMs.toFixed(2)} | ${item.minMs.toFixed(2)} | ${item.maxMs.toFixed(2)} | ${item.p50Ms.toFixed(2)} | ${item.p95Ms.toFixed(2)} | ${item.errors} |`,
);
}
lines.push('');
}
return lines.join('\n');
}
static saveReportToFile(filePath, markdownContent) {
writeFileSync(filePath, markdownContent, 'utf-8');
console.log(chalk.green(`✓ Benchmark report written to: ${filePath}`));
}
}