feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
+265
View File
@@ -0,0 +1,265 @@
#!/usr/bin/env node
// This benchmark intentionally uses direct node:sqlite statements. It validates
// the runtime/host boundary only; production repositories remain governed by
// the typed-schema and adapter decisions in ADR-0004.
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { performance } = require('node:perf_hooks');
const MIB = 1024 * 1024;
const DEFAULT_ITERATIONS = 250;
const DEFAULT_BATCH_SIZE = 10;
function parsePositiveInteger(value, name) {
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed < 1) {
throw new Error(`${name} must be a positive safe integer`);
}
return parsed;
}
function parsePositiveNumber(value, name) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error(`${name} must be a positive number`);
}
return parsed;
}
function parseArguments(argv) {
const options = {
batchSize: DEFAULT_BATCH_SIZE,
iterations: DEFAULT_ITERATIONS,
json: false,
};
for (const argument of argv) {
if (argument === '--') {
continue;
}
if (argument === '--json') {
options.json = true;
continue;
}
const [name, value] = argument.split('=', 2);
if (value === undefined) {
throw new Error(`Unsupported argument: ${argument}`);
}
if (name === '--iterations') {
options.iterations = parsePositiveInteger(value, name);
} else if (name === '--batch-size') {
options.batchSize = parsePositiveInteger(value, name);
} else if (name === '--max-transaction-p95-ms') {
options.maxTransactionP95Ms = parsePositiveNumber(value, name);
} else if (name === '--max-batch-stall-ms') {
options.maxBatchStallMs = parsePositiveNumber(value, name);
} else if (name === '--max-rss-delta-mb') {
options.maxRssDeltaMb = parsePositiveNumber(value, name);
} else {
throw new Error(`Unsupported argument: ${argument}`);
}
}
if (options.batchSize > options.iterations) {
throw new Error('--batch-size cannot exceed --iterations');
}
return options;
}
function percentile(sortedValues, percentileValue) {
const index = Math.min(
sortedValues.length - 1,
Math.ceil((percentileValue / 100) * sortedValues.length) - 1,
);
return sortedValues[Math.max(0, index)];
}
function round(value) {
return Math.round(value * 1_000) / 1_000;
}
function enforceThreshold(actual, maximum, label) {
if (maximum !== undefined && actual > maximum) {
throw new Error(`${label} ${round(actual)} exceeded limit ${maximum}`);
}
}
function runBenchmark(options) {
const nodeMajor = Number(process.versions.node.split('.')[0]);
if (nodeMajor < 24) {
throw new Error('ql3-node-sqlite-benchmark requires Node.js 24 or newer');
}
const { DatabaseSync } = require('node:sqlite');
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-db-bench-'));
const databasePath = path.join(temporaryRoot, 'benchmark.sqlite');
const rssBefore = process.memoryUsage().rss;
const transactionDurations = [];
const batchDurations = [];
let database;
try {
database = new DatabaseSync(databasePath, {
allowExtension: false,
allowUnknownNamedParameters: false,
defensive: true,
enableDoubleQuotedStringLiterals: false,
enableForeignKeyConstraints: true,
timeout: 1_000,
});
database.exec(`
PRAGMA journal_mode = DELETE;
PRAGMA synchronous = FULL;
CREATE TABLE benchmark_runs (
id INTEGER PRIMARY KEY,
status TEXT NOT NULL,
version INTEGER NOT NULL,
event_sequence INTEGER NOT NULL,
created_at_ms INTEGER NOT NULL
);
CREATE TABLE benchmark_events (
id INTEGER PRIMARY KEY,
run_id INTEGER NOT NULL,
sequence INTEGER NOT NULL,
type TEXT NOT NULL,
payload TEXT NOT NULL,
created_at_ms INTEGER NOT NULL,
UNIQUE(run_id, sequence),
FOREIGN KEY(run_id) REFERENCES benchmark_runs(id) ON DELETE CASCADE
);
`);
const insertRun = database.prepare(
'INSERT INTO benchmark_runs(id, status, version, event_sequence, created_at_ms) VALUES (?, ?, ?, ?, ?)',
);
const insertEvent = database.prepare(
'INSERT INTO benchmark_events(id, run_id, sequence, type, payload, created_at_ms) VALUES (?, ?, ?, ?, ?, ?)',
);
const updateRun = database.prepare(
'UPDATE benchmark_runs SET status = ?, version = ?, event_sequence = ? WHERE id = ? AND version = ?',
);
let batchStartedAt = performance.now();
for (let index = 1; index <= options.iterations; index += 1) {
const startedAt = performance.now();
const now = 1_750_000_000_000 + index;
database.exec('BEGIN IMMEDIATE');
try {
insertRun.run(index, 'created', 0, 0, now);
insertEvent.run(
index * 2 - 1,
index,
1,
'run.created',
'{"source":"node-sqlite-benchmark"}',
now,
);
const update = updateRun.run('queued', 1, 2, index, 0);
if (update.changes !== 1) {
throw new Error('benchmark compare-and-set did not update one Run');
}
insertEvent.run(
index * 2,
index,
2,
'run.queued',
'{}',
now,
);
database.exec('COMMIT');
} catch (error) {
database.exec('ROLLBACK');
throw error;
}
transactionDurations.push(performance.now() - startedAt);
if (index % options.batchSize === 0 || index === options.iterations) {
batchDurations.push(performance.now() - batchStartedAt);
batchStartedAt = performance.now();
}
}
const integrity = database.prepare('PRAGMA integrity_check').get();
if (integrity.integrity_check !== 'ok') {
throw new Error(`integrity_check failed: ${integrity.integrity_check}`);
}
const runCount = database
.prepare('SELECT COUNT(*) AS count FROM benchmark_runs')
.get().count;
const eventCount = database
.prepare('SELECT COUNT(*) AS count FROM benchmark_events')
.get().count;
if (runCount !== options.iterations || eventCount !== options.iterations * 2) {
throw new Error('benchmark row counts do not match committed transactions');
}
database.close();
database = undefined;
const sortedTransactions = [...transactionDurations].sort((a, b) => a - b);
const rssDeltaMb = (process.memoryUsage().rss - rssBefore) / MIB;
const result = {
node: process.version,
arch: process.arch,
platform: process.platform,
iterations: options.iterations,
batchSize: options.batchSize,
journalMode: 'delete',
synchronous: 'full',
transactionMs: {
p50: round(percentile(sortedTransactions, 50)),
p95: round(percentile(sortedTransactions, 95)),
p99: round(percentile(sortedTransactions, 99)),
max: round(sortedTransactions[sortedTransactions.length - 1]),
},
maxBatchStallMs: round(Math.max(...batchDurations)),
rssDeltaMb: round(rssDeltaMb),
databaseBytes: fs.statSync(databasePath).size,
integrityCheck: 'ok',
};
enforceThreshold(
result.transactionMs.p95,
options.maxTransactionP95Ms,
'transaction p95 ms',
);
enforceThreshold(
result.maxBatchStallMs,
options.maxBatchStallMs,
'batch stall ms',
);
enforceThreshold(result.rssDeltaMb, options.maxRssDeltaMb, 'RSS delta MiB');
return result;
} finally {
if (database) {
database.close();
}
fs.rmSync(temporaryRoot, { recursive: true, force: true });
}
}
function main() {
const options = parseArguments(process.argv.slice(2));
const result = runBenchmark(options);
if (options.json) {
process.stdout.write(`${JSON.stringify(result)}\n`);
} else {
process.stdout.write(
[
`Node SQLite benchmark (${result.node}, ${result.arch})`,
`transactions: ${result.iterations}`,
`p50/p95/p99: ${result.transactionMs.p50}/${result.transactionMs.p95}/${result.transactionMs.p99} ms`,
`max batch stall: ${result.maxBatchStallMs} ms`,
`RSS delta: ${result.rssDeltaMb} MiB`,
`database: ${result.databaseBytes} bytes`,
].join('\n') + '\n',
);
}
}
try {
main();
} catch (error) {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`,
);
process.exitCode = 1;
}