axiodb 22.27.0 → 22.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "axiodb",
3
- "version": "22.27.0",
3
+ "version": "22.28.0",
4
4
  "description": "The embedded database for Node.js. Replaces SQLite, LowDB, NeDB & raw JSON files with MongoDB-style queries, ACID transactions, zero native dependencies, and built-in InMemoryCache. No compilation, no platform issues. Perfect for desktop apps, CLI tools, and embedded systems.",
5
5
  "main": "./lib/config/DB.js",
6
6
  "types": "./lib/config/DB.d.ts",
package/benchmark/run.js DELETED
@@ -1,661 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const os = require('os');
6
- const { execSync } = require('child_process');
7
- const DEFAULT_BENCHMARK_DIR = path.resolve(__dirname, './Benchmark_TempData');
8
- let activeBenchmarkDir = DEFAULT_BENCHMARK_DIR;
9
-
10
- // Auto-build if lib is missing
11
- const libEntry = path.resolve(__dirname, '../lib/config/DB.js');
12
- if (!fs.existsSync(libEntry)) {
13
- console.log('\x1b[33m[Benchmark] Compiled lib/ not found. Running build first...\x1b[0m');
14
- execSync('npm run build', {
15
- stdio: 'inherit',
16
- cwd: path.resolve(__dirname, '..')
17
- });
18
- }
19
-
20
- const { AxioDB } = require('../lib/config/DB.js');
21
-
22
- // ANSI Color formatting
23
- const c = {
24
- reset: '\x1b[0m',
25
- bright: '\x1b[1m',
26
- dim: '\x1b[2m',
27
- green: '\x1b[32m',
28
- yellow: '\x1b[33m',
29
- blue: '\x1b[34m',
30
- magenta: '\x1b[35m',
31
- cyan: '\x1b[36m',
32
- red: '\x1b[31m',
33
- gray: '\x1b[90m'
34
- };
35
-
36
- // Parse command line arguments
37
- function parseArgs() {
38
- const args = process.argv.slice(2);
39
- const options = {
40
- docs: 15000, // Default set to 15k
41
- batchSize: 3000, // Default batch size set to 3k
42
- queries: 500, // Indexed read lookups
43
- unindexedSamples: 25, // Unindexed full-scan lookups (scan over dataset)
44
- updateQueries: 100, // Indexed updates
45
- deleteQueries: 50, // Indexed deletes
46
- keepData: false, // Auto-delete temp data by default
47
- dir: DEFAULT_BENCHMARK_DIR
48
- };
49
-
50
- for (const arg of args) {
51
- if (arg.startsWith('--docs=')) {
52
- const val = parseInt(arg.split('=')[1], 10);
53
- if (!isNaN(val) && val > 0) options.docs = val;
54
- } else if (arg.startsWith('--batch=')) {
55
- const val = parseInt(arg.split('=')[1], 10);
56
- if (!isNaN(val) && val > 0) options.batchSize = val;
57
- } else if (arg.startsWith('--queries=')) {
58
- const val = parseInt(arg.split('=')[1], 10);
59
- if (!isNaN(val) && val > 0) options.queries = val;
60
- } else if (arg.startsWith('--unindexed=')) {
61
- const val = parseInt(arg.split('=')[1], 10);
62
- if (!isNaN(val) && val > 0) options.unindexedSamples = val;
63
- } else if (arg.startsWith('--dir=')) {
64
- const customDir = arg.split('=')[1];
65
- if (customDir && customDir.trim()) {
66
- options.dir = path.resolve(customDir.trim());
67
- }
68
- } else if (arg === '--keep') {
69
- options.keepData = true;
70
- } else if (arg === '--help' || arg === '-h') {
71
- console.log(`
72
- AxioDB Performance & Scale Benchmark Suite
73
-
74
- Usage:
75
- node benchmark/run.js [options]
76
- npm run benchmark -- [options]
77
-
78
- Options:
79
- --docs=N Total documents to insert (default: 15000, e.g. 10000, 50000, 100000)
80
- --batch=N Batch chunk size for insertMany (default: 3000)
81
- --queries=N Number of random indexed lookups to test (default: 500)
82
- --unindexed=N Number of unindexed full-scan operations to sample (default: 25)
83
- --dir=PATH Custom benchmark temp directory (default: ./benchmark/Benchmark_TempData)
84
- --keep Keep benchmark database files after run (default: auto-clean)
85
- --help, -h Show this help message
86
- `);
87
- process.exit(0);
88
- }
89
- }
90
-
91
- // Adjust batch size if docs < batchSize
92
- if (options.docs < options.batchSize) {
93
- options.batchSize = options.docs;
94
- }
95
-
96
- return options;
97
- }
98
-
99
- // Convert BigInt nanoseconds to milliseconds float
100
- function nsToMs(ns) {
101
- return Number(ns) / 1e6;
102
- }
103
-
104
- // Format bytes into readable MB
105
- function bytesToMB(bytes) {
106
- return (bytes / (1024 * 1024)).toFixed(2);
107
- }
108
-
109
- // Format bytes into readable string (B, KB, MB)
110
- function bytesToReadable(bytes) {
111
- if (bytes < 1024) return `${bytes} B`;
112
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
113
- return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
114
- }
115
-
116
- function safeRate(count, durationMs) {
117
- if (count <= 0 || durationMs <= 0) return 0;
118
- return Math.round((count / durationMs) * 1000);
119
- }
120
-
121
- function clampWorkload(requested, available, minimum = 0) {
122
- if (available <= 0) return 0;
123
- return Math.max(minimum, Math.min(requested, available));
124
- }
125
-
126
- // Calculate directory sizes recursively (Total vs Data-only vs Indexes)
127
- function calculateDirSizes(dirPath) {
128
- let totalBytes = 0;
129
- let withoutIndexBytes = 0;
130
- let indexBytes = 0;
131
- let docCount = 0;
132
-
133
- function traverse(currentPath) {
134
- if (!fs.existsSync(currentPath)) return;
135
- const entries = fs.readdirSync(currentPath, { withFileTypes: true });
136
- for (const entry of entries) {
137
- const fullPath = path.join(currentPath, entry.name);
138
- if (entry.isDirectory()) {
139
- traverse(fullPath);
140
- } else if (entry.isFile()) {
141
- try {
142
- const stats = fs.statSync(fullPath);
143
- totalBytes += stats.size;
144
- // Index files: inside "indexes" folder or ending in .jsonl
145
- const isIndexFile = fullPath.includes(`${path.sep}indexes${path.sep}`) ||
146
- fullPath.endsWith('.jsonl') ||
147
- entry.name === 'index.meta.jsonl';
148
- if (isIndexFile) {
149
- indexBytes += stats.size;
150
- } else {
151
- withoutIndexBytes += stats.size;
152
- if (entry.name.endsWith('.axiodb')) {
153
- docCount++;
154
- }
155
- }
156
- } catch (_) {}
157
- }
158
- }
159
- }
160
-
161
- traverse(dirPath);
162
- return { totalBytes, withoutIndexBytes, indexBytes, docCount };
163
- }
164
-
165
- // Enhanced Percentile calculator (min, p25, p50, p75, p90, p95, p99, max, avg)
166
- function calculatePercentiles(latencies) {
167
- if (!latencies || latencies.length === 0) {
168
- return {
169
- min: '0.000',
170
- p25: '0.000',
171
- p50: '0.000',
172
- p75: '0.000',
173
- p90: '0.000',
174
- p95: '0.000',
175
- p99: '0.000',
176
- max: '0.000',
177
- avg: '0.000'
178
- };
179
- }
180
-
181
- const sorted = [...latencies].sort((a, b) => a - b);
182
- const getP = (p) => sorted[Math.min(Math.floor((p / 100) * sorted.length), sorted.length - 1)];
183
- const sum = sorted.reduce((acc, v) => acc + v, 0);
184
-
185
- return {
186
- min: Number(sorted[0]).toFixed(3),
187
- p25: Number(getP(25)).toFixed(3),
188
- p50: Number(getP(50)).toFixed(3),
189
- p75: Number(getP(75)).toFixed(3),
190
- p90: Number(getP(90)).toFixed(3),
191
- p95: Number(getP(95)).toFixed(3),
192
- p99: Number(getP(99)).toFixed(3),
193
- max: Number(sorted[sorted.length - 1]).toFixed(3),
194
- avg: (sum / sorted.length).toFixed(3)
195
- };
196
- }
197
-
198
- // Helper to remove directory cleanly (robust recursive deletion)
199
- function cleanDirectory(dirPath) {
200
- try {
201
- if (fs.existsSync(dirPath)) {
202
- fs.rmSync(dirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
203
- }
204
- } catch (err) {
205
- // Ignore cleanup race conditions
206
- }
207
- }
208
-
209
- // Format percentiles output line
210
- function printPercentiles(stats, indent = ' ') {
211
- console.log(`${indent}${c.dim}• Min:${c.reset} ${stats.min.padStart(7)} ms | ${c.dim}p25:${c.reset} ${stats.p25.padStart(7)} ms | ${c.dim}p50 (Med):${c.reset} ${stats.p50.padStart(7)} ms`);
212
- console.log(`${indent}${c.dim}• p75:${c.reset} ${stats.p75.padStart(7)} ms | ${c.dim}p90:${c.reset} ${stats.p90.padStart(7)} ms | ${c.dim}p95:${c.reset} ${stats.p95.padStart(7)} ms`);
213
- console.log(`${indent}${c.dim}• p99:${c.reset} ${stats.p99.padStart(7)} ms | ${c.dim}Max:${c.reset} ${stats.max.padStart(7)} ms | ${c.dim}Avg:${c.reset} ${stats.avg.padStart(7)} ms`);
214
- }
215
-
216
- // Fast document batch generator with both indexed and unindexed fields
217
- function generateBatch(startIdx, count) {
218
- const batch = new Array(count);
219
- for (let i = 0; i < count; i++) {
220
- const id = startIdx + i;
221
- batch[i] = {
222
- // Indexed fields: email, age, score, name
223
- name: `User_${id}`,
224
- email: `user_${id}@bench.axiodb.org`,
225
- age: 18 + (id % 62),
226
- score: id % 1000,
227
- // Unindexed fields: region, unindexedTag, metadata
228
- region: ['us-east', 'us-west', 'eu-central', 'ap-south'][id % 4],
229
- unindexedTag: `tag_${id}`,
230
- metadata: {
231
- loginCount: id % 50,
232
- tier: id % 10 === 0 ? 'enterprise' : 'standard'
233
- },
234
- active: id % 2 === 0,
235
- createdAt: new Date().toISOString()
236
- };
237
- }
238
- return batch;
239
- }
240
-
241
- async function runBenchmark() {
242
- const opts = parseArgs();
243
- activeBenchmarkDir = opts.dir;
244
- const initMem = process.memoryUsage();
245
-
246
- console.log(`\n${c.bright}${c.cyan}╔═══════════════════════════════════════════════════════════════════╗${c.reset}`);
247
- console.log(`${c.bright}${c.cyan}║ AxioDB Performance & Scale Benchmark Suite ║${c.reset}`);
248
- console.log(`${c.bright}${c.cyan}╚═══════════════════════════════════════════════════════════════════╝${c.reset}`);
249
-
250
- console.log(`\n${c.bright}System Environment:${c.reset}`);
251
- console.log(` ${c.dim}• Platform:${c.reset} ${os.type()} ${os.release()} (${os.arch()})`);
252
- console.log(` ${c.dim}• CPUs:${c.reset} ${os.cpus().length} cores (${os.cpus()[0]?.model || 'Generic'})`);
253
- console.log(` ${c.dim}• Total RAM:${c.reset} ${(os.totalmem() / 1e9).toFixed(2)} GB`);
254
- console.log(` ${c.dim}• Available RAM:${c.reset} ${(os.freemem() / 1e9).toFixed(2)} GB`);
255
- console.log(` ${c.dim}• Process RAM Initial:${c.reset} RSS: ${bytesToMB(initMem.rss)} MB | Heap: ${bytesToMB(initMem.heapUsed)} MB`);
256
- console.log(` ${c.dim}• Node.js:${c.reset} ${process.version} (V8: ${process.versions.v8})`);
257
- console.log(` ${c.dim}• Target Scale:${c.reset} ${opts.docs.toLocaleString()} documents (Batch Size: ${opts.batchSize.toLocaleString()})`);
258
-
259
- // Setup cleanup handler for interrupts
260
- const cleanup = () => {
261
- if (!opts.keepData) {
262
- cleanDirectory(opts.dir);
263
- }
264
- };
265
-
266
- process.on('SIGINT', () => {
267
- console.log(`\n${c.yellow}Benchmark interrupted. Cleaning up test data...${c.reset}`);
268
- cleanup();
269
- process.exit(1);
270
- });
271
-
272
- process.on('SIGTERM', () => {
273
- console.log(`\n${c.yellow}Benchmark terminated. Cleaning up test data...${c.reset}`);
274
- cleanup();
275
- process.exit(1);
276
- });
277
-
278
- // Ensure clean test directory before start
279
- cleanDirectory(opts.dir);
280
-
281
- let peakMemory = process.memoryUsage().heapUsed;
282
- let peakRss = process.memoryUsage().rss;
283
- let storageFootprint = { totalBytes: 0, withoutIndexBytes: 0, indexBytes: 0, docCount: 0 };
284
-
285
- const trackMemory = () => {
286
- const mem = process.memoryUsage();
287
- if (mem.heapUsed > peakMemory) peakMemory = mem.heapUsed;
288
- if (mem.rss > peakRss) peakRss = mem.rss;
289
- };
290
-
291
- console.log(`\n${c.bright}${c.blue}[1/6] Initializing Database & Indexes...${c.reset}`);
292
- const db = new AxioDB({ GUI: false, RootName: 'BenchmarkDB', CustomPath: opts.dir });
293
- const testDB = await db.createDB('BenchDB');
294
- const collection = await testDB.createCollection('Users');
295
-
296
- // Create multi-field indexes before bulk ingestion
297
- console.log(` ${c.dim}Creating indexes on (email, age, score, name)...${c.reset}`);
298
- const idxStart = process.hrtime.bigint();
299
- await collection.newIndex('email', 'age', 'score', 'name');
300
- const idxDuration = nsToMs(process.hrtime.bigint() - idxStart);
301
- console.log(` ${c.green}✓ Indexes created in ${idxDuration.toFixed(2)}ms${c.reset}`);
302
-
303
- // -------------------------------------------------------------
304
- // PHASE 1: Bulk Ingestion (insertMany)
305
- // -------------------------------------------------------------
306
- console.log(`\n${c.bright}${c.blue}[2/6] Ingesting ${opts.docs.toLocaleString()} Documents in Batches of ${opts.batchSize.toLocaleString()}...${c.reset}`);
307
- const totalBatches = Math.ceil(opts.docs / opts.batchSize);
308
- let totalInserted = 0;
309
-
310
- const ingestStartTime = process.hrtime.bigint();
311
-
312
- for (let b = 0; b < totalBatches; b++) {
313
- const currentBatchCount = Math.min(opts.batchSize, opts.docs - totalInserted);
314
- const batch = generateBatch(totalInserted, currentBatchCount);
315
-
316
- const batchStart = process.hrtime.bigint();
317
- const res = await collection.insertMany(batch);
318
- const batchDuration = nsToMs(process.hrtime.bigint() - batchStart);
319
-
320
- if (res && (res.status === true || res.statusCode === 200)) {
321
- totalInserted += currentBatchCount;
322
- } else {
323
- console.error(`${c.red}Failed inserting batch ${b + 1}: ${JSON.stringify(res)}${c.reset}`);
324
- break;
325
- }
326
-
327
- trackMemory();
328
- const currentMem = process.memoryUsage();
329
- const rate = Math.round((currentBatchCount / (batchDuration / 1000)));
330
- const percent = Math.round((totalInserted / opts.docs) * 100);
331
- process.stdout.write(` ${c.dim}Batch ${b + 1}/${totalBatches} (${percent}%): ${totalInserted.toLocaleString()} docs | ${rate.toLocaleString()} docs/sec | ${batchDuration.toFixed(0)}ms | RSS: ${bytesToMB(currentMem.rss)}MB | Heap: ${bytesToMB(currentMem.heapUsed)}MB\r${c.reset}`);
332
- }
333
-
334
- const totalIngestTimeMs = nsToMs(process.hrtime.bigint() - ingestStartTime);
335
- const overallIngestRate = safeRate(totalInserted, totalIngestTimeMs);
336
-
337
- console.log(`\n ${c.green}✓ Ingestion Complete: ${totalInserted.toLocaleString()} docs in ${(totalIngestTimeMs / 1000).toFixed(2)}s (${c.bright}${overallIngestRate.toLocaleString()} docs/sec${c.reset}${c.green})${c.reset}`);
338
-
339
- const countCheck = await collection.totalDocuments();
340
- const confirmedTotal = countCheck?.data?.total ?? totalInserted;
341
- console.log(` ${c.dim}Verified physical document count: ${confirmedTotal.toLocaleString()} on disk${c.reset}`);
342
-
343
- // Measure exact storage footprint on disk while all documents are present
344
- storageFootprint = calculateDirSizes(opts.dir);
345
- console.log(` ${c.dim}Directory Size:${c.reset} Total: ${bytesToReadable(storageFootprint.totalBytes)} | Data only (no index): ${bytesToReadable(storageFootprint.withoutIndexBytes)} | Indexes: ${bytesToReadable(storageFootprint.indexBytes)}`);
346
-
347
- const readQueryCount = clampWorkload(opts.queries, totalInserted, 1);
348
- const unindexedReadSamples = clampWorkload(opts.unindexedSamples, totalInserted, 1);
349
- const indexedUpdateCount = clampWorkload(opts.updateQueries, totalInserted, 1);
350
- const indexedDeleteCount = clampWorkload(opts.deleteQueries, totalInserted, 0);
351
- const remainingAfterIndexedDeletes = Math.max(totalInserted - indexedDeleteCount, 0);
352
- const unindexedDeleteCount = clampWorkload(opts.unindexedSamples, remainingAfterIndexedDeletes, 0);
353
-
354
- if (
355
- readQueryCount !== opts.queries ||
356
- unindexedReadSamples !== opts.unindexedSamples ||
357
- indexedUpdateCount !== opts.updateQueries ||
358
- indexedDeleteCount !== opts.deleteQueries ||
359
- unindexedDeleteCount !== opts.unindexedSamples
360
- ) {
361
- console.log(`\n ${c.yellow}⚠ Adjusted benchmark operation counts for current dataset size (${totalInserted.toLocaleString()} docs).${c.reset}`);
362
- console.log(` ${c.dim}Indexed reads:${c.reset} ${readQueryCount} | ${c.dim}Unindexed reads:${c.reset} ${unindexedReadSamples} | ${c.dim}Indexed updates:${c.reset} ${indexedUpdateCount} | ${c.dim}Indexed deletes:${c.reset} ${indexedDeleteCount} | ${c.dim}Unindexed deletes:${c.reset} ${unindexedDeleteCount}`);
363
- }
364
-
365
- // -------------------------------------------------------------
366
- // PHASE 2: Read Latency: Indexed vs Unindexed
367
- // -------------------------------------------------------------
368
- console.log(`\n${c.bright}${c.blue}[3/6] Testing Read Latency (Indexed vs Unindexed)...${c.reset}`);
369
-
370
- // 2A: Indexed Point Reads
371
- console.log(` ${c.dim}• Testing Indexed Point Reads (${readQueryCount.toLocaleString()} random exact matches)...${c.reset}`);
372
- const indexedReadLatencies = [];
373
- const readStart = process.hrtime.bigint();
374
-
375
- for (let q = 0; q < readQueryCount; q++) {
376
- const targetId = Math.floor(Math.random() * totalInserted);
377
- const targetEmail = `user_${targetId}@bench.axiodb.org`;
378
-
379
- const qStart = process.hrtime.bigint();
380
- const result = await collection.query({ email: targetEmail }).exec();
381
- const qDuration = nsToMs(process.hrtime.bigint() - qStart);
382
-
383
- if (result && (result.status === true || result.statusCode === 200) && result.data?.documents?.length > 0) {
384
- indexedReadLatencies.push(qDuration);
385
- }
386
- }
387
-
388
- const totalReadTimeMs = nsToMs(process.hrtime.bigint() - readStart);
389
- const readQPS = safeRate(readQueryCount, totalReadTimeMs);
390
- const indexedReadStats = calculatePercentiles(indexedReadLatencies);
391
-
392
- console.log(` ${c.green}✓ Indexed Reads:${c.reset} ${indexedReadLatencies.length}/${readQueryCount} successful (${c.bright}${readQPS.toLocaleString()} QPS${c.reset})`);
393
- printPercentiles(indexedReadStats);
394
-
395
- // 2B: Unindexed Reads (Full Collection Scan)
396
- console.log(`\n ${c.dim}• Testing Unindexed Point Reads (${unindexedReadSamples} full-scan lookups on 'unindexedTag')...${c.reset}`);
397
- const unindexedReadLatencies = [];
398
- const unindexedReadStart = process.hrtime.bigint();
399
-
400
- for (let q = 0; q < unindexedReadSamples; q++) {
401
- const targetId = Math.floor(Math.random() * totalInserted);
402
- const targetTag = `tag_${targetId}`;
403
-
404
- const uStart = process.hrtime.bigint();
405
- const result = await collection.query({ unindexedTag: targetTag }).exec();
406
- const uDuration = nsToMs(process.hrtime.bigint() - uStart);
407
-
408
- if (result && (result.status === true || result.statusCode === 200) && result.data?.documents?.length > 0) {
409
- unindexedReadLatencies.push(uDuration);
410
- }
411
- }
412
-
413
- const totalUnindexedReadTime = nsToMs(process.hrtime.bigint() - unindexedReadStart);
414
- const unindexedReadQPS = safeRate(unindexedReadSamples, totalUnindexedReadTime);
415
- const unindexedReadStats = calculatePercentiles(unindexedReadLatencies);
416
-
417
- console.log(` ${c.green}✓ Unindexed Reads:${c.reset} ${unindexedReadLatencies.length}/${unindexedReadSamples} successful (${unindexedReadQPS.toLocaleString()} QPS)`);
418
- printPercentiles(unindexedReadStats);
419
-
420
- const indexAdvantage = (Number(unindexedReadStats.avg) / Math.max(Number(indexedReadStats.avg), 0.001)).toFixed(1);
421
- console.log(` ${c.cyan}⚡ Index Advantage:${c.reset} Indexed reads are ${c.bright}${indexAdvantage}x faster${c.reset} than unindexed full scans`);
422
-
423
- // -------------------------------------------------------------
424
- // PHASE 3: Update Latency: Indexed vs Unindexed
425
- // -------------------------------------------------------------
426
- console.log(`\n${c.bright}${c.blue}[4/6] Testing Update Latency (UpdateOne: Indexed vs Unindexed)...${c.reset}`);
427
-
428
- // 3A: Indexed Updates
429
- console.log(` ${c.dim}• Testing Indexed UpdateOne (${indexedUpdateCount} queries on indexed 'email')...${c.reset}`);
430
- const indexedUpdateLatencies = [];
431
-
432
- for (let u = 0; u < indexedUpdateCount; u++) {
433
- const targetId = Math.floor(Math.random() * totalInserted);
434
- const targetEmail = `user_${targetId}@bench.axiodb.org`;
435
-
436
- const uStart = process.hrtime.bigint();
437
- const res = await collection.update({ email: targetEmail }).UpdateOne({ score: 9999 });
438
- const uDuration = nsToMs(process.hrtime.bigint() - uStart);
439
-
440
- if (res && (res.status === true || res.statusCode === 200)) {
441
- indexedUpdateLatencies.push(uDuration);
442
- }
443
- }
444
-
445
- const indexedUpdateStats = calculatePercentiles(indexedUpdateLatencies);
446
- console.log(` ${c.green}✓ Indexed Updates:${c.reset} ${indexedUpdateLatencies.length}/${indexedUpdateCount} successful`);
447
- printPercentiles(indexedUpdateStats);
448
-
449
- // 3B: Unindexed Updates
450
- console.log(`\n ${c.dim}• Testing Unindexed UpdateOne (${unindexedReadSamples} queries on 'unindexedTag')...${c.reset}`);
451
- const unindexedUpdateLatencies = [];
452
-
453
- for (let u = 0; u < unindexedReadSamples; u++) {
454
- const targetId = Math.floor(Math.random() * totalInserted);
455
- const targetTag = `tag_${targetId}`;
456
-
457
- const uStart = process.hrtime.bigint();
458
- const res = await collection.update({ unindexedTag: targetTag }).UpdateOne({ score: 8888 });
459
- const uDuration = nsToMs(process.hrtime.bigint() - uStart);
460
-
461
- if (res && (res.status === true || res.statusCode === 200)) {
462
- unindexedUpdateLatencies.push(uDuration);
463
- }
464
- }
465
-
466
- const unindexedUpdateStats = calculatePercentiles(unindexedUpdateLatencies);
467
- console.log(` ${c.green}✓ Unindexed Updates:${c.reset} ${unindexedUpdateLatencies.length}/${unindexedReadSamples} successful`);
468
- printPercentiles(unindexedUpdateStats);
469
-
470
- // -------------------------------------------------------------
471
- // PHASE 4: Delete Latency: Indexed vs Unindexed
472
- // -------------------------------------------------------------
473
- console.log(`\n${c.bright}${c.blue}[5/6] Testing Delete Latency (deleteOne: Indexed vs Unindexed)...${c.reset}`);
474
-
475
- // 4A: Indexed Deletes
476
- console.log(` ${c.dim}• Testing Indexed deleteOne (${indexedDeleteCount} deletions by indexed 'email')...${c.reset}`);
477
- const indexedDeleteLatencies = [];
478
-
479
- for (let d = 0; d < indexedDeleteCount; d++) {
480
- const targetId = totalInserted - 1 - d;
481
- if (targetId < 0) break;
482
- const targetEmail = `user_${targetId}@bench.axiodb.org`;
483
-
484
- const dStart = process.hrtime.bigint();
485
- const res = await collection.delete({ email: targetEmail }).deleteOne();
486
- const dDuration = nsToMs(process.hrtime.bigint() - dStart);
487
-
488
- if (res && (res.status === true || res.statusCode === 200)) {
489
- indexedDeleteLatencies.push(dDuration);
490
- }
491
- }
492
-
493
- const indexedDeleteStats = calculatePercentiles(indexedDeleteLatencies);
494
- console.log(` ${c.green}✓ Indexed Deletions:${c.reset} ${indexedDeleteLatencies.length}/${indexedDeleteCount} successful`);
495
- printPercentiles(indexedDeleteStats);
496
-
497
- // 4B: Unindexed Deletes
498
- console.log(`\n ${c.dim}• Testing Unindexed deleteOne (${unindexedDeleteCount} deletions by 'unindexedTag')...${c.reset}`);
499
- const unindexedDeleteLatencies = [];
500
-
501
- for (let d = 0; d < unindexedDeleteCount; d++) {
502
- const targetId = totalInserted - 1 - indexedDeleteCount - d;
503
- if (targetId < 0) break;
504
- const targetTag = `tag_${targetId}`;
505
-
506
- const dStart = process.hrtime.bigint();
507
- const res = await collection.delete({ unindexedTag: targetTag }).deleteOne();
508
- const dDuration = nsToMs(process.hrtime.bigint() - dStart);
509
-
510
- if (res && (res.status === true || res.statusCode === 200)) {
511
- unindexedDeleteLatencies.push(dDuration);
512
- }
513
- }
514
-
515
- const unindexedDeleteStats = calculatePercentiles(unindexedDeleteLatencies);
516
- console.log(` ${c.green}✓ Unindexed Deletions:${c.reset} ${unindexedDeleteLatencies.length}/${unindexedDeleteCount} successful`);
517
- printPercentiles(unindexedDeleteStats);
518
-
519
- // -------------------------------------------------------------
520
- // PHASE 5: In-Memory Cache & ACID Transactions
521
- // -------------------------------------------------------------
522
- console.log(`\n${c.bright}${c.blue}[6/6] Testing InMemoryCache & ACID Transaction Integrity...${c.reset}`);
523
-
524
- // Cache test
525
- let warmStats = calculatePercentiles([]);
526
- let cacheSpeedupLabel = 'N/A (insufficient remaining documents)';
527
- const remainingForCache = totalInserted - indexedDeleteCount - unindexedDeleteCount;
528
-
529
- if (remainingForCache > 0) {
530
- const cacheSampleId = Math.floor(Math.random() * remainingForCache);
531
- const cacheSampleEmail = `user_${cacheSampleId}@bench.axiodb.org`;
532
-
533
- const coldStart = process.hrtime.bigint();
534
- await collection.query({ email: cacheSampleEmail }).exec();
535
- const coldMs = nsToMs(process.hrtime.bigint() - coldStart);
536
-
537
- const warmLatencies = [];
538
- for (let i = 0; i < 100; i++) {
539
- const wStart = process.hrtime.bigint();
540
- await collection.query({ email: cacheSampleEmail }).exec();
541
- warmLatencies.push(nsToMs(process.hrtime.bigint() - wStart));
542
- }
543
- warmStats = calculatePercentiles(warmLatencies);
544
- const cacheSpeedup = (Number(coldMs) / Math.max(Number(warmStats.avg), 0.001)).toFixed(1);
545
- cacheSpeedupLabel = `${cacheSpeedup}x faster`;
546
-
547
- console.log(` ${c.green}✓ Cache Acceleration:${c.reset} Cold: ${coldMs.toFixed(3)} ms → Warm: ${warmStats.avg} ms (${c.bright}${cacheSpeedupLabel}${c.reset})`);
548
- } else {
549
- console.log(` ${c.yellow}⚠ Cache Acceleration test skipped:${c.reset} no documents remained after delete phases.`);
550
- }
551
-
552
- // ACID Atomic Commit Test (1,000 documents)
553
- console.log(` ${c.dim}• Testing Atomic Commit (1,000 records in 1 transaction)...${c.reset}`);
554
- const txCommitStart = process.hrtime.bigint();
555
- const txCommit = collection.beginTransaction();
556
- for (let i = 0; i < 1000; i++) {
557
- txCommit.insert({
558
- name: `TxCommit_${i}`,
559
- email: `txcommit_${i}@bench.axiodb.org`,
560
- age: 28,
561
- score: 1000,
562
- createdAt: new Date().toISOString()
563
- });
564
- }
565
- const commitRes = await txCommit.commit();
566
- const txCommitMs = nsToMs(process.hrtime.bigint() - txCommitStart);
567
-
568
- if (commitRes && (commitRes.status === true || commitRes.statusCode === 200)) {
569
- console.log(` ${c.green}✓ Atomic Commit Verified:${c.reset} 1,000 records committed in ${txCommitMs.toFixed(2)} ms`);
570
- } else {
571
- console.log(` ${c.red}✗ Atomic Commit Failed:${c.reset} ${JSON.stringify(commitRes)}`);
572
- }
573
-
574
- // ACID Atomic Rollback Test (1,000 documents, 0 data leakage)
575
- console.log(` ${c.dim}• Testing Atomic Rollback (1,000 records staged then rolled back)...${c.reset}`);
576
- const countBeforeRollback = await collection.totalDocuments();
577
- const docsBefore = countBeforeRollback?.data?.total || 0;
578
-
579
- const txRollback = collection.beginTransaction();
580
- for (let i = 0; i < 1000; i++) {
581
- txRollback.insert({
582
- name: `TxGhost_${i}`,
583
- email: `txghost_${i}@bench.axiodb.org`,
584
- age: 99,
585
- score: 0,
586
- createdAt: new Date().toISOString()
587
- });
588
- }
589
- await txRollback.rollback();
590
- const countAfterRollback = await collection.totalDocuments();
591
- const docsAfter = countAfterRollback?.data?.total || 0;
592
-
593
- if (docsBefore === docsAfter) {
594
- console.log(` ${c.green}✓ ACID Rollback Verified:${c.reset} 100% Consistent (0 leaked records: ${docsBefore.toLocaleString()} before == ${docsAfter.toLocaleString()} after)`);
595
- } else {
596
- console.log(` ${c.red}✗ ACID Rollback Failed:${c.reset} Expected ${docsBefore} docs, found ${docsAfter}`);
597
- }
598
-
599
- // -------------------------------------------------------------
600
- // SUMMARY REPORT
601
- // -------------------------------------------------------------
602
- trackMemory();
603
- const finalMem = process.memoryUsage();
604
-
605
- console.log(`\n${c.bright}${c.cyan}╔═══════════════════════════════════════════════════════════════════╗${c.reset}`);
606
- console.log(`${c.bright}${c.cyan}║ BENCHMARK RESULTS SUMMARY ║${c.reset}`);
607
- console.log(`${c.bright}${c.cyan}╚═══════════════════════════════════════════════════════════════════╝${c.reset}`);
608
-
609
- console.log(` ${c.bright}Dataset Scale:${c.reset} ${opts.docs.toLocaleString()} Documents`);
610
- console.log(` ${c.bright}Ingestion Throughput:${c.reset} ${c.green}${overallIngestRate.toLocaleString()} docs/sec${c.reset} (${totalInserted.toLocaleString()} docs in ${(totalIngestTimeMs / 1000).toFixed(2)}s)`);
611
- console.log(` ${c.bright}Indexed Point Read (QPS):${c.reset} ${c.green}${readQPS.toLocaleString()} QPS${c.reset} (p50: ${indexedReadStats.p50}ms | p95: ${indexedReadStats.p95}ms | p99: ${indexedReadStats.p99}ms)`);
612
- console.log(` ${c.bright}Unindexed Read Scan (QPS):${c.reset} ${unindexedReadQPS.toLocaleString()} QPS (p50: ${unindexedReadStats.p50}ms | p95: ${unindexedReadStats.p95}ms)`);
613
- console.log(` ${c.bright}Index Read Speedup:${c.reset} ${c.green}${indexAdvantage}x faster${c.reset} than full scan`);
614
- console.log(` ${c.bright}Indexed Update (p50):${c.reset} ${indexedUpdateStats.p50} ms (p95: ${indexedUpdateStats.p95}ms)`);
615
- console.log(` ${c.bright}Unindexed Update (p50):${c.reset} ${unindexedUpdateStats.p50} ms (p95: ${unindexedUpdateStats.p95}ms)`);
616
- console.log(` ${c.bright}Indexed Delete (p50):${c.reset} ${indexedDeleteStats.p50} ms (p95: ${indexedDeleteStats.p95}ms)`);
617
- console.log(` ${c.bright}Unindexed Delete (p50):${c.reset} ${unindexedDeleteStats.p50} ms (p95: ${unindexedDeleteStats.p95}ms)`);
618
- console.log(` ${c.bright}Cache Acceleration:${c.reset} ${cacheSpeedupLabel.includes('x faster') ? `${c.green}${cacheSpeedupLabel}${c.reset}` : cacheSpeedupLabel} (${warmStats.avg}ms warm cache)`);
619
- console.log(` ${c.bright}ACID Commit Latency:${c.reset} ${txCommitMs.toFixed(2)} ms for 1,000 atomic documents`);
620
- console.log(` ${c.bright}ACID Rollback Integrity:${c.reset} ${c.green}100% Consistent (0 data leakage)${c.reset}`);
621
- console.log(` ${c.bright}System Available RAM:${c.reset} ${(os.freemem() / 1e9).toFixed(2)} GB / ${(os.totalmem() / 1e9).toFixed(2)} GB`);
622
- console.log(` ${c.bright}Node Process Peak RAM:${c.reset} RSS: ${bytesToMB(peakRss)} MB | Heap: ${bytesToMB(peakMemory)} MB`);
623
- console.log(` ${c.bright}Node Process Final RAM:${c.reset} RSS: ${bytesToMB(finalMem.rss)} MB | Heap: ${bytesToMB(finalMem.heapUsed)} MB`);
624
-
625
- // Dynamic comparison estimation based on sample document
626
- const sampleDoc = generateBatch(0, 1)[0];
627
- const sampleDocBytes = Buffer.byteLength(JSON.stringify(sampleDoc));
628
- const estRawJsonBytes = (sampleDocBytes + 1) * opts.docs + 2;
629
- const estLowDBBytes = estRawJsonBytes + 30;
630
- const estSQLiteBytes = Math.ceil(((sampleDocBytes * 0.85) + 48) * opts.docs / 4096) * 4096;
631
-
632
- console.log(`\n ${c.bright}${c.magenta}On-Disk Storage Footprint (${opts.docs.toLocaleString()} Documents):${c.reset}`);
633
- console.log(` ${c.dim}• Total Directory Size (With Indexes):${c.reset} ${c.green}${bytesToReadable(storageFootprint.totalBytes)}${c.reset} (${bytesToMB(storageFootprint.totalBytes)} MB)`);
634
- console.log(` ${c.dim}• Data Only Size (Excluding Indexes):${c.reset} ${c.green}${bytesToReadable(storageFootprint.withoutIndexBytes)}${c.reset} (${bytesToMB(storageFootprint.withoutIndexBytes)} MB)`);
635
- console.log(` ${c.dim}• Index Storage Overhead (JSONL / Meta):${c.reset} ${bytesToReadable(storageFootprint.indexBytes)} (${((storageFootprint.indexBytes / Math.max(storageFootprint.totalBytes, 1)) * 100).toFixed(1)}% of total)`);
636
-
637
- console.log(`\n ${c.bright}Storage & Architecture Comparison (${opts.docs.toLocaleString()} Records):${c.reset}`);
638
- console.log(` ┌─────────────────────────┬──────────────┬──────────────┬────────────────────────────────────┐`);
639
- console.log(` │ Database Engine │ Disk Size │ Storage Type │ Trade-off / Advantage │`);
640
- console.log(` ├─────────────────────────┼──────────────┼──────────────┼────────────────────────────────────┤`);
641
- console.log(` │ ${c.cyan}AxioDB (Data Only)${c.reset} │ ${bytesToReadable(storageFootprint.withoutIndexBytes).padEnd(12)} │ File-per-doc │ Zero native C++ deps; atomic WAL │`);
642
- console.log(` │ ${c.cyan}AxioDB (With Indexes)${c.reset} │ ${bytesToReadable(storageFootprint.totalBytes).padEnd(12)} │ Data + JSONL │ Sub-ms indexed point reads & cache │`);
643
- console.log(` │ SQLite (Table + Indexes)│ ${('~' + bytesToReadable(estSQLiteBytes)).padEnd(12)} │ Single file │ Requires C++ native build toolchain│`);
644
- console.log(` │ LowDB (JSON File) │ ${('~' + bytesToReadable(estLowDBBytes)).padEnd(12)} │ Monolithic │ Rewrites entire file on every write│`);
645
- console.log(` │ Raw JSON File │ ${('~' + bytesToReadable(estRawJsonBytes)).padEnd(12)} │ Single Array │ No indexing; O(N) memory to parse │`);
646
- console.log(` └─────────────────────────┴──────────────┴──────────────┴────────────────────────────────────┘`);
647
-
648
- // Final cleanup of the temporary directory
649
- if (!opts.keepData) {
650
- cleanDirectory(opts.dir);
651
- console.log(`\n${c.dim}✓ Completely removed temporary benchmark folder at: ${opts.dir}${c.reset}\n`);
652
- } else {
653
- console.log(`\n${c.yellow}Preserved benchmark data at: ${opts.dir}${c.reset}\n`);
654
- }
655
- }
656
-
657
- runBenchmark().catch((err) => {
658
- console.error(`\n${c.red}Benchmark execution encountered an error:${c.reset}`, err);
659
- cleanDirectory(activeBenchmarkDir);
660
- process.exit(1);
661
- });
package/funding.json DELETED
@@ -1,61 +0,0 @@
1
- {
2
- "version": "v1.1.0",
3
- "entity": {
4
- "type": "individual",
5
- "role": "maintainer",
6
- "name": "Ankan Saha",
7
- "email": "connect@ankan.in",
8
- "description": "Systems engineer and open-source maintainer based in West Bengal, India. Creator of AxioDB, an embedded ACID document database for Node.js and Bun with 27,000+ annual npm downloads.",
9
- "webpageUrl": {
10
- "url": "https://ankan.in"
11
- }
12
- },
13
- "projects": [
14
- {
15
- "guid": "axiodb",
16
- "name": "AxioDB",
17
- "description": "Zero-dependency, pure TypeScript embedded ACID document database for Node.js and Bun. Eliminates native C++ compilation (node-gyp) overhead while providing durability, indexing, and high-concurrency document operations for local and edge environments.",
18
- "webpageUrl": {
19
- "url": "https://axiodb.in"
20
- },
21
- "repositoryUrl": {
22
- "url": "https://github.com/nexoral/AxioDB"
23
- },
24
- "licenses": [
25
- "spdx:MIT"
26
- ],
27
- "tags": [
28
- "database",
29
- "typescript",
30
- "nodejs",
31
- "embedded",
32
- "acid",
33
- "developer-tools"
34
- ]
35
- }
36
- ],
37
- "funding": {
38
- "channels": [
39
- {
40
- "guid": "bank-transfer",
41
- "type": "bank",
42
- "address": "Indian Bank Account (Details provided upon grant approval)",
43
- "description": "Direct NEFT/IMPS bank transfer for FOSS United grant disbursement."
44
- }
45
- ],
46
- "plans": [
47
- {
48
- "guid": "axiodb-dev-grant",
49
- "status": "active",
50
- "name": "AxioDB Engine Scaling & Concurrency Benchmarks",
51
- "description": "3-month milestone funding plan to support full-time open-source development: 1) In-memory B-Tree indexer for sub-millisecond query filters; 2) Automated ACID crash-recovery test suite testing process kill durability (SIGKILL resistance); 3) Zero-copy streaming JSON reader for bounded memory consumption under multi-gigabyte collections; 4) Public benchmark dashboard comparing AxioDB with LevelDB and SQLite.",
52
- "amount": 75000,
53
- "currency": "INR",
54
- "frequency": "one-time",
55
- "channels": [
56
- "bank-transfer"
57
- ]
58
- }
59
- ]
60
- }
61
- }