187 lines
7.9 KiB
JavaScript
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}`));
|
|
}
|
|
}
|