testing local-first datastores
1import { writeFile, mkdir } from 'fs/promises';
2import { join } from 'path';
3import type { StoreBenchmarkResults, BenchmarkResult } from './types.js';
4
5function formatDuration(ms: number): string {
6 if (ms < 1000) return `${ms.toFixed(0)}ms`;
7 if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
8 return `${(ms / 60000).toFixed(2)}m`;
9}
10
11function formatBytes(bytes: number): string {
12 if (bytes < 1024) return `${bytes} B`;
13 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
14 if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
15 return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
16}
17
18function formatThroughput(bytes: number, ms: number): string {
19 const bytesPerSec = (bytes / ms) * 1000;
20 return `${formatBytes(bytesPerSec)}/s`;
21}
22
23function padRight(str: string, len: number): string {
24 return str.length >= len ? str : str + ' '.repeat(len - str.length);
25}
26
27function padLeft(str: string, len: number): string {
28 return str.length >= len ? str : ' '.repeat(len - str.length) + str;
29}
30
31function printBenchmark(b: BenchmarkResult, indent = ''): void {
32 if (b.failed) {
33 console.log(`${indent}${padRight(b.name, 30)} ${padLeft('FAILED', 10)} ${b.error || 'Unknown error'}`);
34 return;
35 }
36 let line = `${indent}${padRight(b.name, 30)} ${padLeft(formatDuration(b.durationMs), 10)}`;
37 if (b.itemCount !== undefined) {
38 line += ` (${b.itemCount} items)`;
39 }
40 if (b.bytesProcessed !== undefined) {
41 line += ` ${formatBytes(b.bytesProcessed)}`;
42 line += ` @ ${formatThroughput(b.bytesProcessed, b.durationMs)}`;
43 }
44 console.log(line);
45}
46
47function formatResultOrFailed(b: BenchmarkResult, formatter: (b: BenchmarkResult) => string): string {
48 if (b.failed) return 'FAILED';
49 return formatter(b);
50}
51
52export function printResults(results: StoreBenchmarkResults[]): void {
53 console.log('\n' + '='.repeat(80));
54 console.log('BENCHMARK RESULTS');
55 console.log('='.repeat(80));
56
57 for (const r of results) {
58 console.log(`\n${'─'.repeat(80)}`);
59 console.log(`Store: ${r.storeName}`);
60 console.log('─'.repeat(80));
61
62 console.log('\nINIT:');
63 printBenchmark(r.init, ' ');
64
65 console.log('\nWRITES:');
66 printBenchmark(r.writes.allUrls, ' ');
67 printBenchmark(r.writes.allMetadata, ' ');
68 printBenchmark(r.writes.allImages, ' ');
69 printBenchmark(r.writes.allDocuments, ' ');
70
71 console.log('\nREADS:');
72 printBenchmark(r.reads.recentUrls, ' ');
73 printBenchmark(r.reads.randomImages, ' ');
74 printBenchmark(r.reads.randomDocuments, ' ');
75
76 console.log('\nDISK:');
77 console.log(` Total storage: ${formatBytes(r.disk.totalBytes)}`);
78 }
79
80 // Comparison table if multiple stores
81 if (results.length > 1) {
82 console.log('\n' + '='.repeat(80));
83 console.log('COMPARISON');
84 console.log('='.repeat(80));
85
86 const headers = ['Metric', ...results.map(r => r.storeName)];
87 const colWidth = 15;
88
89 console.log('\n' + headers.map(h => padRight(h, colWidth)).join(' │ '));
90 console.log('─'.repeat(colWidth * headers.length + (headers.length - 1) * 3));
91
92 const metrics = [
93 { name: 'Init', get: (r: StoreBenchmarkResults) => formatResultOrFailed(r.init, b => formatDuration(b.durationMs)) },
94 { name: 'Write URLs', get: (r: StoreBenchmarkResults) => formatResultOrFailed(r.writes.allUrls, b => formatDuration(b.durationMs)) },
95 { name: 'Write metadata', get: (r: StoreBenchmarkResults) => formatResultOrFailed(r.writes.allMetadata, b => formatDuration(b.durationMs)) },
96 { name: 'Write images', get: (r: StoreBenchmarkResults) => formatResultOrFailed(r.writes.allImages, b => formatDuration(b.durationMs)) },
97 { name: 'Write docs', get: (r: StoreBenchmarkResults) => formatResultOrFailed(r.writes.allDocuments, b => formatDuration(b.durationMs)) },
98 { name: 'Read URLs', get: (r: StoreBenchmarkResults) => formatResultOrFailed(r.reads.recentUrls, b => formatDuration(b.durationMs)) },
99 { name: 'Read images', get: (r: StoreBenchmarkResults) => formatResultOrFailed(r.reads.randomImages, b => formatDuration(b.durationMs)) },
100 { name: 'Read docs', get: (r: StoreBenchmarkResults) => formatResultOrFailed(r.reads.randomDocuments, b => formatDuration(b.durationMs)) },
101 { name: 'Disk usage', get: (r: StoreBenchmarkResults) => formatBytes(r.disk.totalBytes) },
102 ];
103
104 for (const metric of metrics) {
105 const row = [padRight(metric.name, colWidth), ...results.map(r => padRight(metric.get(r), colWidth))];
106 console.log(row.join(' │ '));
107 }
108 }
109
110 console.log('\n');
111}
112
113export async function saveResults(results: StoreBenchmarkResults[], baseDir = 'results'): Promise<string> {
114 const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
115 const runDir = join(baseDir, `benchmark-${timestamp}`);
116
117 await mkdir(runDir, { recursive: true });
118
119 await writeFile(
120 join(runDir, 'results.json'),
121 JSON.stringify({
122 timestamp: new Date().toISOString(),
123 results
124 }, null, 2)
125 );
126
127 console.log(`Results saved to ${runDir}/results.json`);
128 return runDir;
129}