axiodb 22.26.0 → 22.27.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/README.md +83 -0
- package/benchmark/run.js +661 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -115,6 +115,89 @@ Full per-suite breakdown: [axiodb.in/performance](https://axiodb.in/performance)
|
|
|
115
115
|
|
|
116
116
|
Run it yourself with `npm test`.
|
|
117
117
|
|
|
118
|
+
## ⚡ Benchmarking & Performance Verification
|
|
119
|
+
|
|
120
|
+
If you are evaluating AxioDB, testing performance for your application, or verifying throughput and ACID rollback guarantees, you can run the built-in benchmark suite on your own machine.
|
|
121
|
+
|
|
122
|
+
No configuration or database setup is needed: simply clone the repository, install dependencies, and run:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
git clone https://github.com/nexoral/AxioDB.git
|
|
126
|
+
cd AxioDB
|
|
127
|
+
npm install
|
|
128
|
+
npm run benchmark
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
> **Automated Pre-Check:** The benchmark script checks if compiled files exist in `lib/`. If not, it automatically runs `npm run build` before starting. It also handles cleanup automatically, deleting the temporary benchmark directory upon completion or on `Ctrl+C`.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
### Default Benchmark Configuration
|
|
136
|
+
|
|
137
|
+
Running `npm run benchmark` without arguments executes the default verification profile:
|
|
138
|
+
* **Scale:** **`15,000` documents** inserted via WAL-backed `insertMany`.
|
|
139
|
+
* **Batch Size:** Chunked into **`3,000` documents** per batch to ensure optimal V8 heap utilization.
|
|
140
|
+
* **Indexed Point Reads:** 500 random lookups on the indexed `email` field (reporting **Min, p25, p50, p75, p90, p95, p99, Max, and Avg** latencies).
|
|
141
|
+
* **Unindexed Scans:** 25 random lookups requiring a full collection scan (calculating the exact **Index Advantage multiplier**).
|
|
142
|
+
* **Updates & Deletions:** Compares indexed vs unindexed `UpdateOne` and `deleteOne` latency.
|
|
143
|
+
* **Cache Acceleration:** Measures cold lookups against warm `InMemoryCache` speedup (typically **~50x+ faster**).
|
|
144
|
+
* **ACID Transactions:** Verifies an atomic batch commit (1,000 documents) and an atomic batch rollback (1,000 documents) confirming **zero data leakage**.
|
|
145
|
+
* **Storage Footprint:** Measures physical directory size on disk (**With Indexes** vs. **Data Only**) and compares against estimated storage for SQLite, LowDB, and raw JSON files.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
### Command-Line Arguments & Flags
|
|
150
|
+
|
|
151
|
+
You can customize the benchmark scale, batch chunking, query samples, and cleanup behavior:
|
|
152
|
+
|
|
153
|
+
| Flag | Type | Default | Description |
|
|
154
|
+
| :--- | :--- | :--- | :--- |
|
|
155
|
+
| `--docs=N` | `number` | `15000` | Total number of documents to insert and index (e.g. `5000`, `50000`, `100000`, `500000`). |
|
|
156
|
+
| `--batch=N` | `number` | `3000` | Chunk size per `insertMany` batch to keep memory lean. |
|
|
157
|
+
| `--queries=N` | `number` | `500` | Number of random indexed point queries to measure for latency percentiles. |
|
|
158
|
+
| `--unindexed=N` | `number` | `25` | Number of unindexed full-scan lookups, updates, and deletes to sample. |
|
|
159
|
+
| `--keep` | `boolean` | `false` | Preserves the temporary benchmark database on disk for inspection instead of auto-deleting. |
|
|
160
|
+
| `--help`, `-h` | `boolean` | — | Displays the help menu with all options and usage examples. |
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
### Ready-to-Copy Benchmark Commands
|
|
165
|
+
|
|
166
|
+
#### 1. Default Benchmark (15k docs)
|
|
167
|
+
```bash
|
|
168
|
+
npm run benchmark
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### 2. Quick Smoke Test (5,000 docs — fast ~2s run)
|
|
172
|
+
```bash
|
|
173
|
+
npm run benchmark -- --docs=5000 --batch=1000 --queries=100 --unindexed=10
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### 3. Mid-Scale Test (50,000 docs, 5k batches)
|
|
177
|
+
```bash
|
|
178
|
+
npm run benchmark -- --docs=50000 --batch=5000 --queries=500
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### 4. High-Volume Stress Test (100,000 docs, 10k batches)
|
|
182
|
+
```bash
|
|
183
|
+
npm run benchmark -- --docs=100000 --batch=10000 --queries=1000
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### 5. Maximum Capacity Stress Test (500,000 docs)
|
|
187
|
+
```bash
|
|
188
|
+
npm run benchmark -- --docs=500000 --batch=10000 --queries=1000 --unindexed=15
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### 6. Inspect Physical Storage & Indexes on Disk (`--keep`)
|
|
192
|
+
```bash
|
|
193
|
+
# Run benchmark and prevent automatic cleanup of the data directory
|
|
194
|
+
npm run benchmark -- --docs=15000 --keep
|
|
195
|
+
|
|
196
|
+
# Inspect the created .axiodb document files and indexes/*.jsonl files:
|
|
197
|
+
ls -lh benchmark/Benchmark_TempData/BenchmarkDB/BenchDB/Users
|
|
198
|
+
ls -lh benchmark/Benchmark_TempData/BenchmarkDB/BenchDB/Users/indexes
|
|
199
|
+
```
|
|
200
|
+
|
|
118
201
|
## Installation
|
|
119
202
|
|
|
120
203
|
### npm — the library
|
package/benchmark/run.js
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
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/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "axiodb",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.27.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",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "node ./Test/run.js",
|
|
9
|
+
"benchmark": "node -e \"if (!require('fs').existsSync('./lib/config/DB.js')) require('child_process').execSync('npm run build', { stdio: 'inherit' });\" && node ./benchmark/run.js",
|
|
9
10
|
"build": "tsc",
|
|
10
11
|
"lint": "eslint source/",
|
|
11
12
|
"lint:fix": "eslint source/ --fix"
|