Skip to content

Commit 7fc5a66

Browse files
authored
Merge pull request #10 from gms1/feature/event-loop-utilization-benchmark
benchmark for comparing event loop utilization for different sqlite d…
2 parents 8c94cc1 + 0667846 commit 7fc5a66

21 files changed

Lines changed: 1720 additions & 2 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ prebuilds
3434
.clinerules
3535
.roomodes
3636
/plans
37+
/tools/temp

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,34 @@ npm install @homeofthings/sqlite3 --build-from-source --sqlite_libname=sqlcipher
245245
npm test
246246
```
247247

248+
# Benchmarks
249+
250+
## Driver Comparison
251+
252+
The `tools/benchmark-drivers` directory contains a comprehensive benchmark suite comparing different SQLite drivers for Node.js:
253+
254+
```bash
255+
cd tools/benchmark-drivers
256+
npm install
257+
node index.js
258+
```
259+
260+
This compares `@homeofthings/sqlite3` against other popular SQLite drivers:
261+
- `better-sqlite3` - Synchronous, high-performance
262+
- `node:sqlite` - Built-in Node.js SQLite (v22.6.0+)
263+
264+
**Key insight**: Async drivers like `@homeofthings/sqlite3` show lower raw throughput but provide better event loop availability, allowing other operations to proceed concurrently. Sync drivers block the event loop completely.
265+
266+
## Internal Benchmarks
267+
268+
Internal performance benchmarks are available in `tools/benchmark-internal`:
269+
270+
```bash
271+
node tools/benchmark-internal/run.js
272+
```
273+
274+
See [tools/benchmark-drivers/README.md](tools/benchmark-drivers/README.md) for details.
275+
248276
# Contributors
249277

250278
* [Daniel Lockyer](https://github.com/daniellockyer)

memory-bank/development.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,27 @@ Uses ESLint with configuration in `.eslintrc.js`.
200200

201201
## Benchmarks
202202

203-
Benchmarks use tinybench with proper setup/teardown separation:
203+
### Driver Comparison Benchmarks
204+
205+
The `tools/benchmark-drivers` directory contains a comprehensive benchmark suite comparing different SQLite drivers:
206+
207+
```bash
208+
cd tools/benchmark-drivers
209+
npm install
210+
node index.js
211+
```
212+
213+
This compares `@homeofthings/sqlite3` against:
214+
- `better-sqlite3` - Synchronous, high-performance
215+
- `node:sqlite` - Built-in Node.js SQLite (v22.6.0+)
216+
217+
**Event Loop Utilization**: The benchmarks measure event loop availability:
218+
- Sync drivers (`better-sqlite3`, `node:sqlite`): 0% - blocks completely
219+
- Async drivers (`@homeofthings/sqlite3`): 100% - non-blocking
220+
221+
### Internal Benchmarks
222+
223+
Internal performance benchmarks are in `tools/benchmark-internal`:
204224

205225
```bash
206226
node tools/benchmark-internal/run.js

memory-bank/project-overview.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,10 @@ node-gyp rebuild --debug
160160
# Run tests
161161
yarn test
162162

163-
# Run benchmarks
163+
# Run driver comparison benchmarks
164+
cd tools/benchmark-drivers && npm install && node index.js
165+
166+
# Run internal benchmarks
164167
node tools/benchmark-internal/run.js
165168
```
166169

tools/benchmark-drivers/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules/

tools/benchmark-drivers/README.md

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# SQLite3 Benchmark
2+
3+
A comprehensive benchmark suite comparing the performance and event loop utilization of different SQLite drivers for Node.js.
4+
5+
Thanks to [better-sqlite3/benchmark](https://github.com/WiseLibs/better-sqlite3/tree/master/benchmark) for the initial work!
6+
7+
This benchmark is using small tables with few columns and little data, therefore low I/O, so it's not reasonable to expect an asynchronous driver to perform in anyway better here.
8+
But it is strange, though, that a brief review also highlighted some other “tricks” designed to make the async driver look worse.
9+
10+
- In general, prepared statements were not used for the async driver, but for all others.
11+
The performance improvements are significant, e.g 2.4 x for 'select-iterate', 1.5 x for 'insert'
12+
- The async driver had to open an additional database connection for each isolated transaction, even though this is a limitation of SQLite that affects all drivers equally.
13+
The performance improvements are significant, e.g 'transaction small' is now about 26x faster
14+
15+
## Why Async Drivers are expected to be slower
16+
17+
1. **Event Loop Integration**: Async drivers must integrate with Node.js's event loop, requiring context switches and queue management.
18+
19+
2. **Thread Pool Usage**: Async SQLite operations are using libuv's thread pool, introducing thread scheduling overhead.
20+
21+
Despite lower raw throughput, async drivers provide **Non-Blocking I/O**, by preventing the event loop from being blocked and provide **Concurrency**, by allowing other operations (network requests, file I/O, timers) to proceed while waiting for database operations to complete.
22+
23+
## Supported Drivers
24+
25+
| Driver | Type | Description |
26+
|--------|------|-------------|
27+
| `better-sqlite3` | Synchronous | High-performance synchronous SQLite bindings |
28+
| `@homeofthings/sqlite3` | Asynchronous | Promise-based SQLite bindings (fork of node-sqlite3) |
29+
| `node:sqlite` | Synchronous | Built-in Node.js SQLite (Node.js v22.6.0+) |
30+
31+
## Requirements
32+
33+
- **Node.js**: v20.17.0 or later (for N-API compatibility)
34+
- **For `node:sqlite`**: Node.js v22.6.0+ (experimental) or v22.12.0+ (stable)
35+
36+
## Installation
37+
38+
```bash
39+
npm install
40+
```
41+
42+
## Usage
43+
44+
### Run Default Benchmarks
45+
46+
```bash
47+
node index.js
48+
```
49+
50+
This runs a general-purpose benchmark suite
51+
52+
### Run Specific Benchmarks
53+
54+
```bash
55+
node index.js <search-terms...>
56+
```
57+
58+
Examples:
59+
```bash
60+
# Run only select benchmarks
61+
node index.js select
62+
63+
# Run benchmarks for specific tables
64+
node index.js small
65+
66+
# Run benchmarks for specific columns
67+
node index.js integer text
68+
69+
# Combine search terms
70+
node index.js select small integer
71+
```
72+
73+
### Using Local Development Version
74+
75+
To benchmark the local development version of `@homeofthings/sqlite3` instead of the npm package:
76+
77+
```bash
78+
node index.js --use-local
79+
```
80+
81+
This is useful for testing performance changes before publishing. Requires the native addon to be built:
82+
83+
```bash
84+
# From project root
85+
npm run build
86+
```
87+
88+
The `--use-local` flag can be combined with search terms:
89+
90+
```bash
91+
node index.js --use-local insert small
92+
```
93+
94+
## Benchmark Types
95+
96+
| Type | Description |
97+
|------|-------------|
98+
| `select` | Reading single rows by primary key |
99+
| `select-all` | Reading 100 rows into an array |
100+
| `select-iterate` | Iterating over 100 rows |
101+
| `select-aggregate` | Aggregate functions (COUNT, SUM, AVG, MIN, MAX) with WHERE clause |
102+
| `insert` | Inserting single rows |
103+
| `update` | Updating single rows |
104+
| `transaction` | Inserting 100 rows in a single transaction |
105+
| `update-transaction` | Updating 100 rows in a single transaction |
106+
107+
## Output Format
108+
109+
Results are displayed as:
110+
```
111+
driver-name x 471,255 ops/sec ±0.07% (event loop: 50%, 2.1μs/op)
112+
```
113+
114+
- `x` - Separator (from original benchmark format)
115+
- `ops/sec` - Operations per second (higher is better)
116+
- `±X.XX%` - Relative margin of error
117+
- `event loop: X%, Yμs/op` - Utilization percentage and blocking time per operation (lower is better)
118+
119+
### Example Output
120+
121+
Running `node index.js select` produces output like:
122+
123+
```
124+
select small (nul)
125+
better-sqlite3 x 638,075 ops/sec ±0.44% (event loop: 100%, 1.6μs/op)
126+
@homeofthings/sqlite3 x 88,459 ops/sec ±0.82% (event loop: 47%, 5.3μs/op)
127+
node:sqlite x 543,445 ops/sec ±0.53% (event loop: 100%, 1.8μs/op)
128+
```
129+
130+
### Event Loop Metrics
131+
132+
The **event loop** metrics show how the driver affects the event loop (measured using Node.js native `performance.eventLoopUtilization()` API):
133+
134+
**Utilization Percentage:** How much of the benchmark time the event loop was busy (100% = completely blocked, 0% = completely free)
135+
136+
**Time per Operation:** `(1,000,000 μs/sec ÷ ops/sec) × utilization = μs blocked per operation`
137+
138+
| Driver | Utilization | Time per Op | Meaning |
139+
|-------------------------|-------------|-------------|-------------------------------------------------|
140+
| `better-sqlite3` | 100% | ~1.6μs/op | Blocks completely - all time is event loop time |
141+
| `@homeofthings/sqlite3` | ~47% | ~5.3μs/op | 3.3x more blocking than sync drivers |
142+
| `node:sqlite` | 100% | ~1.8μs/op | Blocks completely - all time is event loop time |
143+
144+
Such metric shows the real cost: dependend on the operation, async drivers may even block the event loop **longer in total** for the same amount of work, even though they don't block it **completely** for the whole operation, like the sync drivers do. However, async drivers also do not always block the event loop **longer in total**:
145+
146+
```
147+
--- inserting rows individually ---
148+
better-sqlite3 x 139,898 ops/sec ±21.94% (event loop: 100%, 7.1μs/op)
149+
@homeofthings/sqlite3 x 47,619 ops/sec ±18.89% (event loop: 22%, 4.6μs/op)
150+
node:sqlite x 128,465 ops/sec ±22.25% (event loop: 100%, 7.8μs/op)
151+
```
152+
153+
### Large Data Performance
154+
155+
For I/O-bound operations (large data reads), async drivers can actually **outperform** sync drivers:
156+
157+
```
158+
--- reading large blobs (16MB each) ---
159+
better-sqlite3 x 83 ops/sec ±7.99% (event loop: 100%, 12.07ms/op)
160+
@homeofthings/sqlite3 x 94 ops/sec ±8.57% (event loop: 34%, 3.63ms/op)
161+
node:sqlite x 127 ops/sec ±10.75% (event loop: 100%, 7.88ms/op)
162+
```
163+
164+
**Why async wins for large data:**
165+
166+
1. **Lower event loop blocking**: 3.63ms vs 12.07ms - async driver blocks 70% less
167+
2. **Higher throughput**: 94 vs 83 ops/sec - async driver is 13% faster
168+
3. **Event loop availability**: 66% free during async operations
169+
170+
For I/O-bound operations, the async driver's overhead becomes negligible compared to disk I/O wait time. The ability to interleave other work becomes an advantage - the event loop can process other tasks while waiting for data.
171+
172+
### Long Running Query Performance
173+
174+
With such a small amount of data we are currently using, it's not so easy to simulate longer running queries. That's why I tried it here using simple aggregation.
175+
176+
Aggregate functions (COUNT, SUM, AVG, MIN, MAX) with WHERE clauses show even more dramatic async advantages:
177+
178+
```
179+
--- aggregate functions (COUNT, SUM, AVG, MIN, MAX) with WHERE clause ---
180+
better-sqlite3 x 11,246 ops/sec ±0.27% (event loop: 100%, 88.9μs/op)
181+
@homeofthings/sqlite3 x 68,779 ops/sec ±0.60% (event loop: 47%, 6.8μs/op)
182+
node:sqlite x 10,982 ops/sec ±0.40% (event loop: 100%, 91.1μs/op)
183+
```
184+
185+
**Why async wins for aggregation:**
186+
187+
1. **6x higher throughput**: 68,779 vs 11,246 ops/sec
188+
2. **13x less event loop blocking**: 6.8μs/op vs 88.9μs/op
189+
3. **Same pattern as large data**: I/O-bound operations benefit from async
190+
191+
Aggregation queries scan 1000 rows per operation. The async driver's ability to yield during I/O makes it significantly more efficient for these multi-row operations.
192+
193+
## Project Structure
194+
195+
```
196+
├── index.js # Main orchestrator
197+
├── benchmark.js # Benchmark runner (tinybench)
198+
├── drivers.js # SQLite driver configurations
199+
├── trials.js # Benchmark trial definitions
200+
├── seed.js # Database seeding
201+
├── types/
202+
│ ├── insert.js # Insert benchmark
203+
│ ├── select.js # Single row select benchmark
204+
│ ├── select-all.js # Multi-row select benchmark
205+
│ ├── select-aggregate.js # Aggregate functions benchmark
206+
│ ├── select-iterate.js # Iteration benchmark
207+
│ └── transaction.js # Transaction benchmark
208+
└── temp/ # Temporary database files (auto-created)
209+
```
210+
211+
## Architecture
212+
213+
Each benchmark runs in an isolated child process to ensure:
214+
- Clean state for each measurement
215+
- Memory isolation between runs
216+
- No interference between drivers
217+
218+
## Adding a New Driver
219+
220+
1. Add the driver to `package.json` dependencies
221+
2. Add a connection function to `drivers.js`:
222+
```javascript
223+
['driver-name', async (filename, pragma) => {
224+
const db = require('driver-package')(filename);
225+
// Apply PRAGMA settings
226+
for (const str of pragma) await db.exec(`PRAGMA ${str}`);
227+
return db;
228+
}]
229+
```
230+
3. Add benchmark implementations in each `types/*.js` file:
231+
```javascript
232+
// Either return sync or async function
233+
234+
exports['driver-name'] = (db, ctx) => {
235+
return () => db.someOperation();
236+
};
237+
238+
exports['driver-name'] = async (db, ctx) => {
239+
return () => db.someOperation();
240+
};
241+
```

0 commit comments

Comments
 (0)