pgserve 2.1.3 → 2.2.1

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.
Files changed (235) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +105 -1
  3. package/bin/autopg-wrapper.cjs +16 -0
  4. package/bin/pgserve-wrapper.cjs +32 -6
  5. package/bin/postgres-server.js +56 -0
  6. package/console/README.md +131 -0
  7. package/console/api.js +173 -0
  8. package/console/app.jsx +483 -0
  9. package/console/colors_and_type.css +227 -0
  10. package/console/components.jsx +167 -0
  11. package/console/console.css +1666 -0
  12. package/console/data.jsx +350 -0
  13. package/console/index.html +31 -0
  14. package/console/screens/databases.jsx +5 -0
  15. package/console/screens/health.jsx +5 -0
  16. package/console/screens/ingress.jsx +5 -0
  17. package/console/screens/optimizer.jsx +5 -0
  18. package/console/screens/rlm-sim.jsx +5 -0
  19. package/console/screens/rlm-trace.jsx +5 -0
  20. package/console/screens/security.jsx +5 -0
  21. package/console/screens/settings.jsx +611 -0
  22. package/console/screens/sql.jsx +5 -0
  23. package/console/screens/sync.jsx +5 -0
  24. package/console/screens/tables.jsx +5 -0
  25. package/console/tweaks-panel.jsx +425 -0
  26. package/package.json +14 -2
  27. package/scripts/postinstall.cjs +60 -0
  28. package/src/cli-config.cjs +310 -0
  29. package/src/cli-install.cjs +112 -11
  30. package/src/cli-restart.cjs +228 -0
  31. package/src/cli-ui.cjs +580 -0
  32. package/src/cluster.js +43 -38
  33. package/src/postgres.js +141 -19
  34. package/src/settings-loader.cjs +235 -0
  35. package/src/settings-migrate.cjs +212 -0
  36. package/src/settings-pg-args.cjs +146 -0
  37. package/src/settings-schema.cjs +422 -0
  38. package/src/settings-validator.cjs +416 -0
  39. package/src/settings-writer.cjs +288 -0
  40. package/src/upgrade/index.js +65 -0
  41. package/src/upgrade/runner.js +23 -0
  42. package/src/upgrade/steps/binary-cache-flush.js +67 -0
  43. package/src/upgrade/steps/consumer-signal.js +40 -0
  44. package/src/upgrade/steps/env-refresh.js +89 -0
  45. package/src/upgrade/steps/health-validate.js +53 -0
  46. package/src/upgrade/steps/plpgsql-resolve.js +66 -0
  47. package/src/upgrade/steps/port-reconcile.js +52 -0
  48. package/.claude/context/windows-debug.md +0 -119
  49. package/.genie/AGENTS.md +0 -15
  50. package/.genie/agents/README.md +0 -110
  51. package/.genie/agents/analyze.md +0 -176
  52. package/.genie/agents/forge.md +0 -290
  53. package/.genie/agents/garbage-cleaner.md +0 -324
  54. package/.genie/agents/garbage-collector.md +0 -596
  55. package/.genie/agents/github-issue-gc.md +0 -618
  56. package/.genie/agents/review.md +0 -380
  57. package/.genie/agents/semantic-analyzer/find-duplicates.md +0 -90
  58. package/.genie/agents/semantic-analyzer/find-orphans.md +0 -99
  59. package/.genie/agents/semantic-analyzer.md +0 -101
  60. package/.genie/agents/update.md +0 -182
  61. package/.genie/agents/wish.md +0 -357
  62. package/.genie/brainstorms/pgserve-v2/DESIGN.md +0 -174
  63. package/.genie/code/AGENTS.md +0 -694
  64. package/.genie/code/agents/audit/risk.md +0 -173
  65. package/.genie/code/agents/audit/security.md +0 -189
  66. package/.genie/code/agents/audit.md +0 -145
  67. package/.genie/code/agents/challenge.md +0 -230
  68. package/.genie/code/agents/change-reviewer.md +0 -295
  69. package/.genie/code/agents/code-garbage-collector.md +0 -425
  70. package/.genie/code/agents/code-quality.md +0 -410
  71. package/.genie/code/agents/commit-suggester.md +0 -255
  72. package/.genie/code/agents/commit.md +0 -124
  73. package/.genie/code/agents/consensus.md +0 -204
  74. package/.genie/code/agents/daily-standup.md +0 -722
  75. package/.genie/code/agents/docgen.md +0 -48
  76. package/.genie/code/agents/explore.md +0 -79
  77. package/.genie/code/agents/fix.md +0 -100
  78. package/.genie/code/agents/git/commit-advisory.md +0 -219
  79. package/.genie/code/agents/git/workflows/issue.md +0 -244
  80. package/.genie/code/agents/git/workflows/pr.md +0 -179
  81. package/.genie/code/agents/git/workflows/release.md +0 -460
  82. package/.genie/code/agents/git/workflows/report.md +0 -342
  83. package/.genie/code/agents/git.md +0 -432
  84. package/.genie/code/agents/implementor.md +0 -161
  85. package/.genie/code/agents/install.md +0 -515
  86. package/.genie/code/agents/issue-creator.md +0 -344
  87. package/.genie/code/agents/polish.md +0 -116
  88. package/.genie/code/agents/qa.md +0 -653
  89. package/.genie/code/agents/refactor.md +0 -294
  90. package/.genie/code/agents/release.md +0 -1129
  91. package/.genie/code/agents/roadmap.md +0 -885
  92. package/.genie/code/agents/tests.md +0 -557
  93. package/.genie/code/agents/tracer.md +0 -50
  94. package/.genie/code/agents/update/upstream-update.md +0 -85
  95. package/.genie/code/agents/update/versions/generic-update.md +0 -305
  96. package/.genie/code/agents/vibe.md +0 -1317
  97. package/.genie/code/spells/agent-configuration.md +0 -58
  98. package/.genie/code/spells/automated-rc-publishing.md +0 -106
  99. package/.genie/code/spells/branch-tracker-guidance.md +0 -28
  100. package/.genie/code/spells/debug.md +0 -320
  101. package/.genie/code/spells/emoji-naming-convention.md +0 -303
  102. package/.genie/code/spells/evidence-storage.md +0 -26
  103. package/.genie/code/spells/file-naming-rules.md +0 -35
  104. package/.genie/code/spells/forge-code-blueprints.md +0 -195
  105. package/.genie/code/spells/genie-integration.md +0 -153
  106. package/.genie/code/spells/publishing-protocol.md +0 -61
  107. package/.genie/code/spells/team-consultation-protocol.md +0 -284
  108. package/.genie/code/spells/tool-requirements.md +0 -20
  109. package/.genie/code/spells/triad-maintenance-protocol.md +0 -154
  110. package/.genie/code/teams/tech-council/council.md +0 -328
  111. package/.genie/code/teams/tech-council/jt.md +0 -352
  112. package/.genie/code/teams/tech-council/nayr.md +0 -305
  113. package/.genie/code/teams/tech-council/oettam.md +0 -375
  114. package/.genie/neurons/README.md +0 -193
  115. package/.genie/neurons/forge.md +0 -106
  116. package/.genie/neurons/genie.md +0 -63
  117. package/.genie/neurons/review.md +0 -106
  118. package/.genie/neurons/wish.md +0 -104
  119. package/.genie/product/README.md +0 -20
  120. package/.genie/product/cli-automation.md +0 -359
  121. package/.genie/product/environment.md +0 -60
  122. package/.genie/product/mission.md +0 -60
  123. package/.genie/product/roadmap.md +0 -44
  124. package/.genie/product/tech-stack.md +0 -34
  125. package/.genie/product/templates/context-template.md +0 -218
  126. package/.genie/product/templates/qa-done-report-template.md +0 -68
  127. package/.genie/product/templates/review-report-template.md +0 -89
  128. package/.genie/product/templates/wish-template.md +0 -120
  129. package/.genie/scripts/helpers/analyze-commit.js +0 -195
  130. package/.genie/scripts/helpers/bullet-counter.js +0 -194
  131. package/.genie/scripts/helpers/bullet-find.js +0 -289
  132. package/.genie/scripts/helpers/bullet-id.js +0 -244
  133. package/.genie/scripts/helpers/check-secrets.js +0 -237
  134. package/.genie/scripts/helpers/count-tokens.js +0 -200
  135. package/.genie/scripts/helpers/create-frontmatter.js +0 -456
  136. package/.genie/scripts/helpers/detect-markers.js +0 -293
  137. package/.genie/scripts/helpers/detect-todos.js +0 -267
  138. package/.genie/scripts/helpers/detect-unlabeled-blocks.js +0 -135
  139. package/.genie/scripts/helpers/embeddings.js +0 -344
  140. package/.genie/scripts/helpers/find-empty-sections.js +0 -158
  141. package/.genie/scripts/helpers/index.js +0 -319
  142. package/.genie/scripts/helpers/validate-frontmatter.js +0 -578
  143. package/.genie/scripts/helpers/validate-links.js +0 -207
  144. package/.genie/scripts/helpers/validate-paths.js +0 -373
  145. package/.genie/spells/README.md +0 -9
  146. package/.genie/spells/ace-protocol.md +0 -118
  147. package/.genie/spells/ask-one-at-a-time.md +0 -175
  148. package/.genie/spells/backup-analyzer.md +0 -542
  149. package/.genie/spells/blocker.md +0 -12
  150. package/.genie/spells/break-things-move-fast.md +0 -56
  151. package/.genie/spells/context-candidates.md +0 -72
  152. package/.genie/spells/context-critic.md +0 -51
  153. package/.genie/spells/defer-to-expertise.md +0 -278
  154. package/.genie/spells/delegate-dont-do.md +0 -292
  155. package/.genie/spells/error-investigation-protocol.md +0 -328
  156. package/.genie/spells/evidence-based-completion.md +0 -273
  157. package/.genie/spells/experiment.md +0 -65
  158. package/.genie/spells/file-creation-protocol.md +0 -229
  159. package/.genie/spells/forge-integration.md +0 -281
  160. package/.genie/spells/forge-orchestration.md +0 -514
  161. package/.genie/spells/gather-context.md +0 -18
  162. package/.genie/spells/global-health-check.md +0 -34
  163. package/.genie/spells/global-noop-roundtrip.md +0 -25
  164. package/.genie/spells/install-genie.md +0 -1232
  165. package/.genie/spells/install.md +0 -82
  166. package/.genie/spells/investigate-before-commit.md +0 -112
  167. package/.genie/spells/know-yourself.md +0 -288
  168. package/.genie/spells/learn.md +0 -828
  169. package/.genie/spells/mcp-diagnostic-protocol.md +0 -246
  170. package/.genie/spells/mcp-first.md +0 -124
  171. package/.genie/spells/multi-step-execution.md +0 -67
  172. package/.genie/spells/orchestration-boundary-protocol.md +0 -256
  173. package/.genie/spells/orchestrator-not-implementor.md +0 -189
  174. package/.genie/spells/prompt.md +0 -746
  175. package/.genie/spells/reflect.md +0 -404
  176. package/.genie/spells/routing-decision-matrix.md +0 -368
  177. package/.genie/spells/run-in-parallel.md +0 -12
  178. package/.genie/spells/session-state-updater-example.md +0 -196
  179. package/.genie/spells/session-state-updater.md +0 -220
  180. package/.genie/spells/track-long-running-tasks.md +0 -133
  181. package/.genie/spells/troubleshoot-infrastructure.md +0 -176
  182. package/.genie/spells/upgrade-genie.md +0 -415
  183. package/.genie/spells/url-presentation-protocol.md +0 -301
  184. package/.genie/spells/wish-initiation.md +0 -158
  185. package/.genie/spells/wish-issue-linkage.md +0 -410
  186. package/.genie/spells/wish-lifecycle.md +0 -100
  187. package/.genie/state/provider-status.json +0 -3
  188. package/.genie/state/version.json +0 -16
  189. package/.genie/wishes/canonical-pgserve-pm2-supervision/WISH.md +0 -290
  190. package/.genie/wishes/pgserve-v2/BRIEF-from-genie-pgserve.md +0 -99
  191. package/.genie/wishes/pgserve-v2/WISH.md +0 -442
  192. package/.genie/wishes/release-system-genie-pattern/WISH.md +0 -268
  193. package/.genie/wishes/release-system-genie-pattern/validation.md +0 -205
  194. package/.gitguardian.yaml +0 -29
  195. package/.gitguardianignore +0 -16
  196. package/.github/workflows/ci.yml +0 -122
  197. package/.github/workflows/release.yml +0 -289
  198. package/.github/workflows/version.yml +0 -228
  199. package/.husky/pre-commit +0 -2
  200. package/AGENTS.md +0 -433
  201. package/CLAUDE.md +0 -1
  202. package/Makefile +0 -285
  203. package/assets/icon.ico +0 -0
  204. package/bun.lock +0 -435
  205. package/bunfig.toml +0 -28
  206. package/ecosystem.config.cjs +0 -23
  207. package/eslint.config.js +0 -63
  208. package/examples/multi-tenant-demo.js +0 -104
  209. package/install.sh +0 -123
  210. package/knip.json +0 -9
  211. package/tests/audit.test.js +0 -189
  212. package/tests/backpressure.test.js +0 -167
  213. package/tests/benchmarks/runner.js +0 -1197
  214. package/tests/benchmarks/vector-generator.js +0 -368
  215. package/tests/cli-install.test.js +0 -322
  216. package/tests/control-db.test.js +0 -285
  217. package/tests/daemon-args.test.js +0 -86
  218. package/tests/daemon-control.test.js +0 -171
  219. package/tests/daemon-fingerprint-integration.test.js +0 -111
  220. package/tests/daemon-pr24-regression.test.js +0 -198
  221. package/tests/fingerprint.test.js +0 -263
  222. package/tests/fixtures/240-orphan-seed.sql +0 -30
  223. package/tests/multi-tenant.test.js +0 -374
  224. package/tests/orphan-cleanup.test.js +0 -390
  225. package/tests/pg-version-regex.test.js +0 -129
  226. package/tests/quick-bench.js +0 -135
  227. package/tests/router-handshake-retry.test.js +0 -119
  228. package/tests/router-handshake-watchdog.test.js +0 -110
  229. package/tests/sdk.test.js +0 -71
  230. package/tests/stale-postmaster-pid.test.js +0 -85
  231. package/tests/stress-test.js +0 -439
  232. package/tests/sync-perf-test.js +0 -150
  233. package/tests/tcp-listen.test.js +0 -368
  234. package/tests/tenancy.test.js +0 -403
  235. 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);