UNPKG

kvbits

Version:

A persistent key-value database for Node.js with Write-Ahead Logging, TTL, and compaction.

248 lines (208 loc) 7.29 kB
const fs = require('fs'); const path = require('path'); const { KV, AsyncKV } = require('../src/main.js'); const NUM_OPERATIONS_SMALL = 10000; const NUM_OPERATIONS_LARGE = 50000; const DATA_DIR = './benchmark_data'; const LOG_FILE = './benchmark_results.log'; // --- Helper Functions --- function cleanupDbFiles(baseName) { const dbPath = path.join(DATA_DIR, `${baseName}.db`); const indexPath = path.join(DATA_DIR, `${baseName}.index`); if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath); if (fs.existsSync(indexPath)) fs.unlinkSync(indexPath); } function ensureDataDir() { if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } } function logResult(message) { console.log(message); fs.appendFileSync(LOG_FILE, `${new Date().toISOString()} | ${message}\n`); } function formatMs(ms) { if (ms < 1000) return `${ms.toFixed(3)}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(3)}s`; return `${(ms / 60000).toFixed(2)}m`; } // --- Benchmark Runner --- async function runSingleTest(label, numOps, actionFn, preFn, postFn) { if (preFn) await preFn(); const startTime = process.hrtime.bigint(); await actionFn(); const endTime = process.hrtime.bigint(); const durationMs = Number(endTime - startTime) / 1e6; const opsPerSec = (numOps / (durationMs / 1000)).toFixed(2); logResult( `${label.padEnd(45)} | ${numOps.toLocaleString().padStart(10)} ops | ${formatMs(durationMs).padStart(10)} | ${opsPerSec.padStart(12)} ops/sec` ); if (postFn) await postFn(); return durationMs; } // --- Main Benchmark Suite --- async function runBenchmarkSuite() { ensureDataDir(); fs.writeFileSync(LOG_FILE, `Benchmark Run: ${new Date().toISOString()}\n`); logResult('='.repeat(100)); logResult( `${'Test Name'.padEnd(45)} | ${'Operations'.padStart(10)} ops | ${'Duration'.padStart(10)} | ${'Throughput'.padStart(12)} ops/sec` ); logResult('-'.repeat(100)); const smallPayload = { message: 'This is a small test payload.', ts: Date.now() }; // const largePayload = { data: 'X'.repeat(1024 * 4) }; // 4KB payload for some tests logResult('\n=== Synchronous KV (syncOnWrite: false, periodicFlush: 60s) ==='); let kvSyncDefault; const kvSyncDefaultOptions = { dbPath: path.join(DATA_DIR, 'sync_default_kv.db'), indexPath: path.join(DATA_DIR, 'sync_default_kv.index'), syncOnWrite: false, flushInterval: 60000, // High to avoid auto-flush during test preload: false // Start with cold cache for read tests after writes }; await runSingleTest( 'KV Set (syncOnWrite: false)', NUM_OPERATIONS_SMALL, async () => { for (let i = 0; i < NUM_OPERATIONS_SMALL; i++) { kvSyncDefault.set(`key:${i}`, { ...smallPayload, id: i }); } await kvSyncDefault.flush(false); // Manual flush, no forced fsync }, () => { cleanupDbFiles('sync_default_kv'); kvSyncDefault = new KV(kvSyncDefaultOptions); }, null // No specific post for this one, reads will use this DB state ); await runSingleTest( 'KV Get (syncOnWrite: false, warm cache)', NUM_OPERATIONS_SMALL, async () => { for (let i = 0; i < NUM_OPERATIONS_SMALL; i++) { kvSyncDefault.get(`key:${i}`); } } ); kvSyncDefault.close(); kvSyncDefault = new KV({...kvSyncDefaultOptions, preload: false}); // Re-open with preload false await runSingleTest( 'KV Get (syncOnWrite: false, cold cache)', NUM_OPERATIONS_SMALL, async () => { for (let i = 0; i < NUM_OPERATIONS_SMALL; i++) { kvSyncDefault.get(`key:${i}`); } }, null, null ); await runSingleTest( 'KV Delete (syncOnWrite: false)', NUM_OPERATIONS_SMALL, async () => { for (let i = 0; i < NUM_OPERATIONS_SMALL; i++) { kvSyncDefault.delete(`key:${i}`); } await kvSyncDefault.flush(false); // Manual flush }, null, () => { kvSyncDefault.close(); cleanupDbFiles('sync_default_kv'); } ); logResult('\n=== Synchronous KV (syncOnWrite: true, periodicFlush: 60s) ==='); let kvSyncTrue; const kvSyncTrueOptions = { dbPath: path.join(DATA_DIR, 'sync_true_kv.db'), indexPath: path.join(DATA_DIR, 'sync_true_kv.index'), syncOnWrite: true, flushInterval: 60000, preload: false }; const numOpsSyncTrue = Math.min(NUM_OPERATIONS_SMALL / 10, 1000); // sync:true is slow, reduce ops await runSingleTest( `KV Set (syncOnWrite: true)`, numOpsSyncTrue, async () => { for (let i = 0; i < numOpsSyncTrue; i++) { kvSyncTrue.set(`key:${i}`, { ...smallPayload, id: i }); } }, () => { cleanupDbFiles('sync_true_kv'); kvSyncTrue = new KV(kvSyncTrueOptions); } ); await runSingleTest( `KV Get (syncOnWrite: true, warm cache)`, numOpsSyncTrue, async () => { for (let i = 0; i < numOpsSyncTrue; i++) { kvSyncTrue.get(`key:${i}`); } } ); kvSyncTrue.close(); kvSyncTrue = new KV({...kvSyncTrueOptions, preload: false}); await runSingleTest( `KV Get (syncOnWrite: true, cold cache)`, numOpsSyncTrue, async () => { for (let i = 0; i < numOpsSyncTrue; i++) { kvSyncTrue.get(`key:${i}`); } } ); await runSingleTest( `KV Delete (syncOnWrite: true)`, numOpsSyncTrue, async () => { for (let i = 0; i < numOpsSyncTrue; i++) { kvSyncTrue.delete(`key:${i}`); } }, null, () => { kvSyncTrue.close(); cleanupDbFiles('sync_true_kv'); } ); // === Asynchronous AsyncKV Benchmarks === logResult('\n=== Asynchronous AsyncKV (defaults, periodicFlush: 60s) ==='); let asyncKv; const asyncKvOptions = { dbPath: path.join(DATA_DIR, 'async_kv.db'), indexPath: path.join(DATA_DIR, 'async_kv.index'), flushInterval: 60000, // Underlying KV syncOnWrite is false by default preload: false }; const numOpsAsync = NUM_OPERATIONS_SMALL / 2; // Async has overhead, adjust if needed await runSingleTest( 'AsyncKV Set', numOpsAsync, async () => { for (let i = 0; i < numOpsAsync; i++) { await asyncKv.set(`key:${i}`, { ...smallPayload, id: i }); } await asyncKv.flush(); }, () => { cleanupDbFiles('async_kv'); asyncKv = new AsyncKV(asyncKvOptions); } ); await runSingleTest( 'AsyncKV Get (warm cache)', numOpsAsync, async () => { for (let i = 0; i < numOpsAsync; i++) { await asyncKv.get(`key:${i}`); } } ); asyncKv.close(); asyncKv = new AsyncKV({...asyncKvOptions, preload: false}); await runSingleTest( 'AsyncKV Get (cold cache)', numOpsAsync, async () => { for (let i = 0; i < numOpsAsync; i++) { await asyncKv.get(`key:${i}`); } } ); await runSingleTest( 'AsyncKV Delete', numOpsAsync, async () => { for (let i = 0; i < numOpsAsync; i++) { await asyncKv.delete(`key:${i}`); } await asyncKv.flush(); // Manual flush }, null, () => { asyncKv.close(); cleanupDbFiles('async_kv'); } ); logResult('\n' + '='.repeat(100)); logResult('Benchmark suite complete.'); logResult(`Results also logged to: ${LOG_FILE}`); } runBenchmarkSuite().catch(err => { console.error("Benchmark suite failed:", err); logResult(`Benchmark suite failed: ${err.stack || err}`); });