pgserve 2.1.3 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -0
- package/README.md +105 -1
- package/bin/autopg-wrapper.cjs +16 -0
- package/bin/pgserve-wrapper.cjs +31 -6
- package/bin/postgres-server.js +56 -0
- package/console/README.md +131 -0
- package/console/api.js +173 -0
- package/console/app.jsx +483 -0
- package/console/colors_and_type.css +227 -0
- package/console/components.jsx +167 -0
- package/console/console.css +1666 -0
- package/console/data.jsx +350 -0
- package/console/index.html +31 -0
- package/console/screens/databases.jsx +5 -0
- package/console/screens/health.jsx +5 -0
- package/console/screens/ingress.jsx +5 -0
- package/console/screens/optimizer.jsx +5 -0
- package/console/screens/rlm-sim.jsx +5 -0
- package/console/screens/rlm-trace.jsx +5 -0
- package/console/screens/security.jsx +5 -0
- package/console/screens/settings.jsx +611 -0
- package/console/screens/sql.jsx +5 -0
- package/console/screens/sync.jsx +5 -0
- package/console/screens/tables.jsx +5 -0
- package/console/tweaks-panel.jsx +425 -0
- package/package.json +11 -1
- package/src/cli-config.cjs +310 -0
- package/src/cli-install.cjs +98 -11
- package/src/cli-restart.cjs +228 -0
- package/src/cli-ui.cjs +580 -0
- package/src/cluster.js +43 -38
- package/src/postgres.js +141 -19
- package/src/settings-loader.cjs +235 -0
- package/src/settings-migrate.cjs +212 -0
- package/src/settings-pg-args.cjs +146 -0
- package/src/settings-schema.cjs +422 -0
- package/src/settings-validator.cjs +416 -0
- package/src/settings-writer.cjs +288 -0
- package/.claude/context/windows-debug.md +0 -119
- package/.genie/AGENTS.md +0 -15
- package/.genie/agents/README.md +0 -110
- package/.genie/agents/analyze.md +0 -176
- package/.genie/agents/forge.md +0 -290
- package/.genie/agents/garbage-cleaner.md +0 -324
- package/.genie/agents/garbage-collector.md +0 -596
- package/.genie/agents/github-issue-gc.md +0 -618
- package/.genie/agents/review.md +0 -380
- package/.genie/agents/semantic-analyzer/find-duplicates.md +0 -90
- package/.genie/agents/semantic-analyzer/find-orphans.md +0 -99
- package/.genie/agents/semantic-analyzer.md +0 -101
- package/.genie/agents/update.md +0 -182
- package/.genie/agents/wish.md +0 -357
- package/.genie/brainstorms/pgserve-v2/DESIGN.md +0 -174
- package/.genie/code/AGENTS.md +0 -694
- package/.genie/code/agents/audit/risk.md +0 -173
- package/.genie/code/agents/audit/security.md +0 -189
- package/.genie/code/agents/audit.md +0 -145
- package/.genie/code/agents/challenge.md +0 -230
- package/.genie/code/agents/change-reviewer.md +0 -295
- package/.genie/code/agents/code-garbage-collector.md +0 -425
- package/.genie/code/agents/code-quality.md +0 -410
- package/.genie/code/agents/commit-suggester.md +0 -255
- package/.genie/code/agents/commit.md +0 -124
- package/.genie/code/agents/consensus.md +0 -204
- package/.genie/code/agents/daily-standup.md +0 -722
- package/.genie/code/agents/docgen.md +0 -48
- package/.genie/code/agents/explore.md +0 -79
- package/.genie/code/agents/fix.md +0 -100
- package/.genie/code/agents/git/commit-advisory.md +0 -219
- package/.genie/code/agents/git/workflows/issue.md +0 -244
- package/.genie/code/agents/git/workflows/pr.md +0 -179
- package/.genie/code/agents/git/workflows/release.md +0 -460
- package/.genie/code/agents/git/workflows/report.md +0 -342
- package/.genie/code/agents/git.md +0 -432
- package/.genie/code/agents/implementor.md +0 -161
- package/.genie/code/agents/install.md +0 -515
- package/.genie/code/agents/issue-creator.md +0 -344
- package/.genie/code/agents/polish.md +0 -116
- package/.genie/code/agents/qa.md +0 -653
- package/.genie/code/agents/refactor.md +0 -294
- package/.genie/code/agents/release.md +0 -1129
- package/.genie/code/agents/roadmap.md +0 -885
- package/.genie/code/agents/tests.md +0 -557
- package/.genie/code/agents/tracer.md +0 -50
- package/.genie/code/agents/update/upstream-update.md +0 -85
- package/.genie/code/agents/update/versions/generic-update.md +0 -305
- package/.genie/code/agents/vibe.md +0 -1317
- package/.genie/code/spells/agent-configuration.md +0 -58
- package/.genie/code/spells/automated-rc-publishing.md +0 -106
- package/.genie/code/spells/branch-tracker-guidance.md +0 -28
- package/.genie/code/spells/debug.md +0 -320
- package/.genie/code/spells/emoji-naming-convention.md +0 -303
- package/.genie/code/spells/evidence-storage.md +0 -26
- package/.genie/code/spells/file-naming-rules.md +0 -35
- package/.genie/code/spells/forge-code-blueprints.md +0 -195
- package/.genie/code/spells/genie-integration.md +0 -153
- package/.genie/code/spells/publishing-protocol.md +0 -61
- package/.genie/code/spells/team-consultation-protocol.md +0 -284
- package/.genie/code/spells/tool-requirements.md +0 -20
- package/.genie/code/spells/triad-maintenance-protocol.md +0 -154
- package/.genie/code/teams/tech-council/council.md +0 -328
- package/.genie/code/teams/tech-council/jt.md +0 -352
- package/.genie/code/teams/tech-council/nayr.md +0 -305
- package/.genie/code/teams/tech-council/oettam.md +0 -375
- package/.genie/neurons/README.md +0 -193
- package/.genie/neurons/forge.md +0 -106
- package/.genie/neurons/genie.md +0 -63
- package/.genie/neurons/review.md +0 -106
- package/.genie/neurons/wish.md +0 -104
- package/.genie/product/README.md +0 -20
- package/.genie/product/cli-automation.md +0 -359
- package/.genie/product/environment.md +0 -60
- package/.genie/product/mission.md +0 -60
- package/.genie/product/roadmap.md +0 -44
- package/.genie/product/tech-stack.md +0 -34
- package/.genie/product/templates/context-template.md +0 -218
- package/.genie/product/templates/qa-done-report-template.md +0 -68
- package/.genie/product/templates/review-report-template.md +0 -89
- package/.genie/product/templates/wish-template.md +0 -120
- package/.genie/scripts/helpers/analyze-commit.js +0 -195
- package/.genie/scripts/helpers/bullet-counter.js +0 -194
- package/.genie/scripts/helpers/bullet-find.js +0 -289
- package/.genie/scripts/helpers/bullet-id.js +0 -244
- package/.genie/scripts/helpers/check-secrets.js +0 -237
- package/.genie/scripts/helpers/count-tokens.js +0 -200
- package/.genie/scripts/helpers/create-frontmatter.js +0 -456
- package/.genie/scripts/helpers/detect-markers.js +0 -293
- package/.genie/scripts/helpers/detect-todos.js +0 -267
- package/.genie/scripts/helpers/detect-unlabeled-blocks.js +0 -135
- package/.genie/scripts/helpers/embeddings.js +0 -344
- package/.genie/scripts/helpers/find-empty-sections.js +0 -158
- package/.genie/scripts/helpers/index.js +0 -319
- package/.genie/scripts/helpers/validate-frontmatter.js +0 -578
- package/.genie/scripts/helpers/validate-links.js +0 -207
- package/.genie/scripts/helpers/validate-paths.js +0 -373
- package/.genie/spells/README.md +0 -9
- package/.genie/spells/ace-protocol.md +0 -118
- package/.genie/spells/ask-one-at-a-time.md +0 -175
- package/.genie/spells/backup-analyzer.md +0 -542
- package/.genie/spells/blocker.md +0 -12
- package/.genie/spells/break-things-move-fast.md +0 -56
- package/.genie/spells/context-candidates.md +0 -72
- package/.genie/spells/context-critic.md +0 -51
- package/.genie/spells/defer-to-expertise.md +0 -278
- package/.genie/spells/delegate-dont-do.md +0 -292
- package/.genie/spells/error-investigation-protocol.md +0 -328
- package/.genie/spells/evidence-based-completion.md +0 -273
- package/.genie/spells/experiment.md +0 -65
- package/.genie/spells/file-creation-protocol.md +0 -229
- package/.genie/spells/forge-integration.md +0 -281
- package/.genie/spells/forge-orchestration.md +0 -514
- package/.genie/spells/gather-context.md +0 -18
- package/.genie/spells/global-health-check.md +0 -34
- package/.genie/spells/global-noop-roundtrip.md +0 -25
- package/.genie/spells/install-genie.md +0 -1232
- package/.genie/spells/install.md +0 -82
- package/.genie/spells/investigate-before-commit.md +0 -112
- package/.genie/spells/know-yourself.md +0 -288
- package/.genie/spells/learn.md +0 -828
- package/.genie/spells/mcp-diagnostic-protocol.md +0 -246
- package/.genie/spells/mcp-first.md +0 -124
- package/.genie/spells/multi-step-execution.md +0 -67
- package/.genie/spells/orchestration-boundary-protocol.md +0 -256
- package/.genie/spells/orchestrator-not-implementor.md +0 -189
- package/.genie/spells/prompt.md +0 -746
- package/.genie/spells/reflect.md +0 -404
- package/.genie/spells/routing-decision-matrix.md +0 -368
- package/.genie/spells/run-in-parallel.md +0 -12
- package/.genie/spells/session-state-updater-example.md +0 -196
- package/.genie/spells/session-state-updater.md +0 -220
- package/.genie/spells/track-long-running-tasks.md +0 -133
- package/.genie/spells/troubleshoot-infrastructure.md +0 -176
- package/.genie/spells/upgrade-genie.md +0 -415
- package/.genie/spells/url-presentation-protocol.md +0 -301
- package/.genie/spells/wish-initiation.md +0 -158
- package/.genie/spells/wish-issue-linkage.md +0 -410
- package/.genie/spells/wish-lifecycle.md +0 -100
- package/.genie/state/provider-status.json +0 -3
- package/.genie/state/version.json +0 -16
- package/.genie/wishes/canonical-pgserve-pm2-supervision/WISH.md +0 -290
- package/.genie/wishes/pgserve-v2/BRIEF-from-genie-pgserve.md +0 -99
- package/.genie/wishes/pgserve-v2/WISH.md +0 -442
- package/.genie/wishes/release-system-genie-pattern/WISH.md +0 -268
- package/.genie/wishes/release-system-genie-pattern/validation.md +0 -205
- package/.gitguardian.yaml +0 -29
- package/.gitguardianignore +0 -16
- package/.github/workflows/ci.yml +0 -122
- package/.github/workflows/release.yml +0 -289
- package/.github/workflows/version.yml +0 -228
- package/.husky/pre-commit +0 -2
- package/AGENTS.md +0 -433
- package/CLAUDE.md +0 -1
- package/Makefile +0 -285
- package/assets/icon.ico +0 -0
- package/bun.lock +0 -435
- package/bunfig.toml +0 -28
- package/ecosystem.config.cjs +0 -23
- package/eslint.config.js +0 -63
- package/examples/multi-tenant-demo.js +0 -104
- package/install.sh +0 -123
- package/knip.json +0 -9
- package/scripts/test-bun-self-heal.sh +0 -163
- package/scripts/test-npx.sh +0 -60
- package/tests/audit.test.js +0 -189
- package/tests/backpressure.test.js +0 -167
- package/tests/benchmarks/runner.js +0 -1197
- package/tests/benchmarks/vector-generator.js +0 -368
- package/tests/cli-install.test.js +0 -322
- package/tests/control-db.test.js +0 -285
- package/tests/daemon-args.test.js +0 -86
- package/tests/daemon-control.test.js +0 -171
- package/tests/daemon-fingerprint-integration.test.js +0 -111
- package/tests/daemon-pr24-regression.test.js +0 -198
- package/tests/fingerprint.test.js +0 -263
- package/tests/fixtures/240-orphan-seed.sql +0 -30
- package/tests/multi-tenant.test.js +0 -374
- package/tests/orphan-cleanup.test.js +0 -390
- package/tests/pg-version-regex.test.js +0 -129
- package/tests/quick-bench.js +0 -135
- package/tests/router-handshake-retry.test.js +0 -119
- package/tests/router-handshake-watchdog.test.js +0 -110
- package/tests/sdk.test.js +0 -71
- package/tests/stale-postmaster-pid.test.js +0 -85
- package/tests/stress-test.js +0 -439
- package/tests/sync-perf-test.js +0 -150
- package/tests/tcp-listen.test.js +0 -368
- package/tests/tenancy.test.js +0 -403
- package/tests/wrapper-supervision.test.js +0 -107
|
@@ -1,1197 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Benchmark Runner
|
|
5
|
-
* Compares SQLite, PostgreSQL, and pgserve performance
|
|
6
|
-
*
|
|
7
|
-
* 100% Bun-native: Uses bun:sqlite instead of better-sqlite3
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { Database } from 'bun:sqlite';
|
|
11
|
-
import { startMultiTenantServer } from '../../src/index.js';
|
|
12
|
-
import { spawn } from 'child_process';
|
|
13
|
-
import fs from 'fs';
|
|
14
|
-
import path from 'path';
|
|
15
|
-
import os from 'os';
|
|
16
|
-
import pg from 'pg';
|
|
17
|
-
import { loadEmbeddings, generateQueryVectors, formatPgVector, getEmbeddingsPath, getGroundTruth, calculateRecall } from './vector-generator.js';
|
|
18
|
-
|
|
19
|
-
const { Pool } = pg;
|
|
20
|
-
const LEGACY_PGSERVE_VERSION = '1.2.0';
|
|
21
|
-
const LEGACY_PGSERVE_SPEC = `pgserve@${LEGACY_PGSERVE_VERSION}`;
|
|
22
|
-
const NPX_BIN = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
23
|
-
|
|
24
|
-
// ============================================================================
|
|
25
|
-
// ANSI Colors and Visual Utilities (stress-test style)
|
|
26
|
-
// ============================================================================
|
|
27
|
-
const C = {
|
|
28
|
-
reset: '\x1b[0m',
|
|
29
|
-
bold: '\x1b[1m',
|
|
30
|
-
dim: '\x1b[2m',
|
|
31
|
-
green: '\x1b[32m',
|
|
32
|
-
yellow: '\x1b[33m',
|
|
33
|
-
cyan: '\x1b[36m',
|
|
34
|
-
red: '\x1b[31m',
|
|
35
|
-
magenta: '\x1b[35m',
|
|
36
|
-
blue: '\x1b[34m',
|
|
37
|
-
white: '\x1b[37m',
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Print benchmark banner
|
|
42
|
-
*/
|
|
43
|
-
function banner() {
|
|
44
|
-
console.log(`
|
|
45
|
-
${C.cyan}${C.bold}╔════════════════════════════════════════════════════════════════╗
|
|
46
|
-
║ pgserve UNIFIED BENCHMARK SUITE ║
|
|
47
|
-
║ ║
|
|
48
|
-
║ Comparing: SQLite │ PostgreSQL │ pgserve 1.2.0 │ pgserve v2 ║
|
|
49
|
-
╚════════════════════════════════════════════════════════════════╝${C.reset}
|
|
50
|
-
`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Print section header
|
|
55
|
-
*/
|
|
56
|
-
function section(name, description) {
|
|
57
|
-
console.log(`
|
|
58
|
-
${C.yellow}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}
|
|
59
|
-
${C.bold}${C.cyan}▶ ${name}${C.reset}
|
|
60
|
-
${C.dim} ${description}${C.reset}
|
|
61
|
-
${C.yellow}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}
|
|
62
|
-
`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Progress bar
|
|
67
|
-
*/
|
|
68
|
-
function progressBar(current, total, width = 30) {
|
|
69
|
-
const pct = Math.min(1, Math.max(0, current / total || 0));
|
|
70
|
-
// filled is clamped to [0, width], so (width - filled) is always non-negative
|
|
71
|
-
const filled = Math.max(0, Math.min(width, Math.round(pct * width)));
|
|
72
|
-
const empty = width - filled;
|
|
73
|
-
return `[${C.green}${'█'.repeat(filled)}${C.dim}${'░'.repeat(empty)}${C.reset}] ${(pct * 100).toFixed(0)}%`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Calculate score for an engine
|
|
78
|
-
* Higher is better - weighted combination of throughput and latency
|
|
79
|
-
*/
|
|
80
|
-
function calculateScore(results) {
|
|
81
|
-
if (results.skipped) return 0;
|
|
82
|
-
|
|
83
|
-
// Score formula:
|
|
84
|
-
// - Base: throughput QPS (main factor)
|
|
85
|
-
// - Bonus: low latency (P99 < 10ms gets bonus)
|
|
86
|
-
// - Penalty: errors
|
|
87
|
-
const throughputScore = results.throughput || 0;
|
|
88
|
-
const latencyBonus = results.p99 > 0 ? Math.max(0, (10 - results.p99) * 10) : 0;
|
|
89
|
-
const errorPenalty = (results.errors || 0) * 100;
|
|
90
|
-
|
|
91
|
-
return Math.round(throughputScore + latencyBonus - errorPenalty);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Print final results table with scores
|
|
96
|
-
*/
|
|
97
|
-
function printFinalResults(allResults, vectorResults, canUseRam) {
|
|
98
|
-
console.log(`
|
|
99
|
-
${C.cyan}${C.bold}
|
|
100
|
-
╔════════════════════════════════════════════════════════════════╗
|
|
101
|
-
║ FINAL RESULTS ║
|
|
102
|
-
╚════════════════════════════════════════════════════════════════╝${C.reset}
|
|
103
|
-
`);
|
|
104
|
-
|
|
105
|
-
// Aggregate results per engine
|
|
106
|
-
// Note: recallCount tracks only SEARCH scenarios (INSERT has 'N/A' recall)
|
|
107
|
-
const engines = {
|
|
108
|
-
sqlite: { name: 'SQLite', crudQps: 0, vecQps: 0, vecRecall: 0, p50: 0, p99: 0, errors: 0, count: 0, vecCount: 0, recallCount: 0 },
|
|
109
|
-
postgres: { name: 'PostgreSQL', crudQps: 0, vecQps: 0, vecRecall: 0, p50: 0, p99: 0, errors: 0, count: 0, vecCount: 0, recallCount: 0, skipped: false },
|
|
110
|
-
pgserveV1: { name: 'pgserve 1.2.0', crudQps: 0, vecQps: 0, vecRecall: 0, p50: 0, p99: 0, errors: 0, count: 0, vecCount: 0, recallCount: 0 },
|
|
111
|
-
pgserve: { name: 'pgserve v2', crudQps: 0, vecQps: 0, vecRecall: 0, p50: 0, p99: 0, errors: 0, count: 0, vecCount: 0, recallCount: 0 },
|
|
112
|
-
pgserveRam: { name: 'pgserve v2 RAM', crudQps: 0, vecQps: 0, vecRecall: 0, p50: 0, p99: 0, errors: 0, count: 0, vecCount: 0, recallCount: 0 },
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// Aggregate CRUD results
|
|
116
|
-
for (const r of allResults) {
|
|
117
|
-
for (const [key, eng] of Object.entries(engines)) {
|
|
118
|
-
const data = r[key];
|
|
119
|
-
if (data && !data.skipped) {
|
|
120
|
-
eng.crudQps += data.throughput || 0;
|
|
121
|
-
eng.p50 += data.p50 || 0;
|
|
122
|
-
eng.p99 += data.p99 || 0;
|
|
123
|
-
eng.errors += data.errors || 0;
|
|
124
|
-
eng.count++;
|
|
125
|
-
} else if (data?.skipped) {
|
|
126
|
-
eng.skipped = true;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Aggregate Vector results (with recall)
|
|
132
|
-
// Note: INSERT scenarios have recall='N/A', only SEARCH scenarios have numeric recall
|
|
133
|
-
for (const r of vectorResults) {
|
|
134
|
-
for (const [key, eng] of Object.entries(engines)) {
|
|
135
|
-
const data = r[key];
|
|
136
|
-
if (data && !data.skipped) {
|
|
137
|
-
eng.vecQps += data.throughput || 0;
|
|
138
|
-
eng.vecCount++;
|
|
139
|
-
// Only count recall for SEARCH scenarios (not INSERT which has 'N/A')
|
|
140
|
-
const recallValue = parseFloat(data.recall);
|
|
141
|
-
if (!isNaN(recallValue)) {
|
|
142
|
-
eng.vecRecall += recallValue;
|
|
143
|
-
eng.recallCount++;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Average the results
|
|
150
|
-
for (const eng of Object.values(engines)) {
|
|
151
|
-
if (eng.count > 0) {
|
|
152
|
-
eng.crudQps = Math.round(eng.crudQps / eng.count);
|
|
153
|
-
eng.p50 = (eng.p50 / eng.count).toFixed(1);
|
|
154
|
-
eng.p99 = (eng.p99 / eng.count).toFixed(1);
|
|
155
|
-
}
|
|
156
|
-
if (eng.vecCount > 0) {
|
|
157
|
-
eng.vecQps = Math.round(eng.vecQps / eng.vecCount);
|
|
158
|
-
} else {
|
|
159
|
-
eng.vecQps = 0;
|
|
160
|
-
}
|
|
161
|
-
// Average recall only from SEARCH scenarios (recallCount), not INSERT scenarios
|
|
162
|
-
if (eng.recallCount > 0) {
|
|
163
|
-
eng.vecRecall = (eng.vecRecall / eng.recallCount).toFixed(1);
|
|
164
|
-
} else {
|
|
165
|
-
eng.vecRecall = 'N/A';
|
|
166
|
-
}
|
|
167
|
-
eng.score = Math.round(eng.crudQps * 0.6 + eng.vecQps * 0.4 - eng.errors * 10);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Print table header (with Recall column)
|
|
171
|
-
const hasVec = vectorResults.length > 0;
|
|
172
|
-
if (hasVec) {
|
|
173
|
-
console.log(`${C.bold}Engine │ CRUD QPS │ Vec QPS │ Recall │ P50 │ P99 │ Errors │ SCORE${C.reset}`);
|
|
174
|
-
console.log(`${'─'.repeat(90)}`);
|
|
175
|
-
} else {
|
|
176
|
-
console.log(`${C.bold}Engine │ CRUD QPS │ P50 │ P99 │ Errors │ SCORE${C.reset}`);
|
|
177
|
-
console.log(`${'─'.repeat(70)}`);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Print each engine row
|
|
181
|
-
const engineOrder = ['sqlite', 'postgres', 'pgserveV1', 'pgserve'];
|
|
182
|
-
if (canUseRam) engineOrder.push('pgserveRam');
|
|
183
|
-
|
|
184
|
-
let maxScore = 0;
|
|
185
|
-
let winner = '';
|
|
186
|
-
|
|
187
|
-
for (const key of engineOrder) {
|
|
188
|
-
const eng = engines[key];
|
|
189
|
-
if (eng.skipped) {
|
|
190
|
-
if (hasVec) {
|
|
191
|
-
console.log(`${C.dim}${eng.name.padEnd(17)} │ ${'-'.padStart(8)} │ ${'-'.padStart(7)} │ ${'-'.padStart(6)} │ ${'-'.padStart(7)} │ ${'-'.padStart(7)} │ ${'-'.padStart(6)} │ ${'-'.padStart(7)}${C.reset}`);
|
|
192
|
-
} else {
|
|
193
|
-
console.log(`${C.dim}${eng.name.padEnd(17)} │ ${'-'.padStart(8)} │ ${'-'.padStart(7)} │ ${'-'.padStart(7)} │ ${'-'.padStart(6)} │ ${'-'.padStart(7)}${C.reset}`);
|
|
194
|
-
}
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const color = eng.errors > 0 ? C.yellow : C.green;
|
|
199
|
-
const scoreColor = eng.score > maxScore ? C.magenta + C.bold : color;
|
|
200
|
-
|
|
201
|
-
if (eng.score > maxScore) {
|
|
202
|
-
maxScore = eng.score;
|
|
203
|
-
winner = eng.name;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (hasVec) {
|
|
207
|
-
// vecRecall is 'N/A' for INSERT-only scenarios, or a numeric string like '100.0' for SEARCH
|
|
208
|
-
const recallStr = eng.vecRecall !== 'N/A' ? `${eng.vecRecall}%` : 'N/A';
|
|
209
|
-
const vecQpsStr = eng.vecCount > 0 ? eng.vecQps.toLocaleString() : 'N/A';
|
|
210
|
-
console.log(`${color}${eng.name.padEnd(17)} │ ${String(eng.crudQps.toLocaleString()).padStart(8)} │ ${vecQpsStr.padStart(7)} │ ${recallStr.padStart(6)} │ ${(eng.p50 + 'ms').padStart(7)} │ ${(eng.p99 + 'ms').padStart(7)} │ ${String(eng.errors).padStart(6)} │ ${scoreColor}${String(eng.score.toLocaleString()).padStart(7)}${C.reset}`);
|
|
211
|
-
} else {
|
|
212
|
-
console.log(`${color}${eng.name.padEnd(17)} │ ${String(eng.crudQps.toLocaleString()).padStart(8)} │ ${(eng.p50 + 'ms').padStart(7)} │ ${(eng.p99 + 'ms').padStart(7)} │ ${String(eng.errors).padStart(6)} │ ${scoreColor}${String(eng.score.toLocaleString()).padStart(7)}${C.reset}`);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
console.log(`${'─'.repeat(hasVec ? 90 : 70)}`);
|
|
217
|
-
|
|
218
|
-
// Winner announcement
|
|
219
|
-
console.log(`
|
|
220
|
-
${C.magenta}${C.bold}╔═══════════════════════════════════════════════════╗
|
|
221
|
-
║ 🏆 WINNER: ${winner.padEnd(20)} SCORE: ${String(maxScore).padStart(7)} ║
|
|
222
|
-
╚═══════════════════════════════════════════════════╝${C.reset}
|
|
223
|
-
`);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Global error handlers (suppress expected PostgreSQL WASM ExitStatus errors)
|
|
227
|
-
process.on('unhandledRejection', (reason, promise) => {
|
|
228
|
-
if (reason && reason.name === 'ExitStatus') return;
|
|
229
|
-
console.error('Unhandled Promise Rejection:', reason);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
process.on('uncaughtException', (error) => {
|
|
233
|
-
if (error && error.name === 'ExitStatus') return;
|
|
234
|
-
console.error('Uncaught Exception:', error);
|
|
235
|
-
process.exit(1);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
const RESULTS_DIR = new URL('./results', import.meta.url).pathname;
|
|
239
|
-
|
|
240
|
-
// PostgreSQL Server configuration (Docker with tmpfs for fair RAM-to-RAM comparison)
|
|
241
|
-
const POSTGRES_CONFIG = {
|
|
242
|
-
host: 'localhost',
|
|
243
|
-
port: 5433, // pgvector/pgvector:pg17 Docker container
|
|
244
|
-
user: 'postgres',
|
|
245
|
-
password: 'postgres',
|
|
246
|
-
database: 'postgres'
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Benchmark scenario configuration
|
|
251
|
-
*/
|
|
252
|
-
const scenarios = [
|
|
253
|
-
{
|
|
254
|
-
name: 'Concurrent Writes (10 agents)',
|
|
255
|
-
description: 'Simulates 10 concurrent agents writing simultaneously',
|
|
256
|
-
operations: [
|
|
257
|
-
{ type: 'INSERT', count: 100, concurrent: 10 }
|
|
258
|
-
]
|
|
259
|
-
},
|
|
260
|
-
{
|
|
261
|
-
name: 'Mixed Workload (messages)',
|
|
262
|
-
description: 'Simulates typical API message operations',
|
|
263
|
-
operations: [
|
|
264
|
-
{ type: 'INSERT', count: 500 },
|
|
265
|
-
{ type: 'SELECT', count: 2000 },
|
|
266
|
-
{ type: 'UPDATE', count: 250 }
|
|
267
|
-
]
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
name: 'Write Lock Contention',
|
|
271
|
-
description: 'Stress test for lock handling with 50 concurrent writers',
|
|
272
|
-
operations: [
|
|
273
|
-
{ type: 'INSERT', count: 100, concurrent: 50 }
|
|
274
|
-
]
|
|
275
|
-
}
|
|
276
|
-
];
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Vector benchmark scenarios (pgvector)
|
|
280
|
-
* Requires: --include-vector flag
|
|
281
|
-
*/
|
|
282
|
-
/**
|
|
283
|
-
* Vector benchmark scenarios
|
|
284
|
-
* Following industry-standard methodology (ANN-Benchmarks, Qdrant, VectorDBBench):
|
|
285
|
-
* - Measure Recall@k alongside QPS
|
|
286
|
-
* - Compare approximate results to brute-force ground truth
|
|
287
|
-
* - Report both metrics together (can't compare QPS without knowing recall)
|
|
288
|
-
*/
|
|
289
|
-
const vectorScenarios = [
|
|
290
|
-
{
|
|
291
|
-
name: 'Vector INSERT (1000 vectors)',
|
|
292
|
-
description: 'Bulk insert performance - where RAM mode shows benefits',
|
|
293
|
-
type: 'INSERT',
|
|
294
|
-
dimension: 1536,
|
|
295
|
-
insertCount: 1000, // Insert 1000 vectors to measure write speed
|
|
296
|
-
},
|
|
297
|
-
{
|
|
298
|
-
name: 'k-NN Search (k=10)',
|
|
299
|
-
description: 'Recall@10 and QPS on 10k vectors, 100 queries',
|
|
300
|
-
type: 'SEARCH',
|
|
301
|
-
dimension: 1536,
|
|
302
|
-
corpusSize: 10000,
|
|
303
|
-
queryCount: 100,
|
|
304
|
-
k: 10,
|
|
305
|
-
warmupQueries: 20 // Warm-up before measuring
|
|
306
|
-
},
|
|
307
|
-
{
|
|
308
|
-
name: 'k-NN Search (k=100)',
|
|
309
|
-
description: 'Recall@100 and QPS on 10k vectors - harder recall target',
|
|
310
|
-
type: 'SEARCH',
|
|
311
|
-
dimension: 1536,
|
|
312
|
-
corpusSize: 10000,
|
|
313
|
-
queryCount: 100,
|
|
314
|
-
k: 100,
|
|
315
|
-
warmupQueries: 20
|
|
316
|
-
}
|
|
317
|
-
];
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Performance metrics
|
|
321
|
-
*/
|
|
322
|
-
class Metrics {
|
|
323
|
-
constructor() {
|
|
324
|
-
this.latencies = [];
|
|
325
|
-
this.errors = 0;
|
|
326
|
-
this.lockTimeouts = 0;
|
|
327
|
-
this.startTime = 0;
|
|
328
|
-
this.endTime = 0;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
start() {
|
|
332
|
-
this.startTime = Date.now();
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
end() {
|
|
336
|
-
this.endTime = Date.now();
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
addLatency(ms) {
|
|
340
|
-
this.latencies.push(ms);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
addError(error) {
|
|
344
|
-
this.errors++;
|
|
345
|
-
if (error.message && error.message.includes('SQLITE_BUSY')) {
|
|
346
|
-
this.lockTimeouts++;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
getThroughput() {
|
|
351
|
-
const durationMs = this.endTime - this.startTime;
|
|
352
|
-
const durationS = durationMs / 1000;
|
|
353
|
-
return Math.round(this.latencies.length / durationS);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
getPercentile(p) {
|
|
357
|
-
if (this.latencies.length === 0) return 0;
|
|
358
|
-
const sorted = [...this.latencies].sort((a, b) => a - b);
|
|
359
|
-
const index = Math.ceil((sorted.length * p) / 100) - 1;
|
|
360
|
-
return sorted[Math.max(0, index)];
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
getReport() {
|
|
364
|
-
return {
|
|
365
|
-
throughput: this.getThroughput(),
|
|
366
|
-
p50: this.getPercentile(50),
|
|
367
|
-
p99: this.getPercentile(99),
|
|
368
|
-
errors: this.errors,
|
|
369
|
-
lockTimeouts: this.lockTimeouts,
|
|
370
|
-
totalOps: this.latencies.length
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function skippedCrud(reason, errors = 1) {
|
|
376
|
-
return { throughput: 0, p50: 0, p99: 0, errors, lockTimeouts: 0, totalOps: 0, skipped: true, reason };
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function skippedVector(reason, errors = 1) {
|
|
380
|
-
return { throughput: 0, recall: 'N/A', p50: 0, p99: 0, errors, totalOps: 0, skipped: true, reason };
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
async function openPgPool({ port, database, max = 20, timeoutMs = 30_000 }) {
|
|
384
|
-
const pool = new Pool({
|
|
385
|
-
host: '127.0.0.1',
|
|
386
|
-
port,
|
|
387
|
-
database,
|
|
388
|
-
user: 'postgres',
|
|
389
|
-
password: 'postgres',
|
|
390
|
-
max,
|
|
391
|
-
connectionTimeoutMillis: 1000
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
const deadline = Date.now() + timeoutMs;
|
|
395
|
-
let lastError;
|
|
396
|
-
while (Date.now() < deadline) {
|
|
397
|
-
try {
|
|
398
|
-
await pool.query('SELECT 1');
|
|
399
|
-
return pool;
|
|
400
|
-
} catch (error) {
|
|
401
|
-
lastError = error;
|
|
402
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
await pool.end().catch(() => {});
|
|
407
|
-
throw lastError || new Error(`PostgreSQL did not become ready on port ${port}`);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
async function runCrudScenarioOnPool(pool, scenario) {
|
|
411
|
-
await pool.query(`
|
|
412
|
-
DROP TABLE IF EXISTS bench_messages;
|
|
413
|
-
CREATE TABLE bench_messages (
|
|
414
|
-
id SERIAL PRIMARY KEY,
|
|
415
|
-
content TEXT,
|
|
416
|
-
timestamp BIGINT
|
|
417
|
-
)
|
|
418
|
-
`);
|
|
419
|
-
|
|
420
|
-
const metrics = new Metrics();
|
|
421
|
-
metrics.start();
|
|
422
|
-
|
|
423
|
-
for (const op of scenario.operations) {
|
|
424
|
-
if (op.type === 'INSERT') {
|
|
425
|
-
const concurrent = op.concurrent || 1;
|
|
426
|
-
const perThread = Math.floor(op.count / concurrent);
|
|
427
|
-
|
|
428
|
-
const promises = [];
|
|
429
|
-
for (let i = 0; i < concurrent; i++) {
|
|
430
|
-
promises.push(
|
|
431
|
-
(async () => {
|
|
432
|
-
for (let j = 0; j < perThread; j++) {
|
|
433
|
-
const start = Date.now();
|
|
434
|
-
try {
|
|
435
|
-
await pool.query(
|
|
436
|
-
'INSERT INTO bench_messages (content, timestamp) VALUES ($1, $2)',
|
|
437
|
-
[`Message ${i}-${j}`, Date.now()]
|
|
438
|
-
);
|
|
439
|
-
metrics.addLatency(Date.now() - start);
|
|
440
|
-
} catch (error) {
|
|
441
|
-
metrics.addError(error);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
})()
|
|
445
|
-
);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
await Promise.all(promises);
|
|
449
|
-
} else if (op.type === 'SELECT') {
|
|
450
|
-
for (let i = 0; i < op.count; i++) {
|
|
451
|
-
const start = Date.now();
|
|
452
|
-
try {
|
|
453
|
-
await pool.query('SELECT * FROM bench_messages LIMIT 10');
|
|
454
|
-
metrics.addLatency(Date.now() - start);
|
|
455
|
-
} catch (error) {
|
|
456
|
-
metrics.addError(error);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
} else if (op.type === 'UPDATE') {
|
|
460
|
-
for (let i = 0; i < op.count; i++) {
|
|
461
|
-
const start = Date.now();
|
|
462
|
-
try {
|
|
463
|
-
await pool.query(
|
|
464
|
-
'UPDATE bench_messages SET content = $1 WHERE id = $2',
|
|
465
|
-
[`Updated ${i}`, (i % 100) + 1]
|
|
466
|
-
);
|
|
467
|
-
metrics.addLatency(Date.now() - start);
|
|
468
|
-
} catch (error) {
|
|
469
|
-
metrics.addError(error);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
metrics.end();
|
|
476
|
-
await pool.query('DROP TABLE IF EXISTS bench_messages');
|
|
477
|
-
return metrics.getReport();
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
async function runVectorScenarioOnPool(pool, scenario, embeddings, queryVectors, groundTruth) {
|
|
481
|
-
await pool.query('CREATE EXTENSION IF NOT EXISTS vector');
|
|
482
|
-
|
|
483
|
-
await pool.query(`
|
|
484
|
-
DROP TABLE IF EXISTS embeddings;
|
|
485
|
-
CREATE TABLE embeddings (
|
|
486
|
-
id INTEGER PRIMARY KEY,
|
|
487
|
-
vector vector(${scenario.dimension})
|
|
488
|
-
)
|
|
489
|
-
`);
|
|
490
|
-
|
|
491
|
-
if (scenario.type === 'INSERT') {
|
|
492
|
-
console.log(` Inserting ${scenario.insertCount} vectors (measured)...`);
|
|
493
|
-
const latencies = [];
|
|
494
|
-
|
|
495
|
-
for (let i = 0; i < scenario.insertCount; i++) {
|
|
496
|
-
const vec = formatPgVector(embeddings.vectors[i]);
|
|
497
|
-
const start = performance.now();
|
|
498
|
-
await pool.query('INSERT INTO embeddings (id, vector) VALUES ($1, $2::vector)', [i + 1, vec]);
|
|
499
|
-
latencies.push(performance.now() - start);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const totalTime = latencies.reduce((a, b) => a + b, 0);
|
|
503
|
-
const qps = Math.round((scenario.insertCount / totalTime) * 1000);
|
|
504
|
-
latencies.sort((a, b) => a - b);
|
|
505
|
-
const p50 = latencies[Math.floor(latencies.length * 0.5)]?.toFixed(1) || 0;
|
|
506
|
-
const p99 = latencies[Math.floor(latencies.length * 0.99)]?.toFixed(1) || 0;
|
|
507
|
-
|
|
508
|
-
await pool.query('DROP TABLE IF EXISTS embeddings');
|
|
509
|
-
|
|
510
|
-
return {
|
|
511
|
-
throughput: qps,
|
|
512
|
-
recall: 'N/A',
|
|
513
|
-
p50: parseFloat(p50),
|
|
514
|
-
p99: parseFloat(p99),
|
|
515
|
-
errors: 0,
|
|
516
|
-
totalOps: scenario.insertCount,
|
|
517
|
-
skipped: false
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
console.log(' Inserting vectors...');
|
|
522
|
-
for (let i = 0; i < embeddings.vectors.length; i++) {
|
|
523
|
-
const vec = formatPgVector(embeddings.vectors[i]);
|
|
524
|
-
await pool.query('INSERT INTO embeddings (id, vector) VALUES ($1, $2::vector)', [i + 1, vec]);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
console.log(' Warming up...');
|
|
528
|
-
for (let i = 0; i < scenario.warmupQueries; i++) {
|
|
529
|
-
const queryVec = formatPgVector(queryVectors[i % queryVectors.length]);
|
|
530
|
-
await pool.query('SELECT id FROM embeddings ORDER BY vector <-> $1::vector LIMIT $2', [queryVec, scenario.k]);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
console.log(' Running measured queries...');
|
|
534
|
-
const latencies = [];
|
|
535
|
-
const approximateResults = [];
|
|
536
|
-
|
|
537
|
-
for (let i = 0; i < scenario.queryCount; i++) {
|
|
538
|
-
const queryVec = formatPgVector(queryVectors[i]);
|
|
539
|
-
const start = performance.now();
|
|
540
|
-
const result = await pool.query(
|
|
541
|
-
'SELECT id FROM embeddings ORDER BY vector <-> $1::vector LIMIT $2',
|
|
542
|
-
[queryVec, scenario.k]
|
|
543
|
-
);
|
|
544
|
-
latencies.push(performance.now() - start);
|
|
545
|
-
approximateResults.push(result.rows.map(r => r.id));
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
const { recall } = calculateRecall(approximateResults, groundTruth, scenario.k);
|
|
549
|
-
const totalTime = latencies.reduce((a, b) => a + b, 0);
|
|
550
|
-
const qps = Math.round((scenario.queryCount / totalTime) * 1000);
|
|
551
|
-
latencies.sort((a, b) => a - b);
|
|
552
|
-
const p50 = latencies[Math.floor(latencies.length * 0.5)]?.toFixed(1) || 0;
|
|
553
|
-
const p99 = latencies[Math.floor(latencies.length * 0.99)]?.toFixed(1) || 0;
|
|
554
|
-
|
|
555
|
-
await pool.query('DROP TABLE IF EXISTS embeddings');
|
|
556
|
-
|
|
557
|
-
return {
|
|
558
|
-
throughput: qps,
|
|
559
|
-
recall: (recall * 100).toFixed(1),
|
|
560
|
-
p50: parseFloat(p50),
|
|
561
|
-
p99: parseFloat(p99),
|
|
562
|
-
errors: 0,
|
|
563
|
-
totalOps: scenario.queryCount,
|
|
564
|
-
skipped: false
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
async function startLegacyPgserve({ port, enablePgvector = false }) {
|
|
569
|
-
const dataDir = path.join(RESULTS_DIR, `pgserve-${LEGACY_PGSERVE_VERSION}-port-${port}`);
|
|
570
|
-
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
571
|
-
|
|
572
|
-
const args = [
|
|
573
|
-
'-y',
|
|
574
|
-
LEGACY_PGSERVE_SPEC,
|
|
575
|
-
'--port',
|
|
576
|
-
String(port),
|
|
577
|
-
'--host',
|
|
578
|
-
'127.0.0.1',
|
|
579
|
-
'--data',
|
|
580
|
-
dataDir,
|
|
581
|
-
'--log',
|
|
582
|
-
'error',
|
|
583
|
-
'--no-stats',
|
|
584
|
-
'--no-cluster',
|
|
585
|
-
];
|
|
586
|
-
if (enablePgvector) args.push('--pgvector');
|
|
587
|
-
|
|
588
|
-
const tail = [];
|
|
589
|
-
const child = spawn(NPX_BIN, args, {
|
|
590
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
591
|
-
env: { ...process.env, NO_COLOR: '1' },
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
const append = (chunk) => {
|
|
595
|
-
tail.push(String(chunk));
|
|
596
|
-
while (tail.join('').length > 4000) tail.shift();
|
|
597
|
-
};
|
|
598
|
-
child.stdout.on('data', append);
|
|
599
|
-
child.stderr.on('data', append);
|
|
600
|
-
|
|
601
|
-
let exited = false;
|
|
602
|
-
child.once('exit', () => {
|
|
603
|
-
exited = true;
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
try {
|
|
607
|
-
const pool = await openPgPool({ port, database: 'bench_test', timeoutMs: 60_000 });
|
|
608
|
-
await pool.end();
|
|
609
|
-
} catch (error) {
|
|
610
|
-
await stopChildProcess(child);
|
|
611
|
-
const output = tail.join('').trim();
|
|
612
|
-
const detail = output ? `; output: ${output}` : '';
|
|
613
|
-
throw new Error(`${LEGACY_PGSERVE_SPEC} failed to become ready: ${error.message}${detail}`);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
return {
|
|
617
|
-
async stop() {
|
|
618
|
-
if (!exited) await stopChildProcess(child);
|
|
619
|
-
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
620
|
-
},
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
async function stopChildProcess(child) {
|
|
625
|
-
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
626
|
-
|
|
627
|
-
child.kill('SIGTERM');
|
|
628
|
-
const exited = await new Promise((resolve) => {
|
|
629
|
-
const timer = setTimeout(() => resolve(false), 3000);
|
|
630
|
-
child.once('exit', () => {
|
|
631
|
-
clearTimeout(timer);
|
|
632
|
-
resolve(true);
|
|
633
|
-
});
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
if (!exited) {
|
|
637
|
-
child.kill('SIGKILL');
|
|
638
|
-
await new Promise((resolve) => child.once('exit', resolve));
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* SQLite Benchmark
|
|
644
|
-
*/
|
|
645
|
-
async function benchmarkSQLite(scenario) {
|
|
646
|
-
console.log(' 🔸 Running SQLite benchmark...');
|
|
647
|
-
|
|
648
|
-
const dbPath = path.join(RESULTS_DIR, 'sqlite-bench.db');
|
|
649
|
-
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
|
|
650
|
-
|
|
651
|
-
const db = new Database(dbPath);
|
|
652
|
-
|
|
653
|
-
// Setup schema
|
|
654
|
-
db.exec(`
|
|
655
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
656
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
657
|
-
content TEXT,
|
|
658
|
-
timestamp INTEGER
|
|
659
|
-
)
|
|
660
|
-
`);
|
|
661
|
-
|
|
662
|
-
const metrics = new Metrics();
|
|
663
|
-
metrics.start();
|
|
664
|
-
|
|
665
|
-
// Run operations
|
|
666
|
-
for (const op of scenario.operations) {
|
|
667
|
-
if (op.type === 'INSERT') {
|
|
668
|
-
const concurrent = op.concurrent || 1;
|
|
669
|
-
const perThread = Math.floor(op.count / concurrent);
|
|
670
|
-
|
|
671
|
-
for (let i = 0; i < concurrent; i++) {
|
|
672
|
-
for (let j = 0; j < perThread; j++) {
|
|
673
|
-
const start = Date.now();
|
|
674
|
-
try {
|
|
675
|
-
db.prepare('INSERT INTO messages (content, timestamp) VALUES (?, ?)').run(
|
|
676
|
-
`Message ${i}-${j}`,
|
|
677
|
-
Date.now()
|
|
678
|
-
);
|
|
679
|
-
metrics.addLatency(Date.now() - start);
|
|
680
|
-
} catch (error) {
|
|
681
|
-
metrics.addError(error);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
} else if (op.type === 'SELECT') {
|
|
686
|
-
for (let i = 0; i < op.count; i++) {
|
|
687
|
-
const start = Date.now();
|
|
688
|
-
try {
|
|
689
|
-
db.prepare('SELECT * FROM messages LIMIT 10').all();
|
|
690
|
-
metrics.addLatency(Date.now() - start);
|
|
691
|
-
} catch (error) {
|
|
692
|
-
metrics.addError(error);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
} else if (op.type === 'UPDATE') {
|
|
696
|
-
for (let i = 0; i < op.count; i++) {
|
|
697
|
-
const start = Date.now();
|
|
698
|
-
try {
|
|
699
|
-
db.prepare('UPDATE messages SET content = ? WHERE id = ?').run(
|
|
700
|
-
`Updated ${i}`,
|
|
701
|
-
(i % 100) + 1
|
|
702
|
-
);
|
|
703
|
-
metrics.addLatency(Date.now() - start);
|
|
704
|
-
} catch (error) {
|
|
705
|
-
metrics.addError(error);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
metrics.end();
|
|
712
|
-
db.close();
|
|
713
|
-
|
|
714
|
-
return metrics.getReport();
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* PostgreSQL Server Benchmark (remote real PostgreSQL)
|
|
719
|
-
*/
|
|
720
|
-
async function benchmarkPostgreSQL(scenario) {
|
|
721
|
-
console.log(' 🔷 Running PostgreSQL Server benchmark...');
|
|
722
|
-
|
|
723
|
-
let pool;
|
|
724
|
-
try {
|
|
725
|
-
pool = await openPgPool({ ...POSTGRES_CONFIG });
|
|
726
|
-
return await runCrudScenarioOnPool(pool, scenario);
|
|
727
|
-
} catch (error) {
|
|
728
|
-
console.error(' PostgreSQL benchmark skipped:', error.message);
|
|
729
|
-
return skippedCrud(error.message, 0);
|
|
730
|
-
} finally {
|
|
731
|
-
await pool?.end().catch(() => {});
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
/**
|
|
736
|
-
* pgserve 1.2.0 Benchmark (published npm package)
|
|
737
|
-
*/
|
|
738
|
-
async function benchmarkPgserveV1(scenario) {
|
|
739
|
-
console.log(` 🧭 Running ${LEGACY_PGSERVE_SPEC} benchmark...`);
|
|
740
|
-
|
|
741
|
-
let legacy;
|
|
742
|
-
let pool;
|
|
743
|
-
try {
|
|
744
|
-
legacy = await startLegacyPgserve({ port: 18431 });
|
|
745
|
-
pool = await openPgPool({ port: 18431, database: 'bench_test' });
|
|
746
|
-
return await runCrudScenarioOnPool(pool, scenario);
|
|
747
|
-
} catch (error) {
|
|
748
|
-
console.error(` ${LEGACY_PGSERVE_SPEC} benchmark skipped:`, error.message);
|
|
749
|
-
return skippedCrud(error.message);
|
|
750
|
-
} finally {
|
|
751
|
-
await pool?.end().catch(() => {});
|
|
752
|
-
await legacy?.stop().catch(() => {});
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
/**
|
|
757
|
-
* pgserve Benchmark (our solution - embedded PostgreSQL with TRUE concurrency)
|
|
758
|
-
* @param {Object} scenario - Benchmark scenario
|
|
759
|
-
* @param {boolean} useRam - Use /dev/shm RAM storage (Linux only)
|
|
760
|
-
*/
|
|
761
|
-
async function benchmarkPgserve(scenario, useRam = false) {
|
|
762
|
-
const mode = useRam ? 'RAM' : 'disk';
|
|
763
|
-
console.log(` 🚀 Running pgserve v2 (${mode}) benchmark...`);
|
|
764
|
-
|
|
765
|
-
let server;
|
|
766
|
-
let pool;
|
|
767
|
-
try {
|
|
768
|
-
// Start pgserve in memory mode (optionally with RAM storage)
|
|
769
|
-
const port = useRam ? 18433 : 18432;
|
|
770
|
-
server = await startMultiTenantServer({
|
|
771
|
-
port,
|
|
772
|
-
logLevel: 'error',
|
|
773
|
-
useRam
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
pool = await openPgPool({ port, database: 'bench_test' });
|
|
777
|
-
return await runCrudScenarioOnPool(pool, scenario);
|
|
778
|
-
} catch (error) {
|
|
779
|
-
console.error(` pgserve v2 (${mode}) benchmark failed:`, error.message);
|
|
780
|
-
return skippedCrud(error.message);
|
|
781
|
-
} finally {
|
|
782
|
-
await pool?.end().catch(() => {});
|
|
783
|
-
await server?.stop().catch(() => {});
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// ============================================================================
|
|
788
|
-
// VECTOR BENCHMARKS (pgvector)
|
|
789
|
-
// ============================================================================
|
|
790
|
-
|
|
791
|
-
/**
|
|
792
|
-
* PostgreSQL Server Vector Benchmark
|
|
793
|
-
* Supports both INSERT and SEARCH scenarios
|
|
794
|
-
*/
|
|
795
|
-
async function benchmarkPostgreSQLVector(scenario, embeddings, queryVectors, groundTruth) {
|
|
796
|
-
console.log(' 🔷 Running PostgreSQL vector benchmark...');
|
|
797
|
-
|
|
798
|
-
let pool;
|
|
799
|
-
try {
|
|
800
|
-
pool = await openPgPool({ ...POSTGRES_CONFIG });
|
|
801
|
-
return await runVectorScenarioOnPool(pool, scenario, embeddings, queryVectors, groundTruth);
|
|
802
|
-
} catch (error) {
|
|
803
|
-
console.error(' PostgreSQL vector benchmark skipped:', error.message);
|
|
804
|
-
return skippedVector(error.message, 0);
|
|
805
|
-
} finally {
|
|
806
|
-
await pool?.end().catch(() => {});
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
/**
|
|
811
|
-
* pgserve 1.2.0 Vector Benchmark (published npm package)
|
|
812
|
-
*/
|
|
813
|
-
async function benchmarkPgserveV1Vector(scenario, embeddings, queryVectors, groundTruth) {
|
|
814
|
-
console.log(` 🧭 Running ${LEGACY_PGSERVE_SPEC} vector benchmark...`);
|
|
815
|
-
|
|
816
|
-
let legacy;
|
|
817
|
-
let pool;
|
|
818
|
-
try {
|
|
819
|
-
legacy = await startLegacyPgserve({ port: 18434, enablePgvector: true });
|
|
820
|
-
pool = await openPgPool({ port: 18434, database: 'vector_bench' });
|
|
821
|
-
return await runVectorScenarioOnPool(pool, scenario, embeddings, queryVectors, groundTruth);
|
|
822
|
-
} catch (error) {
|
|
823
|
-
console.error(` ${LEGACY_PGSERVE_SPEC} vector benchmark skipped:`, error.message);
|
|
824
|
-
return skippedVector(error.message);
|
|
825
|
-
} finally {
|
|
826
|
-
await pool?.end().catch(() => {});
|
|
827
|
-
await legacy?.stop().catch(() => {});
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
/**
|
|
832
|
-
* pgserve Vector Benchmark
|
|
833
|
-
* Supports both INSERT and SEARCH scenarios
|
|
834
|
-
*/
|
|
835
|
-
async function benchmarkPgserveVector(scenario, embeddings, queryVectors, groundTruth, useRam = false) {
|
|
836
|
-
const mode = useRam ? 'RAM' : 'disk';
|
|
837
|
-
console.log(` 🚀 Running pgserve v2 (${mode}) vector benchmark...`);
|
|
838
|
-
|
|
839
|
-
let server;
|
|
840
|
-
let pool;
|
|
841
|
-
try {
|
|
842
|
-
// Start pgserve (use different ports for vector benchmarks to avoid conflicts)
|
|
843
|
-
const port = useRam ? 18436 : 18435;
|
|
844
|
-
server = await startMultiTenantServer({
|
|
845
|
-
port,
|
|
846
|
-
logLevel: 'error',
|
|
847
|
-
useRam,
|
|
848
|
-
enablePgvector: true
|
|
849
|
-
});
|
|
850
|
-
|
|
851
|
-
pool = await openPgPool({ port, database: 'vector_bench' });
|
|
852
|
-
return await runVectorScenarioOnPool(pool, scenario, embeddings, queryVectors, groundTruth);
|
|
853
|
-
} catch (error) {
|
|
854
|
-
console.error(` pgserve v2 (${mode}) vector benchmark failed:`, error.message);
|
|
855
|
-
return skippedVector(error.message, 0);
|
|
856
|
-
} finally {
|
|
857
|
-
await pool?.end().catch(() => {});
|
|
858
|
-
await server?.stop().catch(() => {});
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
/**
|
|
863
|
-
* Generate comparison report
|
|
864
|
-
*/
|
|
865
|
-
function generateReport(results, vectorResults = []) {
|
|
866
|
-
const report = {
|
|
867
|
-
timestamp: new Date().toISOString(),
|
|
868
|
-
scenarios: results,
|
|
869
|
-
vectorScenarios: vectorResults
|
|
870
|
-
};
|
|
871
|
-
|
|
872
|
-
// Save JSON
|
|
873
|
-
const jsonPath = path.join(RESULTS_DIR, 'benchmark-results.json');
|
|
874
|
-
fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2));
|
|
875
|
-
|
|
876
|
-
// Generate markdown
|
|
877
|
-
let md = '# Benchmark Results\n\n';
|
|
878
|
-
md += `**Date:** ${new Date().toLocaleString()}\n\n`;
|
|
879
|
-
md += '## Quick Start\n\n';
|
|
880
|
-
md += '```bash\n';
|
|
881
|
-
md += '# Zero install - just run!\n';
|
|
882
|
-
md += 'npx pgserve\n\n';
|
|
883
|
-
md += '# Connect from any PostgreSQL client\n';
|
|
884
|
-
md += 'psql postgresql://localhost:5432/mydb\n';
|
|
885
|
-
md += '```\n\n';
|
|
886
|
-
|
|
887
|
-
const metricValue = (data, key) => {
|
|
888
|
-
if (!data || data.skipped) return 'N/A';
|
|
889
|
-
const value = data[key];
|
|
890
|
-
return value === undefined || value === null ? 'N/A' : value;
|
|
891
|
-
};
|
|
892
|
-
const metricNumber = (data, key) => {
|
|
893
|
-
if (!data || data.skipped) return null;
|
|
894
|
-
const value = Number.parseFloat(data[key]);
|
|
895
|
-
return Number.isFinite(value) ? value : null;
|
|
896
|
-
};
|
|
897
|
-
const winnerName = (rows, key, direction) => {
|
|
898
|
-
let winner = null;
|
|
899
|
-
for (const row of rows) {
|
|
900
|
-
const value = metricNumber(row.data, key);
|
|
901
|
-
if (value === null) continue;
|
|
902
|
-
if (!winner || (direction === 'max' ? value > winner.value : value < winner.value)) {
|
|
903
|
-
winner = { name: row.name, value };
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
return winner?.name || 'N/A';
|
|
907
|
-
};
|
|
908
|
-
const pctDelta = (current, baseline) => {
|
|
909
|
-
if (!current || !baseline || current.skipped || baseline.skipped || baseline.throughput <= 0) return null;
|
|
910
|
-
return ((current.throughput / baseline.throughput - 1) * 100).toFixed(1);
|
|
911
|
-
};
|
|
912
|
-
const renderMetricTable = (rows, metrics) => {
|
|
913
|
-
let table = `| Metric | ${rows.map(r => r.name).join(' | ')} | Winner |\n`;
|
|
914
|
-
table += `| --- | ${rows.map(() => '---:').join(' | ')} | --- |\n`;
|
|
915
|
-
for (const metric of metrics) {
|
|
916
|
-
table += `| ${metric.label} | ${rows.map(r => metric.format(metricValue(r.data, metric.key))).join(' | ')} | ${winnerName(rows, metric.key, metric.direction)} |\n`;
|
|
917
|
-
}
|
|
918
|
-
return `${table}\n`;
|
|
919
|
-
};
|
|
920
|
-
const plain = (value) => String(value);
|
|
921
|
-
const percent = (value) => value === 'N/A' ? value : `${value}%`;
|
|
922
|
-
|
|
923
|
-
for (const scenario of results) {
|
|
924
|
-
md += `## ${scenario.name}\n\n`;
|
|
925
|
-
md += `${scenario.description}\n\n`;
|
|
926
|
-
|
|
927
|
-
const rows = [
|
|
928
|
-
{ name: 'SQLite', data: scenario.sqlite },
|
|
929
|
-
{ name: 'PostgreSQL', data: scenario.postgres },
|
|
930
|
-
{ name: 'pgserve 1.2.0', data: scenario.pgserveV1 },
|
|
931
|
-
{ name: 'pgserve v2', data: scenario.pgserve },
|
|
932
|
-
];
|
|
933
|
-
if (scenario.pgserveRam && !scenario.pgserveRam.skipped) {
|
|
934
|
-
rows.push({ name: 'pgserve v2 RAM', data: scenario.pgserveRam });
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
md += renderMetricTable(rows, [
|
|
938
|
-
{ label: 'Throughput (qps)', key: 'throughput', direction: 'max', format: plain },
|
|
939
|
-
{ label: 'P50 latency (ms)', key: 'p50', direction: 'min', format: plain },
|
|
940
|
-
{ label: 'P99 latency (ms)', key: 'p99', direction: 'min', format: plain },
|
|
941
|
-
{ label: 'Errors', key: 'errors', direction: 'min', format: plain },
|
|
942
|
-
]);
|
|
943
|
-
|
|
944
|
-
const delta = pctDelta(scenario.pgserve, scenario.pgserveV1);
|
|
945
|
-
if (delta !== null) {
|
|
946
|
-
md += `**pgserve v2 vs 1.2.0:** ${delta}% throughput delta.\n\n`;
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
// Vector benchmark results (with Recall@k)
|
|
951
|
-
if (vectorResults && vectorResults.length > 0) {
|
|
952
|
-
md += '---\n\n';
|
|
953
|
-
md += '## Vector Benchmarks (pgvector) - Recall@k Methodology\n\n';
|
|
954
|
-
md += '*Following industry-standard ANN-Benchmarks methodology: comparing approximate results to brute-force ground truth.*\n\n';
|
|
955
|
-
|
|
956
|
-
for (const scenario of vectorResults) {
|
|
957
|
-
md += `### ${scenario.name}\n\n`;
|
|
958
|
-
md += `${scenario.description}\n\n`;
|
|
959
|
-
|
|
960
|
-
const rows = [
|
|
961
|
-
{ name: 'PostgreSQL', data: scenario.postgres },
|
|
962
|
-
{ name: 'pgserve 1.2.0', data: scenario.pgserveV1 },
|
|
963
|
-
{ name: 'pgserve v2', data: scenario.pgserve },
|
|
964
|
-
];
|
|
965
|
-
if (scenario.pgserveRam && !scenario.pgserveRam.skipped) {
|
|
966
|
-
rows.push({ name: 'pgserve v2 RAM', data: scenario.pgserveRam });
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
md += renderMetricTable(rows, [
|
|
970
|
-
{ label: `Recall@${scenario.k || 10}`, key: 'recall', direction: 'max', format: percent },
|
|
971
|
-
{ label: 'Throughput (qps)', key: 'throughput', direction: 'max', format: plain },
|
|
972
|
-
{ label: 'P50 latency (ms)', key: 'p50', direction: 'min', format: plain },
|
|
973
|
-
{ label: 'P99 latency (ms)', key: 'p99', direction: 'min', format: plain },
|
|
974
|
-
{ label: 'Errors', key: 'errors', direction: 'min', format: plain },
|
|
975
|
-
]);
|
|
976
|
-
|
|
977
|
-
// Find winner among non-skipped (considering both recall and throughput)
|
|
978
|
-
const candidates = {};
|
|
979
|
-
if (!scenario.postgres.skipped) candidates.postgres = { recall: parseFloat(scenario.postgres.recall), qps: scenario.postgres.throughput };
|
|
980
|
-
if (!scenario.pgserveV1.skipped) candidates.pgserveV1 = { recall: parseFloat(scenario.pgserveV1.recall), qps: scenario.pgserveV1.throughput };
|
|
981
|
-
if (!scenario.pgserve.skipped) candidates.pgserve = { recall: parseFloat(scenario.pgserve.recall), qps: scenario.pgserve.throughput };
|
|
982
|
-
if (scenario.pgserveRam && !scenario.pgserveRam.skipped) candidates.pgserveRam = { recall: parseFloat(scenario.pgserveRam.recall), qps: scenario.pgserveRam.throughput };
|
|
983
|
-
|
|
984
|
-
if (Object.keys(candidates).length > 0) {
|
|
985
|
-
const nameMap = { postgres: 'PostgreSQL', pgserveV1: 'pgserve 1.2.0', pgserve: 'pgserve v2', pgserveRam: 'pgserve v2 RAM' };
|
|
986
|
-
// Winner = highest QPS among those with 100% recall, otherwise highest recall
|
|
987
|
-
const perfect = Object.entries(candidates).filter(([, v]) => v.recall === 100);
|
|
988
|
-
let winnerKey;
|
|
989
|
-
if (perfect.length > 0) {
|
|
990
|
-
winnerKey = perfect.reduce((a, b) => a[1].qps > b[1].qps ? a : b)[0];
|
|
991
|
-
md += `**${nameMap[winnerKey]} wins** (100% recall @ ${candidates[winnerKey].qps} qps)\n\n`;
|
|
992
|
-
} else {
|
|
993
|
-
winnerKey = Object.entries(candidates).reduce((a, b) => a[1].recall > b[1].recall ? a : b)[0];
|
|
994
|
-
md += `**${nameMap[winnerKey]} wins** (${candidates[winnerKey].recall}% recall @ ${candidates[winnerKey].qps} qps)\n\n`;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
const delta = pctDelta(scenario.pgserve, scenario.pgserveV1);
|
|
999
|
-
if (delta !== null) {
|
|
1000
|
-
md += `**pgserve v2 vs 1.2.0:** ${delta}% throughput delta.\n\n`;
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
md += '---\n\n';
|
|
1006
|
-
md += '## Why pgserve?\n\n';
|
|
1007
|
-
md += '- **TRUE Concurrency**: Native PostgreSQL process forking\n';
|
|
1008
|
-
md += '- **RAM Mode**: `--ram` flag for /dev/shm storage (Linux)\n';
|
|
1009
|
-
md += '- **Zero Config**: Just run `npx pgserve`\n';
|
|
1010
|
-
md += '- **Auto-Provision**: Databases created on first connection\n';
|
|
1011
|
-
md += '- **PostgreSQL 17.7**: Latest stable, native binaries\n';
|
|
1012
|
-
|
|
1013
|
-
const mdPath = path.join(RESULTS_DIR, 'benchmark-results.md');
|
|
1014
|
-
fs.writeFileSync(mdPath, md);
|
|
1015
|
-
|
|
1016
|
-
console.log(`\nResults saved to:`);
|
|
1017
|
-
console.log(` JSON: ${jsonPath}`);
|
|
1018
|
-
console.log(` Markdown: ${mdPath}\n`);
|
|
1019
|
-
|
|
1020
|
-
return report;
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
/**
|
|
1024
|
-
* Main runner
|
|
1025
|
-
*/
|
|
1026
|
-
async function main() {
|
|
1027
|
-
// Parse CLI args
|
|
1028
|
-
const args = process.argv.slice(2);
|
|
1029
|
-
const includeVector = args.includes('--include-vector') || args.includes('--vector');
|
|
1030
|
-
const vectorOnly = args.includes('--vector-only');
|
|
1031
|
-
|
|
1032
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
1033
|
-
console.log(`
|
|
1034
|
-
pgserve Benchmark Suite
|
|
1035
|
-
|
|
1036
|
-
Usage:
|
|
1037
|
-
bun tests/benchmarks/runner.js [options]
|
|
1038
|
-
|
|
1039
|
-
Options:
|
|
1040
|
-
--include-vector Include vector (pgvector) benchmarks
|
|
1041
|
-
--vector-only Run only vector benchmarks
|
|
1042
|
-
--help, -h Show this help
|
|
1043
|
-
|
|
1044
|
-
Vector benchmarks require:
|
|
1045
|
-
- PostgreSQL: Built-in pgvector support
|
|
1046
|
-
- PostgreSQL: Docker image pgvector/pgvector:pg17
|
|
1047
|
-
- pgserve 1.2.0 and v2: --pgvector support
|
|
1048
|
-
`);
|
|
1049
|
-
process.exit(0);
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
// Print banner
|
|
1053
|
-
banner();
|
|
1054
|
-
if (includeVector || vectorOnly) {
|
|
1055
|
-
console.log(`${C.dim} + Vector benchmarks (pgvector) enabled${C.reset}\n`);
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
// Ensure results directory exists
|
|
1059
|
-
if (!fs.existsSync(RESULTS_DIR)) {
|
|
1060
|
-
fs.mkdirSync(RESULTS_DIR, { recursive: true });
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
const results = [];
|
|
1064
|
-
const vectorResults = [];
|
|
1065
|
-
|
|
1066
|
-
// Check if RAM mode is available (Linux only with /dev/shm)
|
|
1067
|
-
const canUseRam = os.platform() === 'linux' && fs.existsSync('/dev/shm');
|
|
1068
|
-
if (canUseRam) {
|
|
1069
|
-
console.log(`${C.green}💾 RAM mode available (/dev/shm detected)${C.reset}\n`);
|
|
1070
|
-
} else {
|
|
1071
|
-
console.log(`${C.yellow}⚠️ RAM mode not available (Linux /dev/shm required)${C.reset}\n`);
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
// Run CRUD benchmarks (unless --vector-only)
|
|
1075
|
-
if (!vectorOnly) {
|
|
1076
|
-
for (const scenario of scenarios) {
|
|
1077
|
-
section(scenario.name, scenario.description);
|
|
1078
|
-
|
|
1079
|
-
const sqlite = await benchmarkSQLite(scenario);
|
|
1080
|
-
const postgres = await benchmarkPostgreSQL(scenario);
|
|
1081
|
-
const pgserveV1 = await benchmarkPgserveV1(scenario);
|
|
1082
|
-
const pgserve = await benchmarkPgserve(scenario, false); // disk mode
|
|
1083
|
-
const pgserveRam = canUseRam
|
|
1084
|
-
? await benchmarkPgserve(scenario, true) // RAM mode
|
|
1085
|
-
: { throughput: 0, p50: 0, p99: 0, errors: 0, lockTimeouts: 0, totalOps: 0, skipped: true };
|
|
1086
|
-
|
|
1087
|
-
results.push({
|
|
1088
|
-
name: scenario.name,
|
|
1089
|
-
description: scenario.description,
|
|
1090
|
-
sqlite,
|
|
1091
|
-
postgres,
|
|
1092
|
-
pgserveV1,
|
|
1093
|
-
pgserve,
|
|
1094
|
-
pgserveRam
|
|
1095
|
-
});
|
|
1096
|
-
|
|
1097
|
-
// Scenario results summary
|
|
1098
|
-
console.log(`\n ${C.bold}Results:${C.reset}`);
|
|
1099
|
-
console.log(` ${C.dim}──────────────────────────────────────────${C.reset}`);
|
|
1100
|
-
console.log(` ${C.yellow}🔸${C.reset} SQLite: ${C.bold}${sqlite.throughput}${C.reset} qps, P50=${sqlite.p50}ms, P99=${sqlite.p99}ms`);
|
|
1101
|
-
console.log(` ${C.cyan}🔷${C.reset} PostgreSQL: ${postgres.skipped ? `${C.dim}SKIPPED${C.reset}` : `${C.bold}${postgres.throughput}${C.reset} qps, P50=${postgres.p50}ms, P99=${postgres.p99}ms`}`);
|
|
1102
|
-
console.log(` ${C.blue}🧭${C.reset} pgserve 1.2.0: ${pgserveV1.skipped ? `${C.dim}SKIPPED${C.reset}` : `${C.bold}${pgserveV1.throughput}${C.reset} qps, P50=${pgserveV1.p50}ms, P99=${pgserveV1.p99}ms`}`);
|
|
1103
|
-
console.log(` ${C.green}🚀${C.reset} pgserve v2: ${C.bold}${pgserve.throughput}${C.reset} qps, P50=${pgserve.p50}ms, P99=${pgserve.p99}ms`);
|
|
1104
|
-
if (canUseRam) {
|
|
1105
|
-
console.log(` ${C.magenta}⚡${C.reset} pgserve v2 RAM: ${C.bold}${pgserveRam.throughput}${C.reset} qps, P50=${pgserveRam.p50}ms, P99=${pgserveRam.p99}ms`);
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
// Run vector benchmarks (if --include-vector or --vector-only)
|
|
1111
|
-
if (includeVector || vectorOnly) {
|
|
1112
|
-
console.log(`\n${C.cyan}${C.bold}
|
|
1113
|
-
╔════════════════════════════════════════════════════════════════╗
|
|
1114
|
-
║ Vector Benchmarks (pgvector) - Recall@k Methodology ║
|
|
1115
|
-
╚════════════════════════════════════════════════════════════════╝${C.reset}\n`);
|
|
1116
|
-
|
|
1117
|
-
// Load or generate embeddings
|
|
1118
|
-
console.log(`${C.dim}📦 Loading embeddings...${C.reset}`);
|
|
1119
|
-
const dimension = 1536;
|
|
1120
|
-
const corpusSize = 10000; // 10k vectors (~60MB) to exceed buffer cache and show RAM vs disk difference
|
|
1121
|
-
|
|
1122
|
-
// Ensure embeddings file exists
|
|
1123
|
-
getEmbeddingsPath(corpusSize, dimension);
|
|
1124
|
-
const embeddings = loadEmbeddings(`embeddings-${corpusSize}-${dimension}.json`);
|
|
1125
|
-
const queryVectors = generateQueryVectors(100, dimension);
|
|
1126
|
-
console.log(`${C.dim} Loaded ${embeddings.vectors.length} corpus vectors, ${queryVectors.length} query vectors${C.reset}\n`);
|
|
1127
|
-
|
|
1128
|
-
for (const scenario of vectorScenarios) {
|
|
1129
|
-
section(`Vector: ${scenario.name}`, scenario.description);
|
|
1130
|
-
|
|
1131
|
-
let groundTruth = null;
|
|
1132
|
-
|
|
1133
|
-
// Only compute ground truth for SEARCH scenarios
|
|
1134
|
-
if (scenario.type === 'SEARCH') {
|
|
1135
|
-
console.log(`${C.dim} 📐 Computing ground truth (brute-force k=${scenario.k})...${C.reset}`);
|
|
1136
|
-
groundTruth = getGroundTruth(
|
|
1137
|
-
embeddings.vectors,
|
|
1138
|
-
queryVectors.slice(0, scenario.queryCount),
|
|
1139
|
-
scenario.k,
|
|
1140
|
-
`corpus-${corpusSize}-dim-${dimension}-queries-${scenario.queryCount}`
|
|
1141
|
-
);
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
// Run benchmarks
|
|
1145
|
-
const postgres = await benchmarkPostgreSQLVector(scenario, embeddings, queryVectors, groundTruth);
|
|
1146
|
-
const pgserveV1 = await benchmarkPgserveV1Vector(scenario, embeddings, queryVectors, groundTruth);
|
|
1147
|
-
const pgserve = await benchmarkPgserveVector(scenario, embeddings, queryVectors, groundTruth, false);
|
|
1148
|
-
const pgserveRam = canUseRam
|
|
1149
|
-
? await benchmarkPgserveVector(scenario, embeddings, queryVectors, groundTruth, true)
|
|
1150
|
-
: { throughput: 0, recall: 'N/A', p50: 0, p99: 0, errors: 0, totalOps: 0, skipped: true };
|
|
1151
|
-
|
|
1152
|
-
vectorResults.push({
|
|
1153
|
-
name: scenario.name,
|
|
1154
|
-
description: scenario.description,
|
|
1155
|
-
type: scenario.type,
|
|
1156
|
-
k: scenario.k,
|
|
1157
|
-
postgres,
|
|
1158
|
-
pgserveV1,
|
|
1159
|
-
pgserve,
|
|
1160
|
-
pgserveRam
|
|
1161
|
-
});
|
|
1162
|
-
|
|
1163
|
-
// Vector scenario results summary
|
|
1164
|
-
const isInsert = scenario.type === 'INSERT';
|
|
1165
|
-
console.log(`\n ${C.bold}Results (${isInsert ? 'INSERT QPS' : `Recall@${scenario.k} + QPS`}):${C.reset}`);
|
|
1166
|
-
console.log(` ${C.dim}──────────────────────────────────────────────────────${C.reset}`);
|
|
1167
|
-
const formatResult = (r, icon, color, name) => {
|
|
1168
|
-
if (r.skipped) return ` ${color}${icon}${C.reset} ${name.padEnd(14)} ${C.dim}SKIPPED${r.reason ? ` (${r.reason})` : ''}${C.reset}`;
|
|
1169
|
-
if (isInsert) {
|
|
1170
|
-
return ` ${color}${icon}${C.reset} ${name.padEnd(14)} ${C.bold}${r.throughput}${C.reset} inserts/sec, P50=${r.p50}ms, P99=${r.p99}ms`;
|
|
1171
|
-
}
|
|
1172
|
-
return ` ${color}${icon}${C.reset} ${name.padEnd(14)} Recall: ${C.bold}${r.recall}%${C.reset}, ${C.bold}${r.throughput}${C.reset} qps, P50=${r.p50}ms`;
|
|
1173
|
-
};
|
|
1174
|
-
console.log(formatResult(postgres, '🔷', C.cyan, 'PostgreSQL:'));
|
|
1175
|
-
console.log(formatResult(pgserveV1, '🧭', C.blue, 'pgserve 1.2.0:'));
|
|
1176
|
-
console.log(formatResult(pgserve, '🚀', C.green, 'pgserve v2:'));
|
|
1177
|
-
if (canUseRam) {
|
|
1178
|
-
console.log(formatResult(pgserveRam, '⚡', C.magenta, 'pgserve v2 RAM:'));
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// Save detailed JSON report
|
|
1184
|
-
generateReport(results, vectorResults);
|
|
1185
|
-
|
|
1186
|
-
// Print final results table with scores
|
|
1187
|
-
printFinalResults(results, vectorResults, canUseRam);
|
|
1188
|
-
|
|
1189
|
-
console.log(`${C.cyan}${C.bold}╔════════════════════════════════════════════════════════════════╗
|
|
1190
|
-
║ Benchmarks Complete! ║
|
|
1191
|
-
║ ║
|
|
1192
|
-
║ Try it yourself: npx pgserve ║
|
|
1193
|
-
╚════════════════════════════════════════════════════════════════╝${C.reset}
|
|
1194
|
-
`);
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
main().catch(console.error);
|