From cce74421e41373a70cf0fa47b5db65ab24e55812 Mon Sep 17 00:00:00 2001 From: Guenter Sandner Date: Sat, 11 Apr 2026 19:22:26 +0200 Subject: [PATCH 1/2] benchmark for comparing event loop utilization for different sqlite drivers --- .gitignore | 1 + README.md | 28 + memory-bank/development.md | 22 +- memory-bank/project-overview.md | 5 +- tools/benchmark-drivers/.gitignore | 1 + tools/benchmark-drivers/README.md | 218 +++++ tools/benchmark-drivers/benchmark.js | 75 ++ tools/benchmark-drivers/drivers.js | 43 + tools/benchmark-drivers/index.js | 88 ++ tools/benchmark-drivers/package.json | 18 + tools/benchmark-drivers/seed.js | 47 + tools/benchmark-drivers/trials.js | 98 +++ tools/benchmark-drivers/types/insert.js | 22 + tools/benchmark-drivers/types/select-all.js | 20 + .../benchmark-drivers/types/select-iterate.js | 33 + tools/benchmark-drivers/types/select.js | 20 + tools/benchmark-drivers/types/transaction.js | 43 + .../types/update-transaction.js | 43 + tools/benchmark-drivers/types/update.js | 25 + tools/benchmark-drivers/yarn.lock | 800 ++++++++++++++++++ 20 files changed, 1648 insertions(+), 2 deletions(-) create mode 100644 tools/benchmark-drivers/.gitignore create mode 100644 tools/benchmark-drivers/README.md create mode 100755 tools/benchmark-drivers/benchmark.js create mode 100644 tools/benchmark-drivers/drivers.js create mode 100755 tools/benchmark-drivers/index.js create mode 100644 tools/benchmark-drivers/package.json create mode 100644 tools/benchmark-drivers/seed.js create mode 100644 tools/benchmark-drivers/trials.js create mode 100644 tools/benchmark-drivers/types/insert.js create mode 100644 tools/benchmark-drivers/types/select-all.js create mode 100644 tools/benchmark-drivers/types/select-iterate.js create mode 100644 tools/benchmark-drivers/types/select.js create mode 100644 tools/benchmark-drivers/types/transaction.js create mode 100644 tools/benchmark-drivers/types/update-transaction.js create mode 100644 tools/benchmark-drivers/types/update.js create mode 100644 tools/benchmark-drivers/yarn.lock diff --git a/.gitignore b/.gitignore index c36d29e3..15f378d7 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ prebuilds .clinerules .roomodes /plans +/tools/temp diff --git a/README.md b/README.md index 10c5b02a..eb75b5e3 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,34 @@ npm install @homeofthings/sqlite3 --build-from-source --sqlite_libname=sqlcipher npm test ``` +# Benchmarks + +## Driver Comparison + +The `tools/benchmark-drivers` directory contains a comprehensive benchmark suite comparing different SQLite drivers for Node.js: + +```bash +cd tools/benchmark-drivers +npm install +node index.js +``` + +This compares `@homeofthings/sqlite3` against other popular SQLite drivers: +- `better-sqlite3` - Synchronous, high-performance +- `node:sqlite` - Built-in Node.js SQLite (v22.6.0+) + +**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. + +## Internal Benchmarks + +Internal performance benchmarks are available in `tools/benchmark-internal`: + +```bash +node tools/benchmark-internal/run.js +``` + +See [tools/benchmark-drivers/README.md](tools/benchmark-drivers/README.md) for details. + # Contributors * [Daniel Lockyer](https://github.com/daniellockyer) diff --git a/memory-bank/development.md b/memory-bank/development.md index 267df38c..682432f9 100644 --- a/memory-bank/development.md +++ b/memory-bank/development.md @@ -200,7 +200,27 @@ Uses ESLint with configuration in `.eslintrc.js`. ## Benchmarks -Benchmarks use tinybench with proper setup/teardown separation: +### Driver Comparison Benchmarks + +The `tools/benchmark-drivers` directory contains a comprehensive benchmark suite comparing different SQLite drivers: + +```bash +cd tools/benchmark-drivers +npm install +node index.js +``` + +This compares `@homeofthings/sqlite3` against: +- `better-sqlite3` - Synchronous, high-performance +- `node:sqlite` - Built-in Node.js SQLite (v22.6.0+) + +**Event Loop Utilization**: The benchmarks measure event loop availability: +- Sync drivers (`better-sqlite3`, `node:sqlite`): 0% - blocks completely +- Async drivers (`@homeofthings/sqlite3`): 100% - non-blocking + +### Internal Benchmarks + +Internal performance benchmarks are in `tools/benchmark-internal`: ```bash node tools/benchmark-internal/run.js diff --git a/memory-bank/project-overview.md b/memory-bank/project-overview.md index e16a6ad3..992b360c 100644 --- a/memory-bank/project-overview.md +++ b/memory-bank/project-overview.md @@ -160,7 +160,10 @@ node-gyp rebuild --debug # Run tests yarn test -# Run benchmarks +# Run driver comparison benchmarks +cd tools/benchmark-drivers && npm install && node index.js + +# Run internal benchmarks node tools/benchmark-internal/run.js ``` diff --git a/tools/benchmark-drivers/.gitignore b/tools/benchmark-drivers/.gitignore new file mode 100644 index 00000000..c2658d7d --- /dev/null +++ b/tools/benchmark-drivers/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/tools/benchmark-drivers/README.md b/tools/benchmark-drivers/README.md new file mode 100644 index 00000000..1946ecea --- /dev/null +++ b/tools/benchmark-drivers/README.md @@ -0,0 +1,218 @@ +# SQLite3 Benchmark + +A comprehensive benchmark suite comparing the performance and event loop utilization of different SQLite drivers for Node.js. + +Thanks to [better-sqlite3/benchmark](https://github.com/WiseLibs/better-sqlite3/tree/master/benchmark) for the initial work! + +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. +But it is strange, though, that a brief review also highlighted some other “tricks” designed to make the async driver look worse. + +- In general, prepared statements were not used for the async driver, but for all others. + The performance improvements are significant, e.g 2.4 x for 'select-iterate', 1.5 x for 'insert' +- 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. + The performance improvements are significant, e.g 'transaction small' is now about 26x faster + +## Why Async Drivers are expected to be slower + +1. **Event Loop Integration**: Async drivers must integrate with Node.js's event loop, requiring context switches and queue management. + +2. **Thread Pool Usage**: Async SQLite operations are using libuv's thread pool, introducing thread scheduling overhead. + +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. + +## Supported Drivers + +| Driver | Type | Description | +|--------|------|-------------| +| `better-sqlite3` | Synchronous | High-performance synchronous SQLite bindings | +| `@homeofthings/sqlite3` | Asynchronous | Promise-based SQLite bindings (fork of node-sqlite3) | +| `node:sqlite` | Synchronous | Built-in Node.js SQLite (Node.js v22.6.0+) | + +## Requirements + +- **Node.js**: v20.17.0 or later (for N-API compatibility) +- **For `node:sqlite`**: Node.js v22.6.0+ (experimental) or v22.12.0+ (stable) + +## Installation + +```bash +npm install +``` + +## Usage + +### Run Default Benchmarks + +```bash +node index.js +``` + +This runs a general-purpose benchmark suite + +### Run Specific Benchmarks + +```bash +node index.js +``` + +Examples: +```bash +# Run only select benchmarks +node index.js select + +# Run benchmarks for specific tables +node index.js small + +# Run benchmarks for specific columns +node index.js integer text + +# Combine search terms +node index.js select small integer +``` + +### Using Local Development Version + +To benchmark the local development version of `@homeofthings/sqlite3` instead of the npm package: + +```bash +node index.js --use-local +``` + +This is useful for testing performance changes before publishing. Requires the native addon to be built: + +```bash +# From project root +npm run build +``` + +The `--use-local` flag can be combined with search terms: + +```bash +node index.js --use-local insert small +``` + +## Benchmark Types + +| Type | Description | +|------|-------------| +| `select` | Reading single rows by primary key | +| `select-all` | Reading 100 rows into an array | +| `select-iterate` | Iterating over 100 rows | +| `insert` | Inserting single rows | +| `update` | Updating single rows | +| `transaction` | Inserting 100 rows in a single transaction | +| `update-transaction` | Updating 100 rows in a single transaction | + +## Output Format + +Results are displayed as: +``` +driver-name x 471,255 ops/sec ±0.07% (event loop: 50%, 2.1μs/op) +``` + +- `x` - Separator (from original benchmark format) +- `ops/sec` - Operations per second (higher is better) +- `±X.XX%` - Relative margin of error +- `event loop: X%, Yμs/op` - Utilization percentage and blocking time per operation (lower is better) + +### Example Output + +Running `node index.js select` produces output like: + +``` +select small (nul) +better-sqlite3 x 638,075 ops/sec ±0.44% (event loop: 100%, 1.6μs/op) +@homeofthings/sqlite3 x 88,459 ops/sec ±0.82% (event loop: 47%, 5.3μs/op) +node:sqlite x 543,445 ops/sec ±0.53% (event loop: 100%, 1.8μs/op) +``` + +### Event Loop Metrics + +The **event loop** metrics show how the driver affects the event loop (measured using Node.js native `performance.eventLoopUtilization()` API): + +**Utilization Percentage:** How much of the benchmark time the event loop was busy (100% = completely blocked, 0% = completely free) + +**Time per Operation:** `(1,000,000 μs/sec ÷ ops/sec) × utilization = μs blocked per operation` + +| Driver | Utilization | Time per Op | Meaning | +|-------------------------|-------------|-------------|-------------------------------------------------| +| `better-sqlite3` | 100% | ~1.6μs/op | Blocks completely - all time is event loop time | +| `@homeofthings/sqlite3` | ~47% | ~5.3μs/op | 3.3x more blocking than sync drivers | +| `node:sqlite` | 100% | ~1.8μs/op | Blocks completely - all time is event loop time | + +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**: + +``` +--- inserting rows individually --- +better-sqlite3 x 139,898 ops/sec ±21.94% (event loop: 100%, 7.1μs/op) +@homeofthings/sqlite3 x 47,619 ops/sec ±18.89% (event loop: 22%, 4.6μs/op) +node:sqlite x 128,465 ops/sec ±22.25% (event loop: 100%, 7.8μs/op) +``` + +### Large Data Performance + +For I/O-bound operations (large data reads), async drivers can actually **outperform** sync drivers: + +``` +--- reading large blobs (16MB each) --- +better-sqlite3 x 83 ops/sec ±7.99% (event loop: 100%, 12.07ms/op) +@homeofthings/sqlite3 x 94 ops/sec ±8.57% (event loop: 34%, 3.63ms/op) +node:sqlite x 127 ops/sec ±10.75% (event loop: 100%, 7.88ms/op) +``` + +**Why async wins for large data:** + +1. **Lower event loop blocking**: 3.63ms vs 12.07ms - async driver blocks 70% less +2. **Higher throughput**: 94 vs 83 ops/sec - async driver is 13% faster +3. **Event loop availability**: 66% free during async operations + +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. + +## Project Structure + +``` +├── index.js # Main orchestrator +├── benchmark.js # Benchmark runner (tinybench) +├── drivers.js # SQLite driver configurations +├── trials.js # Benchmark trial definitions +├── seed.js # Database seeding +├── types/ +│ ├── insert.js # Insert benchmark +│ ├── select.js # Single row select benchmark +│ ├── select-all.js # Multi-row select benchmark +│ ├── select-iterate.js # Iteration benchmark +│ └── transaction.js # Transaction benchmark +└── temp/ # Temporary database files (auto-created) +``` + +## Architecture + +Each benchmark runs in an isolated child process to ensure: +- Clean state for each measurement +- Memory isolation between runs +- No interference between drivers + +## Adding a New Driver + +1. Add the driver to `package.json` dependencies +2. Add a connection function to `drivers.js`: +```javascript +['driver-name', async (filename, pragma) => { + const db = require('driver-package')(filename); + // Apply PRAGMA settings + for (const str of pragma) await db.exec(`PRAGMA ${str}`); + return db; +}] +``` +3. Add benchmark implementations in each `types/*.js` file: +```javascript +// Either return sync or async function + +exports['driver-name'] = (db, ctx) => { + return () => db.someOperation(); +}; + +exports['driver-name'] = async (db, ctx) => { + return () => db.someOperation(); +}; +``` diff --git a/tools/benchmark-drivers/benchmark.js b/tools/benchmark-drivers/benchmark.js new file mode 100755 index 00000000..d112473a --- /dev/null +++ b/tools/benchmark-drivers/benchmark.js @@ -0,0 +1,75 @@ +#!/usr/bin/env node +'use strict'; +const { Bench } = require('tinybench'); +const { performance } = require('perf_hooks'); + +const formatResult = (task, eventLoopUtilization) => { + // Format: 471,255 ops/sec ±0.07% (event loop: 50%, 2.1μs/op) + const hz = task.result?.hz || 0; + const rme = task.result?.rme || 0; + const ops = Math.round(hz).toLocaleString('en-US'); + // Calculate event loop time per operation in microseconds + // Formula: (1,000,000 μs/sec / ops/sec) × utilization = μs blocked per operation + const timePerOpUs = hz > 0 ? (1000000 / hz) * eventLoopUtilization : 0; + // Format: use μs for values < 1000, otherwise ms + let eluTimeStr; + if (timePerOpUs < 1000) { + eluTimeStr = `${timePerOpUs.toFixed(1)}μs/op`; + } else { + eluTimeStr = `${(timePerOpUs / 1000).toFixed(2)}ms/op`; + } + // Format utilization percentage + const eluPct = (eventLoopUtilization * 100).toFixed(0); + return `${ops} ops/sec ±${rme.toFixed(2)}% (event loop: ${eluPct}%, ${eluTimeStr})`; +}; + +const runWithEventLoopMeasurement = async (fn, isAsync) => { + const bench = new Bench({ time: 1000, warmupTime: 0 }); + + // Warmup run + const warmupBench = new Bench({ time: 100, warmupTime: 0 }); + warmupBench.add('warmup', isAsync ? async () => { await fn(); } : fn); + await warmupBench.run(); + + // Actual benchmark with event loop measurement using native API + const eluStart = performance.eventLoopUtilization(); + + bench.add('test', isAsync ? async () => { await fn(); } : fn); + await bench.run(); + + const eluEnd = performance.eventLoopUtilization(); + const eventLoopUtilization = performance.eventLoopUtilization(eluStart, eluEnd).utilization; + + return formatResult(bench.tasks[0], eventLoopUtilization); +}; + +const runSync = async (fn) => { + return runWithEventLoopMeasurement(fn, false); +}; + +const runAsync = async (fn) => { + return runWithEventLoopMeasurement(fn, true); +}; + +const display = (result) => { + process.stdout.write(result); + process.exit(); +}; + +(async () => { + process.on('unhandledRejection', (err) => { throw err; }); + const ctx = JSON.parse(process.argv[2]); + const type = require(`./types/${ctx.type}`); + const drivers = require('./drivers')(ctx.useLocal); + const db = await drivers.get(ctx.driver)('../temp/benchmark.db', ctx.pragma); + if (!type.readonly) { + for (const table of ctx.tables) await db.exec(`DELETE FROM ${table} WHERE rowid > 1;`); + await db.exec('VACUUM;'); + } + const fn = type[ctx.driver](db, ctx); + if (typeof fn === 'function') { + setImmediate(async () => { display(await runSync(fn)); }); + } else { + setImmediate(async () => { display(await runAsync(await fn)); }); + } +})(); diff --git a/tools/benchmark-drivers/drivers.js b/tools/benchmark-drivers/drivers.js new file mode 100644 index 00000000..23450f79 --- /dev/null +++ b/tools/benchmark-drivers/drivers.js @@ -0,0 +1,43 @@ +'use strict'; + +/* + Every benchmark trial will be executed once for each SQLite driver listed + below. Each driver has a function to open a new database connection on a + given filename and a list of PRAGMA statements. + + When useLocal is true, @homeofthings/sqlite3 is loaded from the local + development path (../../lib/sqlite3) instead of the npm package. + */ + +module.exports = (useLocal = false) => new Map([ + ['better-sqlite3', async (filename, pragma) => { + const db = require('better-sqlite3')(filename); + for (const str of pragma) db.pragma(str); + return db; + }], + ['@homeofthings/sqlite3', async (filename, pragma) => { + // Use local development path when --use-local flag is set + const modulePath = useLocal + ? require.resolve('../../lib/sqlite3') + : '@homeofthings/sqlite3'; + const { SqliteDatabase } = require(modulePath); + const db = await SqliteDatabase.open(filename); + for (const str of pragma) await db.run(`PRAGMA ${str}`); + return db; + }], + ...!moduleExists('node:sqlite') ? [] : [ + ['node:sqlite', async (filename, pragma) => { + const db = new (require('node:sqlite').DatabaseSync)(filename); + for (const str of pragma) db.exec(`PRAGMA ${str}`); + return db; + }] + ], +]); + +function moduleExists(moduleName) { + try { + return !!(require.resolve(moduleName)); + } catch (_) { + return false; + } +}; diff --git a/tools/benchmark-drivers/index.js b/tools/benchmark-drivers/index.js new file mode 100755 index 00000000..d54c9290 --- /dev/null +++ b/tools/benchmark-drivers/index.js @@ -0,0 +1,88 @@ +'use strict'; +const { execFileSync } = require('child_process'); +const clc = require('cli-color'); + +const getTrials = (searchTerms) => { + // Without any command-line arguments, we do a general-purpose benchmark. + if (!searchTerms.length) return require('./trials').default; + + // With command-line arguments, the user can run specific groups of trials. + return require('./trials').searchable.filter(filterBySearchTerms(searchTerms)); +}; + +const filterBySearchTerms = (searchTerms) => (trial) => { + const terms = [ + trial.type, + trial.table, + `(${trial.columns.join(', ')})`, + `(${trial.columns.join(',')})`, + ...trial.columns, + ...trial.customPragma, + ]; + return searchTerms.every(arg => terms.includes(arg)); +}; + +const sortTrials = (a, b) => { + const aRo = require(`./types/${a.type}`).readonly; + const bRo = require(`./types/${b.type}`).readonly; + if (typeof aRo !== 'boolean') throw new TypeError(`Missing readonly export in benchmark type ${a.type}`); + if (typeof bRo !== 'boolean') throw new TypeError(`Missing readonly export in benchmark type ${b.type}`); + return bRo - aRo; +}; + +const displayTrialName = (trial) => { + if (trial.description) return console.log(clc.magenta(`--- ${trial.description} ---`)); + const name = `${trial.type} ${trial.table} (${trial.columns.join(', ')})`; + const pragma = trial.customPragma.length ? ` | ${trial.customPragma.join('; ')}` : ''; + console.log(clc.magenta(name) + clc.yellow(pragma)); +}; + +const createContext = (trial, driver, useLocal) => { + const tableInfo = Object.assign({}, tables.get(trial.table), { data: undefined }); + return JSON.stringify(Object.assign({}, trial, tableInfo, { driver, tables: [...tables.keys()], useLocal })); +}; + +const erase = () => { + return clc.move(0, -1) + clc.erase.line; +}; + +// Parse command-line arguments +const args = process.argv.slice(2); +const useLocal = args.includes('--use-local'); +const searchTerms = args.filter(arg => !arg.startsWith('--')); + +// Determine which trials should be executed. +process.chdir(__dirname); +const trials = getTrials(searchTerms).sort(sortTrials); +if (!trials.length) { + console.log(clc.yellow('No matching benchmarks found!')); + process.exit(); +} + +// Create the temporary database needed to run the benchmark trials. +console.log('Generating tables...'); +const tables = require('./seed')(); +process.stdout.write(erase()); + +// Execute each trial for each available driver. +const drivers = require('./drivers')(useLocal); +const nameLength = [...drivers.keys()].reduce((m, d) => Math.max(m, d.length), 0); +for (const trial of trials) { + displayTrialName(trial); + for (const driver of drivers.keys()) { + const driverName = driver.padEnd(nameLength); + const ctx = createContext(trial, driver, useLocal); + process.stdout.write(`${driver} (running...)\n`); + try { + const result = execFileSync('node', [...process.execArgv, './benchmark.js', ctx], { stdio: 'pipe', encoding: 'utf8' }); + console.log(erase() + `${driverName} x ${result}`); + } catch (err) { + console.log(erase() + clc.red(`${driverName} ERROR (probably out of memory)`)); + process.stderr.write(clc.xterm(247)(clc.strip(err.stderr))); + } + } + console.log(''); +} + +console.log(clc.green('All benchmarks complete!')); +process.exit(); diff --git a/tools/benchmark-drivers/package.json b/tools/benchmark-drivers/package.json new file mode 100644 index 00000000..687c59e2 --- /dev/null +++ b/tools/benchmark-drivers/package.json @@ -0,0 +1,18 @@ +{ + "name": "benchmark", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@homeofthings/sqlite3": "^6.3.1", + "better-sqlite3": "^12.8.0", + "cli-color": "^2.0.4", + "fs-extra": "^11.3.4", + "tinybench": "^4.0.1" + } +} diff --git a/tools/benchmark-drivers/seed.js b/tools/benchmark-drivers/seed.js new file mode 100644 index 00000000..17f07ac6 --- /dev/null +++ b/tools/benchmark-drivers/seed.js @@ -0,0 +1,47 @@ +'use strict'; +const fs = require('fs-extra'); +const path = require('path'); + +const tables = new Map([ + ['small', { + schema: '(nul, integer INTEGER, real REAL, text TEXT, blob BLOB)', + data: [null, 0x7fffffff, 1 / 3, 'this is the text', Buffer.from('this is the blob')], + count: 10000, + }], + ['large_text', { + schema: '(text TEXT)', + data: ['this is the text'.repeat(65536)], + count: 1000, + }], + ['large_blob', { + schema: '(blob BLOB)', + data: [Buffer.from('this is the blob'.repeat(1048576))], + count: 1000, + }], +]); + +/* + This function creates a pre-populated database that is deleted when the + process exits. + */ + +module.exports = () => { + const tempDir = path.join(__dirname, '..', 'temp'); + process.on('exit', () => fs.removeSync(tempDir)); + fs.removeSync(tempDir); + fs.ensureDirSync(tempDir); + + const db = require('better-sqlite3')(path.join(tempDir, 'benchmark.db')); + db.pragma('journal_mode = OFF'); + db.pragma('synchronous = OFF'); + + for (const [name, ctx] of tables.entries()) { + db.exec(`CREATE TABLE ${name} ${ctx.schema}`); + const columns = db.pragma(`table_info(${name})`).map(() => '?'); + const insert = db.prepare(`INSERT INTO ${name} VALUES (${columns.join(', ')})`).bind(ctx.data); + for (let i = 0; i < ctx.count; ++i) insert.run(); + } + + db.close(); + return tables; +}; diff --git a/tools/benchmark-drivers/trials.js b/tools/benchmark-drivers/trials.js new file mode 100644 index 00000000..63497d24 --- /dev/null +++ b/tools/benchmark-drivers/trials.js @@ -0,0 +1,98 @@ +'use strict'; + +exports.default = [ + { type: 'select', table: 'small', columns: ['nul', 'integer', 'real', 'text'], + description: 'reading rows individually' }, + { + type: 'select', table: 'large_blob', columns: ['blob'], + description: 'reading large blobs' + }, + { type: 'select-all', table: 'small', columns: ['nul', 'integer', 'real', 'text'], + description: 'reading 100 rows into an array' + }, + { type: 'insert', table: 'small', columns: ['nul', 'integer', 'real', 'text'], + description: 'inserting rows individually' }, + { + type: 'insert', table: 'large_blob', columns: ['blob'], + description: 'inserting large blobs' + }, + { + type: 'update', table: 'small', columns: ['nul', 'integer', 'real', 'text'], + description: 'updating rows individually' + }, + { + type: 'update', table: 'large_blob', columns: ['blob'], + description: 'updating large blobs' + }, + { type: 'transaction', table: 'small', columns: ['nul', 'integer', 'real', 'text'], + description: 'inserting 100 rows in a single transaction' }, + { + type: 'update-transaction', table: 'small', columns: ['nul', 'integer', 'real', 'text'], + description: 'updating 100 rows in a single transaction' + }, +]; + +exports.searchable = [ + { type: 'select', table: 'small', columns: ['nul'] }, + { type: 'select', table: 'small', columns: ['integer'] }, + { type: 'select', table: 'small', columns: ['real'] }, + { type: 'select', table: 'small', columns: ['text'] }, + { type: 'select', table: 'small', columns: ['blob'] }, + { type: 'select', table: 'large_text', columns: ['text'] }, + { type: 'select', table: 'large_blob', columns: ['blob'] }, + { type: 'select-all', table: 'small', columns: ['nul'] }, + { type: 'select-all', table: 'small', columns: ['integer'] }, + { type: 'select-all', table: 'small', columns: ['real'] }, + { type: 'select-all', table: 'small', columns: ['text'] }, + { type: 'select-all', table: 'small', columns: ['blob'] }, + { type: 'select-all', table: 'large_text', columns: ['text'] }, + { type: 'select-all', table: 'large_blob', columns: ['blob'] }, + { type: 'select-iterate', table: 'small', columns: ['nul'] }, + { type: 'select-iterate', table: 'small', columns: ['integer'] }, + { type: 'select-iterate', table: 'small', columns: ['real'] }, + { type: 'select-iterate', table: 'small', columns: ['text'] }, + { type: 'select-iterate', table: 'small', columns: ['blob'] }, + { type: 'select-iterate', table: 'large_text', columns: ['text'] }, + { type: 'select-iterate', table: 'large_blob', columns: ['blob'] }, + { type: 'insert', table: 'small', columns: ['nul'] }, + { type: 'insert', table: 'small', columns: ['integer'] }, + { type: 'insert', table: 'small', columns: ['real'] }, + { type: 'insert', table: 'small', columns: ['text'] }, + { type: 'insert', table: 'small', columns: ['blob'] }, + { type: 'insert', table: 'large_text', columns: ['text'] }, + { type: 'insert', table: 'large_blob', columns: ['blob'] }, + { type: 'transaction', table: 'small', columns: ['nul'] }, + { type: 'transaction', table: 'small', columns: ['integer'] }, + { type: 'transaction', table: 'small', columns: ['real'] }, + { type: 'transaction', table: 'small', columns: ['text'] }, + { type: 'transaction', table: 'small', columns: ['blob'] }, + { type: 'transaction', table: 'large_text', columns: ['text'] }, + { type: 'transaction', table: 'large_blob', columns: ['blob'] }, + { type: 'update', table: 'small', columns: ['nul'] }, + { type: 'update', table: 'small', columns: ['integer'] }, + { type: 'update', table: 'small', columns: ['real'] }, + { type: 'update', table: 'small', columns: ['text'] }, + { type: 'update', table: 'small', columns: ['blob'] }, + { type: 'update', table: 'large_text', columns: ['text'] }, + { type: 'update', table: 'large_blob', columns: ['blob'] }, + { type: 'update-transaction', table: 'small', columns: ['nul'] }, + { type: 'update-transaction', table: 'small', columns: ['integer'] }, + { type: 'update-transaction', table: 'small', columns: ['real'] }, + { type: 'update-transaction', table: 'small', columns: ['text'] }, + { type: 'update-transaction', table: 'small', columns: ['blob'] }, + { type: 'update-transaction', table: 'large_text', columns: ['text'] }, + { type: 'update-transaction', table: 'large_blob', columns: ['blob'] }, +]; + +(() => { + const defaultPragma = []; + const yes = /^\s*(1|true|on|yes)\s*$/i; + if (yes.test(process.env.NO_CACHE)) defaultPragma.push('cache_size = 0'); + else defaultPragma.push('cache_size = -16000'); + if (yes.test(process.env.NO_WAL)) defaultPragma.push('journal_mode = DELETE', 'synchronous = FULL'); + else defaultPragma.push('journal_mode = WAL', 'synchronous = NORMAL'); + for (const trial of [].concat(...Object.values(exports))) { + trial.customPragma = trial.pragma || []; + trial.pragma = defaultPragma.concat(trial.customPragma); + } +})(); diff --git a/tools/benchmark-drivers/types/insert.js b/tools/benchmark-drivers/types/insert.js new file mode 100644 index 00000000..d3191e4d --- /dev/null +++ b/tools/benchmark-drivers/types/insert.js @@ -0,0 +1,22 @@ +'use strict'; +exports.readonly = false; // Inserting rows individually (`.run()`) + +exports['better-sqlite3'] = (db, { table, columns }) => { + const stmt = db.prepare(`INSERT INTO ${table} (${columns.join(', ')}) VALUES (${columns.map(x => '@' + x).join(', ')})`); + const row = db.prepare(`SELECT ${columns.join(', ')} FROM ${table} LIMIT 1`).get(); + return () => stmt.run(row); +}; + +exports['@homeofthings/sqlite3'] = async (db, { table, columns }) => { + const stmt = await db.prepare(`INSERT INTO ${table} (${columns.join(', ')}) VALUES (${columns.map(x => '@' + x).join(', ')})`); + const row = Object.assign({}, ...Object.entries(await db.get(`SELECT ${columns.join(', ')} FROM ${table} LIMIT 1`)) + .filter(([k]) => columns.includes(k)) + .map(([k, v]) => ({ ['@' + k]: v }))); + return async () => await stmt.run(row); +}; + +exports['node:sqlite'] = (db, { table, columns }) => { + const stmt = db.prepare(`INSERT INTO ${table} (${columns.join(', ')}) VALUES (${columns.map(x => '@' + x).join(', ')})`); + const row = db.prepare(`SELECT ${columns.join(', ')} FROM ${table} LIMIT 1`).get(); + return () => stmt.run(row); +}; diff --git a/tools/benchmark-drivers/types/select-all.js b/tools/benchmark-drivers/types/select-all.js new file mode 100644 index 00000000..6242ae9d --- /dev/null +++ b/tools/benchmark-drivers/types/select-all.js @@ -0,0 +1,20 @@ +'use strict'; +exports.readonly = true; // Reading 100 rows into an array (`.all()`) + +exports['better-sqlite3'] = (db, { table, columns, count }) => { + const stmt = db.prepare(`SELECT ${columns.join(', ')} FROM ${table} WHERE rowid >= ? LIMIT 100`); + let rowid = -100; + return () => stmt.all((rowid += 100) % count + 1); +}; + +exports['@homeofthings/sqlite3'] = async (db, { table, columns, count }) => { + const stmt = await db.prepare(`SELECT ${columns.join(', ')} FROM ${table} WHERE rowid >= ? LIMIT 100`); + let rowid = -100; + return async () => await stmt.all((rowid += 100) % count + 1); +}; + +exports['node:sqlite'] = (db, { table, columns, count }) => { + const stmt = db.prepare(`SELECT ${columns.join(', ')} FROM ${table} WHERE rowid >= ? LIMIT 100`); + let rowid = -100; + return () => stmt.all((rowid += 100) % count + 1); +}; diff --git a/tools/benchmark-drivers/types/select-iterate.js b/tools/benchmark-drivers/types/select-iterate.js new file mode 100644 index 00000000..dd49fbb4 --- /dev/null +++ b/tools/benchmark-drivers/types/select-iterate.js @@ -0,0 +1,33 @@ +'use strict'; +exports.readonly = true; // Iterating over 100 rows (`.iterate()`) + +exports['better-sqlite3'] = (db, { table, columns, count }) => { + const stmt = db.prepare(`SELECT ${columns.join(', ')} FROM ${table} WHERE rowid >= ? LIMIT 100`); + let rowid = -100; + return () => { + for (const row of stmt.iterate((rowid += 100) % count + 1)) { + // iterate over rows - row intentionally unused + } + }; +}; + +exports['@homeofthings/sqlite3'] = async (db, { table, columns, count }) => { + const stmt = await db.prepare(`SELECT ${columns.join(', ')} FROM ${table} WHERE rowid = ?`); + let rowid = -100; + return async () => { + rowid += 100; + for (let index = 0; index < 100; index++) { + await stmt.get((rowid + index) % count + 1); + } + }; +}; + +exports['node:sqlite'] = (db, { table, columns, count }) => { + const stmt = db.prepare(`SELECT ${columns.join(', ')} FROM ${table} WHERE rowid >= ? LIMIT 100`); + let rowid = -100; + return () => { + for (const row of stmt.iterate((rowid += 100) % count + 1)) { + // iterate over rows - row intentionally unused + } + }; +}; diff --git a/tools/benchmark-drivers/types/select.js b/tools/benchmark-drivers/types/select.js new file mode 100644 index 00000000..1b433f57 --- /dev/null +++ b/tools/benchmark-drivers/types/select.js @@ -0,0 +1,20 @@ +'use strict'; +exports.readonly = true; // Reading rows individually (`.get()`) + +exports['better-sqlite3'] = (db, { table, columns, count }) => { + const stmt = db.prepare(`SELECT ${columns.join(', ')} FROM ${table} WHERE rowid = ?`); + let rowid = -1; + return () => stmt.get(++rowid % count + 1); +}; + +exports['@homeofthings/sqlite3'] = async (db, { table, columns, count }) => { + const stmt = await db.prepare(`SELECT ${columns.join(', ')} FROM ${table} WHERE rowid = ?`); + let rowid = -1; + return async () => await stmt.get(++rowid % count + 1); +}; + +exports['node:sqlite'] = (db, { table, columns, count }) => { + const stmt = db.prepare(`SELECT ${columns.join(', ')} FROM ${table} WHERE rowid = ?`); + let rowid = -1; + return () => stmt.get(++rowid % count + 1); +}; diff --git a/tools/benchmark-drivers/types/transaction.js b/tools/benchmark-drivers/types/transaction.js new file mode 100644 index 00000000..2d30a4c2 --- /dev/null +++ b/tools/benchmark-drivers/types/transaction.js @@ -0,0 +1,43 @@ +'use strict'; +exports.readonly = false; // Inserting 100 rows in a single transaction + +exports['better-sqlite3'] = (db, { table, columns }) => { + const stmt = db.prepare(`INSERT INTO ${table} (${columns.join(', ')}) VALUES (${columns.map(x => '@' + x).join(', ')})`); + const row = db.prepare(`SELECT ${columns.join(', ')} FROM ${table} LIMIT 1`).get(); + const trx = db.transaction((row) => { + for (let i = 0; i < 100; ++i) stmt.run(row); + }); + return () => trx(row); +}; + +exports['@homeofthings/sqlite3'] = async (db, { table, columns }) => { + const stmt = await db.prepare(`INSERT INTO ${table} (${columns.join(', ')}) VALUES (${columns.map(x => '@' + x).join(', ')})`); + const row = Object.assign({}, ...Object.entries(await db.get(`SELECT ${columns.join(', ')} FROM ${table} LIMIT 1`)) + .filter(([k]) => columns.includes(k)) + .map(([k, v]) => ({ ['@' + k]: v }))); + return async () => { + await db.run('BEGIN'); + try { + for (let i = 0; i < 100; ++i) await stmt.run(row); + await db.run('COMMIT'); + } catch (err) { + await db.run('ROLLBACK'); + throw err; + } + }; +}; + +exports['node:sqlite'] = (db, { table, columns }) => { + const stmt = db.prepare(`INSERT INTO ${table} (${columns.join(', ')}) VALUES (${columns.map(x => '@' + x).join(', ')})`); + const row = db.prepare(`SELECT ${columns.join(', ')} FROM ${table} LIMIT 1`).get(); + return () => { + db.exec('BEGIN'); + try { + for (let i = 0; i < 100; ++i) stmt.run(row); + db.exec('COMMIT'); + } catch (err) { + db.isTransaction && db.exec('ROLLBACK'); + throw err; + } + }; +}; diff --git a/tools/benchmark-drivers/types/update-transaction.js b/tools/benchmark-drivers/types/update-transaction.js new file mode 100644 index 00000000..37b6e0c6 --- /dev/null +++ b/tools/benchmark-drivers/types/update-transaction.js @@ -0,0 +1,43 @@ +'use strict'; +exports.readonly = false; // Updating 100 rows in a single transaction + +exports['better-sqlite3'] = (db, { table, columns, count }) => { + const stmt = db.prepare(`UPDATE ${table} SET ${columns.map(c => `${c} = @${c}`).join(', ')} WHERE rowid = @rowid`); + const row = db.prepare(`SELECT ${columns.join(', ')} FROM ${table} LIMIT 1`).get(); + const trx = db.transaction((row) => { + for (let i = 0; i < 100; ++i) stmt.run({ ...row, rowid: (i % count) + 1 }); + }); + return () => trx(row); +}; + +exports['@homeofthings/sqlite3'] = async (db, { table, columns, count }) => { + const stmt = await db.prepare(`UPDATE ${table} SET ${columns.map(c => `${c} = @${c}`).join(', ')} WHERE rowid = @rowid`); + const row = Object.assign({}, ...Object.entries(await db.get(`SELECT ${columns.join(', ')} FROM ${table} LIMIT 1`)) + .filter(([k]) => columns.includes(k)) + .map(([k, v]) => ({ ['@' + k]: v }))); + return async () => { + await db.run('BEGIN'); + try { + for (let i = 0; i < 100; ++i) await stmt.run({ ...row, '@rowid': (i % count) + 1 }); + await db.run('COMMIT'); + } catch (err) { + await db.run('ROLLBACK'); + throw err; + } + }; +}; + +exports['node:sqlite'] = (db, { table, columns, count }) => { + const stmt = db.prepare(`UPDATE ${table} SET ${columns.map(c => `${c} = @${c}`).join(', ')} WHERE rowid = @rowid`); + const row = db.prepare(`SELECT ${columns.join(', ')} FROM ${table} LIMIT 1`).get(); + return () => { + db.exec('BEGIN'); + try { + for (let i = 0; i < 100; ++i) stmt.run({ ...row, rowid: (i % count) + 1 }); + db.exec('COMMIT'); + } catch (err) { + db.isTransaction && db.exec('ROLLBACK'); + throw err; + } + }; +}; diff --git a/tools/benchmark-drivers/types/update.js b/tools/benchmark-drivers/types/update.js new file mode 100644 index 00000000..02b5169a --- /dev/null +++ b/tools/benchmark-drivers/types/update.js @@ -0,0 +1,25 @@ +'use strict'; +exports.readonly = false; // Updating rows individually (`.run()`) + +exports['better-sqlite3'] = (db, { table, columns, count }) => { + const stmt = db.prepare(`UPDATE ${table} SET ${columns.map(c => `${c} = @${c}`).join(', ')} WHERE rowid = @rowid`); + const row = db.prepare(`SELECT ${columns.join(', ')} FROM ${table} LIMIT 1`).get(); + let rowid = -1; + return () => stmt.run({ ...row, rowid: ++rowid % count + 1 }); +}; + +exports['@homeofthings/sqlite3'] = async (db, { table, columns, count }) => { + const stmt = await db.prepare(`UPDATE ${table} SET ${columns.map(c => `${c} = @${c}`).join(', ')} WHERE rowid = @rowid`); + const row = Object.assign({}, ...Object.entries(await db.get(`SELECT ${columns.join(', ')} FROM ${table} LIMIT 1`)) + .filter(([k]) => columns.includes(k)) + .map(([k, v]) => ({ ['@' + k]: v }))); + let rowid = -1; + return async () => await stmt.run({ ...row, '@rowid': ++rowid % count + 1 }); +}; + +exports['node:sqlite'] = (db, { table, columns, count }) => { + const stmt = db.prepare(`UPDATE ${table} SET ${columns.map(c => `${c} = @${c}`).join(', ')} WHERE rowid = @rowid`); + const row = db.prepare(`SELECT ${columns.join(', ')} FROM ${table} LIMIT 1`).get(); + let rowid = -1; + return () => stmt.run({ ...row, rowid: ++rowid % count + 1 }); +}; diff --git a/tools/benchmark-drivers/yarn.lock b/tools/benchmark-drivers/yarn.lock new file mode 100644 index 00000000..b9ec4e23 --- /dev/null +++ b/tools/benchmark-drivers/yarn.lock @@ -0,0 +1,800 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@gar/promise-retry@^1.0.0": + version "1.0.3" + resolved "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz" + integrity sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA== + +"@homeofthings/sqlite3@^6.3.1": + version "6.3.1" + resolved "https://registry.npmjs.org/@homeofthings/sqlite3/-/sqlite3-6.3.1.tgz" + integrity sha512-d+RqnFap9bfW64EXfcT7L7wB56DZuTL6z6DtBGVAuZc+Nr7LtH0R/qcjimsFoshEuMbtpHhlV/cx8wOBSEGtNw== + dependencies: + bindings "^1.5.0" + node-addon-api "^8.7.0" + prebuild-install "^7.1.3" + tar "^7.5.13" + optionalDependencies: + node-gyp "12.x" + +"@isaacs/fs-minipass@^4.0.0": + version "4.0.1" + resolved "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz" + integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w== + dependencies: + minipass "^7.0.4" + +"@npmcli/agent@^4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz" + integrity sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA== + dependencies: + agent-base "^7.1.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.1" + lru-cache "^11.2.1" + socks-proxy-agent "^8.0.3" + +"@npmcli/fs@^5.0.0": + version "5.0.0" + resolved "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz" + integrity sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og== + dependencies: + semver "^7.3.5" + +"@npmcli/redact@^4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz" + integrity sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q== + +abbrev@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz" + integrity sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA== + +agent-base@^7.1.0, agent-base@^7.1.2: + version "7.1.4" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz" + integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== + +balanced-match@^4.0.2: + version "4.0.4" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz" + integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +better-sqlite3@^12.8.0: + version "12.8.0" + resolved "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz" + integrity sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ== + dependencies: + bindings "^1.5.0" + prebuild-install "^7.1.1" + +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +brace-expansion@^5.0.5: + version "5.0.5" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz" + integrity sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ== + dependencies: + balanced-match "^4.0.2" + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +cacache@^20.0.1: + version "20.0.4" + resolved "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz" + integrity sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA== + dependencies: + "@npmcli/fs" "^5.0.0" + fs-minipass "^3.0.0" + glob "^13.0.0" + lru-cache "^11.1.0" + minipass "^7.0.3" + minipass-collect "^2.0.1" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + p-map "^7.0.2" + ssri "^13.0.0" + +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +chownr@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz" + integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== + +cli-color@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz" + integrity sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA== + dependencies: + d "^1.0.1" + es5-ext "^0.10.64" + es6-iterator "^2.0.3" + memoizee "^0.4.15" + timers-ext "^0.1.7" + +d@^1.0.1, d@^1.0.2, d@1: + version "1.0.2" + resolved "https://registry.npmjs.org/d/-/d-1.0.2.tgz" + integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== + dependencies: + es5-ext "^0.10.64" + type "^2.7.2" + +debug@^4.3.4, debug@4: + version "4.4.3" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +detect-libc@^2.0.0: + version "2.1.2" + resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.5" + resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz" + integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== + dependencies: + once "^1.4.0" + +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14, es5-ext@~0.10.2: + version "0.10.64" + resolved "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz" + integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + esniff "^2.0.1" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.4" + resolved "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz" + integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg== + dependencies: + d "^1.0.2" + ext "^1.7.0" + +es6-weak-map@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz" + integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== + dependencies: + d "1" + es5-ext "^0.10.46" + es6-iterator "^2.0.3" + es6-symbol "^3.1.1" + +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== + dependencies: + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" + +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz" + integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA== + dependencies: + d "1" + es5-ext "~0.10.14" + +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + +exponential-backoff@^3.1.1: + version "3.1.3" + resolved "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz" + integrity sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA== + +ext@^1.7.0: + version "1.7.0" + resolved "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + +fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs-extra@^11.3.4: + version "11.3.4" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz" + integrity sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-minipass@^3.0.0: + version "3.0.3" + resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz" + integrity sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw== + dependencies: + minipass "^7.0.3" + +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz" + integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== + +glob@^13.0.0: + version "13.0.6" + resolved "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz" + integrity sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw== + dependencies: + minimatch "^10.2.2" + minipass "^7.1.3" + path-scurry "^2.0.2" + +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.6: + version "4.2.11" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +http-cache-semantics@^4.1.1: + version "4.2.0" + resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz" + integrity sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ== + +http-proxy-agent@^7.0.0: + version "7.0.2" + resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + +https-proxy-agent@^7.0.1: + version "7.0.6" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + dependencies: + agent-base "^7.1.2" + debug "4" + +iconv-lite@^0.7.2: + version "0.7.2" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz" + integrity sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +inherits@^2.0.3, inherits@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +ip-address@^10.0.1: + version "10.1.0" + resolved "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz" + integrity sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q== + +is-promise@^2.2.2: + version "2.2.2" + resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz" + integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== + +isexe@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz" + integrity sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw== + +jsonfile@^6.0.1: + version "6.2.0" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz" + integrity sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +lru-cache@^11.0.0, lru-cache@^11.1.0, lru-cache@^11.2.1: + version "11.3.3" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz" + integrity sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ== + +lru-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz" + integrity sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ== + dependencies: + es5-ext "~0.10.2" + +make-fetch-happen@^15.0.0: + version "15.0.5" + resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.5.tgz" + integrity sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg== + dependencies: + "@gar/promise-retry" "^1.0.0" + "@npmcli/agent" "^4.0.0" + "@npmcli/redact" "^4.0.0" + cacache "^20.0.1" + http-cache-semantics "^4.1.1" + minipass "^7.0.2" + minipass-fetch "^5.0.0" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^1.0.0" + proc-log "^6.0.0" + ssri "^13.0.0" + +memoizee@^0.4.15: + version "0.4.17" + resolved "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz" + integrity sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA== + dependencies: + d "^1.0.2" + es5-ext "^0.10.64" + es6-weak-map "^2.0.3" + event-emitter "^0.3.5" + is-promise "^2.2.2" + lru-queue "^0.1.0" + next-tick "^1.1.0" + timers-ext "^0.1.7" + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + +minimatch@^10.2.2: + version "10.2.5" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz" + integrity sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg== + dependencies: + brace-expansion "^5.0.5" + +minimist@^1.2.0, minimist@^1.2.3: + version "1.2.8" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass-collect@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz" + integrity sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw== + dependencies: + minipass "^7.0.3" + +minipass-fetch@^5.0.0: + version "5.0.2" + resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz" + integrity sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ== + dependencies: + minipass "^7.0.3" + minipass-sized "^2.0.0" + minizlib "^3.0.1" + optionalDependencies: + iconv-lite "^0.7.2" + +minipass-flush@^1.0.5: + version "1.0.7" + resolved "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz" + integrity sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz" + integrity sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA== + dependencies: + minipass "^7.1.2" + +minipass@^3.0.0: + version "3.3.6" + resolved "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^7.0.2, minipass@^7.0.3, minipass@^7.0.4, minipass@^7.1.2, minipass@^7.1.3: + version "7.1.3" + resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz" + integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A== + +minizlib@^3.0.1, minizlib@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz" + integrity sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw== + dependencies: + minipass "^7.1.2" + +mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: + version "0.5.3" + resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +napi-build-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz" + integrity sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA== + +negotiator@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz" + integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== + +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + +node-abi@^3.3.0: + version "3.89.0" + resolved "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz" + integrity sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA== + dependencies: + semver "^7.3.5" + +node-addon-api@^8.7.0: + version "8.7.0" + resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz" + integrity sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA== + +node-gyp@12.x: + version "12.2.0" + resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz" + integrity sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ== + dependencies: + env-paths "^2.2.0" + exponential-backoff "^3.1.1" + graceful-fs "^4.2.6" + make-fetch-happen "^15.0.0" + nopt "^9.0.0" + proc-log "^6.0.0" + semver "^7.3.5" + tar "^7.5.4" + tinyglobby "^0.2.12" + which "^6.0.0" + +nopt@^9.0.0: + version "9.0.0" + resolved "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz" + integrity sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw== + dependencies: + abbrev "^4.0.0" + +once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +p-map@^7.0.2: + version "7.0.4" + resolved "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz" + integrity sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ== + +path-scurry@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz" + integrity sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg== + dependencies: + lru-cache "^11.0.0" + minipass "^7.1.2" + +picomatch@^4.0.4: + version "4.0.4" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz" + integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== + +prebuild-install@^7.1.1, prebuild-install@^7.1.3: + version "7.1.3" + resolved "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz" + integrity sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^2.0.0" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + +proc-log@^6.0.0: + version "6.1.0" + resolved "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz" + integrity sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ== + +pump@^3.0.0: + version "3.0.4" + resolved "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz" + integrity sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@^3.1.1, readable-stream@^3.4.0: + version "3.6.2" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +safe-buffer@^5.0.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^7.3.5: + version "7.7.4" + resolved "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz" + integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== + +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks-proxy-agent@^8.0.3: + version "8.0.5" + resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz" + integrity sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw== + dependencies: + agent-base "^7.1.2" + debug "^4.3.4" + socks "^2.8.3" + +socks@^2.8.3: + version "2.8.7" + resolved "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz" + integrity sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A== + dependencies: + ip-address "^10.0.1" + smart-buffer "^4.2.0" + +ssri@^13.0.0: + version "13.0.1" + resolved "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz" + integrity sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ== + dependencies: + minipass "^7.0.3" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + +tar-fs@^2.0.0: + version "2.1.4" + resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz" + integrity sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tar@^7.5.13, tar@^7.5.4: + version "7.5.13" + resolved "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz" + integrity sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng== + dependencies: + "@isaacs/fs-minipass" "^4.0.0" + chownr "^3.0.0" + minipass "^7.1.2" + minizlib "^3.1.0" + yallist "^5.0.0" + +timers-ext@^0.1.7: + version "0.1.8" + resolved "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz" + integrity sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww== + dependencies: + es5-ext "^0.10.64" + next-tick "^1.1.0" + +tinybench@^4.0.1: + version "4.1.0" + resolved "https://registry.npmjs.org/tinybench/-/tinybench-4.1.0.tgz" + integrity sha512-8JZoQRJgWWEIIeAmpiNmMHIREmUY3oGX8GRmlmNapLr/qtgMe+K76vM2qabh85hNScnE2lqTVTajVETjuD9Ixg== + +tinyglobby@^0.2.12: + version "0.2.16" + resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz" + integrity sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.4" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +type@^2.7.2: + version "2.7.3" + resolved "https://registry.npmjs.org/type/-/type-2.7.3.tgz" + integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ== + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +which@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/which/-/which-6.0.1.tgz" + integrity sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg== + dependencies: + isexe "^4.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yallist@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz" + integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== From 066784691b360f4375b45ddcc1b923666c88df96 Mon Sep 17 00:00:00 2001 From: Guenter Sandner Date: Sun, 12 Apr 2026 09:32:27 +0200 Subject: [PATCH 2/2] benchmark for simulating long running queries --- tools/benchmark-drivers/README.md | 23 ++++++++++ tools/benchmark-drivers/trials.js | 5 +++ .../types/select-aggregate.js | 44 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 tools/benchmark-drivers/types/select-aggregate.js diff --git a/tools/benchmark-drivers/README.md b/tools/benchmark-drivers/README.md index 1946ecea..97b8ceb0 100644 --- a/tools/benchmark-drivers/README.md +++ b/tools/benchmark-drivers/README.md @@ -98,6 +98,7 @@ node index.js --use-local insert small | `select` | Reading single rows by primary key | | `select-all` | Reading 100 rows into an array | | `select-iterate` | Iterating over 100 rows | +| `select-aggregate` | Aggregate functions (COUNT, SUM, AVG, MIN, MAX) with WHERE clause | | `insert` | Inserting single rows | | `update` | Updating single rows | | `transaction` | Inserting 100 rows in a single transaction | @@ -168,6 +169,27 @@ node:sqlite x 127 ops/sec ±10.75% (event loop: 100%, 7.88ms/op) 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. +### Long Running Query Performance + +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. + +Aggregate functions (COUNT, SUM, AVG, MIN, MAX) with WHERE clauses show even more dramatic async advantages: + +``` +--- aggregate functions (COUNT, SUM, AVG, MIN, MAX) with WHERE clause --- +better-sqlite3 x 11,246 ops/sec ±0.27% (event loop: 100%, 88.9μs/op) +@homeofthings/sqlite3 x 68,779 ops/sec ±0.60% (event loop: 47%, 6.8μs/op) +node:sqlite x 10,982 ops/sec ±0.40% (event loop: 100%, 91.1μs/op) +``` + +**Why async wins for aggregation:** + +1. **6x higher throughput**: 68,779 vs 11,246 ops/sec +2. **13x less event loop blocking**: 6.8μs/op vs 88.9μs/op +3. **Same pattern as large data**: I/O-bound operations benefit from async + +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. + ## Project Structure ``` @@ -180,6 +202,7 @@ For I/O-bound operations, the async driver's overhead becomes negligible compare │ ├── insert.js # Insert benchmark │ ├── select.js # Single row select benchmark │ ├── select-all.js # Multi-row select benchmark +│ ├── select-aggregate.js # Aggregate functions benchmark │ ├── select-iterate.js # Iteration benchmark │ └── transaction.js # Transaction benchmark └── temp/ # Temporary database files (auto-created) diff --git a/tools/benchmark-drivers/trials.js b/tools/benchmark-drivers/trials.js index 63497d24..91e5cd9c 100644 --- a/tools/benchmark-drivers/trials.js +++ b/tools/benchmark-drivers/trials.js @@ -30,6 +30,10 @@ exports.default = [ type: 'update-transaction', table: 'small', columns: ['nul', 'integer', 'real', 'text'], description: 'updating 100 rows in a single transaction' }, + { + type: 'select-aggregate', table: 'small', columns: ['nul', 'integer', 'real', 'text'], + description: 'aggregate functions (COUNT, SUM, AVG, MIN, MAX) with WHERE clause' + }, ]; exports.searchable = [ @@ -82,6 +86,7 @@ exports.searchable = [ { type: 'update-transaction', table: 'small', columns: ['blob'] }, { type: 'update-transaction', table: 'large_text', columns: ['text'] }, { type: 'update-transaction', table: 'large_blob', columns: ['blob'] }, + { type: 'select-aggregate', table: 'small', columns: ['nul', 'integer', 'real', 'text'] }, ]; (() => { diff --git a/tools/benchmark-drivers/types/select-aggregate.js b/tools/benchmark-drivers/types/select-aggregate.js new file mode 100644 index 00000000..7d7bb24d --- /dev/null +++ b/tools/benchmark-drivers/types/select-aggregate.js @@ -0,0 +1,44 @@ +'use strict'; +exports.readonly = true; // Aggregate functions (COUNT, SUM, AVG, MIN, MAX) with WHERE clause + +exports['better-sqlite3'] = (db, { table, columns }) => { + const stmt = db.prepare(` + SELECT COUNT(*), SUM(integer), AVG(real), MIN(text), MAX(text) + FROM ${table} + WHERE rowid >= ? AND rowid < ? + `); + let start = 0; + return () => { + const result = stmt.get(start, start + 1000); + start = (start + 1) % 9000; // Cycle through 0-8999 to stay within 10000 rows + return result; + }; +}; + +exports['@homeofthings/sqlite3'] = async (db, { table, columns }) => { + const stmt = await db.prepare(` + SELECT COUNT(*), SUM(integer), AVG(real), MIN(text), MAX(text) + FROM ${table} + WHERE rowid >= ? AND rowid < ? + `); + let start = 0; + return async () => { + const result = await stmt.get(start, start + 1000); + start = (start + 1) % 9000; // Cycle through 0-8999 to stay within 10000 rows + return result; + }; +}; + +exports['node:sqlite'] = (db, { table, columns }) => { + const stmt = db.prepare(` + SELECT COUNT(*), SUM(integer), AVG(real), MIN(text), MAX(text) + FROM ${table} + WHERE rowid >= ? AND rowid < ? + `); + let start = 0; + return () => { + const result = stmt.get(start, start + 1000); + start = (start + 1) % 9000; // Cycle through 0-8999 to stay within 10000 rows + return result; + }; +};