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}`)); } }