pgserve 0.1.4 → 1.0.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.
- package/.claude/settings.local.json +11 -0
- package/README.md +281 -256
- package/bin/pglite-server.js +212 -395
- package/package.json +13 -10
- package/src/cluster.js +322 -0
- package/src/index.js +8 -171
- package/src/postgres.js +479 -0
- package/src/protocol.js +49 -10
- package/src/router.js +117 -114
- package/src/sync.js +344 -0
- package/tests/benchmarks/runner.js +300 -155
- package/tests/sync-perf-test.js +150 -0
- package/src/detector.js +0 -105
- package/src/pool.js +0 -320
- package/src/ports.js +0 -114
- package/src/registry.js +0 -134
- package/src/server.js +0 -265
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Benchmark Runner
|
|
5
|
-
* Compares SQLite, PGlite, and
|
|
5
|
+
* Compares SQLite, PGlite, PostgreSQL Server, and pgserve performance
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import Database from 'better-sqlite3';
|
|
9
9
|
import { PGlite } from '@electric-sql/pglite';
|
|
10
|
-
import {
|
|
11
|
-
import { execSync } from 'child_process';
|
|
10
|
+
import { startMultiTenantServer } from '../../src/index.js';
|
|
12
11
|
import fs from 'fs';
|
|
13
12
|
import path from 'path';
|
|
14
13
|
import pg from 'pg';
|
|
@@ -17,38 +16,41 @@ const { Pool } = pg;
|
|
|
17
16
|
|
|
18
17
|
// Global error handlers (suppress expected PGlite WASM ExitStatus errors)
|
|
19
18
|
process.on('unhandledRejection', (reason, promise) => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
console.error('❌ Unhandled Promise Rejection:', reason);
|
|
19
|
+
if (reason && reason.name === 'ExitStatus') return;
|
|
20
|
+
console.error('Unhandled Promise Rejection:', reason);
|
|
25
21
|
});
|
|
26
22
|
|
|
27
23
|
process.on('uncaughtException', (error) => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
console.error('❌ Uncaught Exception:', error);
|
|
24
|
+
if (error && error.name === 'ExitStatus') return;
|
|
25
|
+
console.error('Uncaught Exception:', error);
|
|
33
26
|
process.exit(1);
|
|
34
27
|
});
|
|
35
28
|
|
|
36
29
|
const RESULTS_DIR = new URL('./results', import.meta.url).pathname;
|
|
37
30
|
|
|
31
|
+
// PostgreSQL Server configuration (Docker with tmpfs for fair RAM-to-RAM comparison)
|
|
32
|
+
const POSTGRES_CONFIG = {
|
|
33
|
+
host: 'localhost',
|
|
34
|
+
port: 15432,
|
|
35
|
+
user: 'postgres',
|
|
36
|
+
password: 'benchpass',
|
|
37
|
+
database: 'bench'
|
|
38
|
+
};
|
|
39
|
+
|
|
38
40
|
/**
|
|
39
41
|
* Benchmark scenario configuration
|
|
40
42
|
*/
|
|
41
43
|
const scenarios = [
|
|
42
44
|
{
|
|
43
45
|
name: 'Concurrent Writes (10 agents)',
|
|
44
|
-
description: 'Simulates
|
|
46
|
+
description: 'Simulates 10 concurrent agents writing simultaneously',
|
|
45
47
|
operations: [
|
|
46
48
|
{ type: 'INSERT', count: 100, concurrent: 10 }
|
|
47
49
|
]
|
|
48
50
|
},
|
|
49
51
|
{
|
|
50
52
|
name: 'Mixed Workload (messages)',
|
|
51
|
-
description: 'Simulates
|
|
53
|
+
description: 'Simulates typical API message operations',
|
|
52
54
|
operations: [
|
|
53
55
|
{ type: 'INSERT', count: 500 },
|
|
54
56
|
{ type: 'SELECT', count: 2000 },
|
|
@@ -57,7 +59,7 @@ const scenarios = [
|
|
|
57
59
|
},
|
|
58
60
|
{
|
|
59
61
|
name: 'Write Lock Contention',
|
|
60
|
-
description: 'Stress test for lock handling',
|
|
62
|
+
description: 'Stress test for lock handling with 50 concurrent writers',
|
|
61
63
|
operations: [
|
|
62
64
|
{ type: 'INSERT', count: 100, concurrent: 50 }
|
|
63
65
|
]
|
|
@@ -163,6 +165,29 @@ async function benchmarkSQLite(scenario) {
|
|
|
163
165
|
}
|
|
164
166
|
}
|
|
165
167
|
}
|
|
168
|
+
} else if (op.type === 'SELECT') {
|
|
169
|
+
for (let i = 0; i < op.count; i++) {
|
|
170
|
+
const start = Date.now();
|
|
171
|
+
try {
|
|
172
|
+
db.prepare('SELECT * FROM messages LIMIT 10').all();
|
|
173
|
+
metrics.addLatency(Date.now() - start);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
metrics.addError(error);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} else if (op.type === 'UPDATE') {
|
|
179
|
+
for (let i = 0; i < op.count; i++) {
|
|
180
|
+
const start = Date.now();
|
|
181
|
+
try {
|
|
182
|
+
db.prepare('UPDATE messages SET content = ? WHERE id = ?').run(
|
|
183
|
+
`Updated ${i}`,
|
|
184
|
+
(i % 100) + 1
|
|
185
|
+
);
|
|
186
|
+
metrics.addLatency(Date.now() - start);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
metrics.addError(error);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
166
191
|
}
|
|
167
192
|
}
|
|
168
193
|
|
|
@@ -173,58 +198,20 @@ async function benchmarkSQLite(scenario) {
|
|
|
173
198
|
}
|
|
174
199
|
|
|
175
200
|
/**
|
|
176
|
-
* PGlite Benchmark
|
|
201
|
+
* PGlite Benchmark (in-process WASM PostgreSQL)
|
|
177
202
|
*/
|
|
178
203
|
async function benchmarkPGlite(scenario) {
|
|
179
204
|
console.log(' 🔹 Running PGlite benchmark...');
|
|
180
205
|
|
|
181
|
-
// Clean up stale instances
|
|
182
|
-
cleanup();
|
|
183
|
-
|
|
184
206
|
const dataDir = path.join(RESULTS_DIR, 'pglite-bench');
|
|
185
207
|
if (fs.existsSync(dataDir)) {
|
|
186
208
|
fs.rmSync(dataDir, { recursive: true });
|
|
187
209
|
}
|
|
188
210
|
|
|
189
|
-
const
|
|
190
|
-
dataDir,
|
|
191
|
-
port: 12999,
|
|
192
|
-
autoPort: true,
|
|
193
|
-
logLevel: 'error'
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// Connect via PostgreSQL pool (proper way to use the server)
|
|
197
|
-
const pool = new Pool({
|
|
198
|
-
host: 'localhost',
|
|
199
|
-
port: instance.port,
|
|
200
|
-
database: 'postgres',
|
|
201
|
-
max: 20,
|
|
202
|
-
connectionTimeoutMillis: 10000,
|
|
203
|
-
ssl: false
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
// Give server a moment to be fully ready
|
|
207
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
208
|
-
|
|
209
|
-
// Wait for server to be ready with retries
|
|
210
|
-
let connected = false;
|
|
211
|
-
for (let i = 0; i < 10; i++) {
|
|
212
|
-
try {
|
|
213
|
-
await pool.query('SELECT 1');
|
|
214
|
-
connected = true;
|
|
215
|
-
break;
|
|
216
|
-
} catch (error) {
|
|
217
|
-
if (i === 9) throw error;
|
|
218
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (!connected) {
|
|
223
|
-
throw new Error('Failed to connect to PGlite server');
|
|
224
|
-
}
|
|
211
|
+
const db = new PGlite(dataDir);
|
|
225
212
|
|
|
226
213
|
// Setup schema
|
|
227
|
-
await
|
|
214
|
+
await db.exec(`
|
|
228
215
|
CREATE TABLE IF NOT EXISTS messages (
|
|
229
216
|
id SERIAL PRIMARY KEY,
|
|
230
217
|
content TEXT,
|
|
@@ -235,71 +222,75 @@ async function benchmarkPGlite(scenario) {
|
|
|
235
222
|
const metrics = new Metrics();
|
|
236
223
|
metrics.start();
|
|
237
224
|
|
|
238
|
-
// Run operations
|
|
225
|
+
// Run operations (PGlite is single-threaded, so concurrent = sequential)
|
|
239
226
|
for (const op of scenario.operations) {
|
|
240
227
|
if (op.type === 'INSERT') {
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
228
|
+
const total = op.count;
|
|
229
|
+
for (let i = 0; i < total; i++) {
|
|
230
|
+
const start = Date.now();
|
|
231
|
+
try {
|
|
232
|
+
await db.query(
|
|
233
|
+
'INSERT INTO messages (content, timestamp) VALUES ($1, $2)',
|
|
234
|
+
[`Message ${i}`, Date.now()]
|
|
235
|
+
);
|
|
236
|
+
metrics.addLatency(Date.now() - start);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
metrics.addError(error);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} else if (op.type === 'SELECT') {
|
|
242
|
+
for (let i = 0; i < op.count; i++) {
|
|
243
|
+
const start = Date.now();
|
|
244
|
+
try {
|
|
245
|
+
await db.query('SELECT * FROM messages LIMIT 10');
|
|
246
|
+
metrics.addLatency(Date.now() - start);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
metrics.addError(error);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} else if (op.type === 'UPDATE') {
|
|
252
|
+
for (let i = 0; i < op.count; i++) {
|
|
253
|
+
const start = Date.now();
|
|
254
|
+
try {
|
|
255
|
+
await db.query(
|
|
256
|
+
'UPDATE messages SET content = $1 WHERE id = $2',
|
|
257
|
+
[`Updated ${i}`, (i % 100) + 1]
|
|
258
|
+
);
|
|
259
|
+
metrics.addLatency(Date.now() - start);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
metrics.addError(error);
|
|
262
|
+
}
|
|
262
263
|
}
|
|
263
|
-
|
|
264
|
-
await Promise.all(promises);
|
|
265
264
|
}
|
|
266
265
|
}
|
|
267
266
|
|
|
268
267
|
metrics.end();
|
|
269
268
|
|
|
270
|
-
// Cleanup
|
|
271
|
-
await pool.end();
|
|
272
|
-
|
|
273
|
-
// Stop instance
|
|
274
269
|
try {
|
|
275
|
-
await
|
|
276
|
-
} catch (
|
|
277
|
-
// ExitStatus errors
|
|
278
|
-
if (error.name !== 'ExitStatus') {
|
|
279
|
-
console.error('⚠️ Error stopping instance:', error.message);
|
|
280
|
-
}
|
|
270
|
+
await db.close();
|
|
271
|
+
} catch (e) {
|
|
272
|
+
// Ignore ExitStatus errors from WASM cleanup
|
|
281
273
|
}
|
|
282
274
|
|
|
283
275
|
return metrics.getReport();
|
|
284
276
|
}
|
|
285
277
|
|
|
286
278
|
/**
|
|
287
|
-
* PostgreSQL Server Benchmark
|
|
279
|
+
* PostgreSQL Server Benchmark (remote real PostgreSQL)
|
|
288
280
|
*/
|
|
289
281
|
async function benchmarkPostgreSQL(scenario) {
|
|
290
282
|
console.log(' 🔷 Running PostgreSQL Server benchmark...');
|
|
291
283
|
|
|
292
284
|
const pool = new Pool({
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
user: 'postgres',
|
|
296
|
-
password: '#Duassenha#2024',
|
|
297
|
-
database: 'genie_evolution',
|
|
298
|
-
max: 20 // Connection pool size
|
|
285
|
+
...POSTGRES_CONFIG,
|
|
286
|
+
max: 20
|
|
299
287
|
});
|
|
300
288
|
|
|
301
289
|
try {
|
|
302
|
-
//
|
|
290
|
+
// Test connection first
|
|
291
|
+
await pool.query('SELECT 1');
|
|
292
|
+
|
|
293
|
+
// Setup schema
|
|
303
294
|
await pool.query(`
|
|
304
295
|
DROP TABLE IF EXISTS bench_messages;
|
|
305
296
|
CREATE TABLE bench_messages (
|
|
@@ -339,6 +330,29 @@ async function benchmarkPostgreSQL(scenario) {
|
|
|
339
330
|
}
|
|
340
331
|
|
|
341
332
|
await Promise.all(promises);
|
|
333
|
+
} else if (op.type === 'SELECT') {
|
|
334
|
+
for (let i = 0; i < op.count; i++) {
|
|
335
|
+
const start = Date.now();
|
|
336
|
+
try {
|
|
337
|
+
await pool.query('SELECT * FROM bench_messages LIMIT 10');
|
|
338
|
+
metrics.addLatency(Date.now() - start);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
metrics.addError(error);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} else if (op.type === 'UPDATE') {
|
|
344
|
+
for (let i = 0; i < op.count; i++) {
|
|
345
|
+
const start = Date.now();
|
|
346
|
+
try {
|
|
347
|
+
await pool.query(
|
|
348
|
+
'UPDATE bench_messages SET content = $1 WHERE id = $2',
|
|
349
|
+
[`Updated ${i}`, (i % 100) + 1]
|
|
350
|
+
);
|
|
351
|
+
metrics.addLatency(Date.now() - start);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
metrics.addError(error);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
342
356
|
}
|
|
343
357
|
}
|
|
344
358
|
|
|
@@ -350,9 +364,135 @@ async function benchmarkPostgreSQL(scenario) {
|
|
|
350
364
|
|
|
351
365
|
return metrics.getReport();
|
|
352
366
|
} catch (error) {
|
|
353
|
-
console.error('
|
|
367
|
+
console.error(' PostgreSQL benchmark failed:', error.message);
|
|
368
|
+
await pool.end();
|
|
369
|
+
return { throughput: 0, p50: 0, p99: 0, errors: 1, lockTimeouts: 0, totalOps: 0, skipped: true };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* pgserve Benchmark (our solution - embedded PostgreSQL with TRUE concurrency)
|
|
375
|
+
*/
|
|
376
|
+
async function benchmarkPgserve(scenario) {
|
|
377
|
+
console.log(' 🚀 Running pgserve benchmark...');
|
|
378
|
+
|
|
379
|
+
let server;
|
|
380
|
+
try {
|
|
381
|
+
// Start pgserve in memory mode
|
|
382
|
+
server = await startMultiTenantServer({
|
|
383
|
+
port: 18432,
|
|
384
|
+
logLevel: 'error'
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Wait for server to be fully ready
|
|
388
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
389
|
+
|
|
390
|
+
const pool = new Pool({
|
|
391
|
+
host: 'localhost',
|
|
392
|
+
port: 18432,
|
|
393
|
+
database: 'bench_test',
|
|
394
|
+
user: 'postgres',
|
|
395
|
+
password: 'postgres',
|
|
396
|
+
max: 20,
|
|
397
|
+
connectionTimeoutMillis: 30000
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Wait for connection with retries
|
|
401
|
+
let connected = false;
|
|
402
|
+
for (let i = 0; i < 10; i++) {
|
|
403
|
+
try {
|
|
404
|
+
await pool.query('SELECT 1');
|
|
405
|
+
connected = true;
|
|
406
|
+
break;
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (i === 9) throw error;
|
|
409
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!connected) {
|
|
414
|
+
throw new Error('Failed to connect to pgserve');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Setup schema
|
|
418
|
+
await pool.query(`
|
|
419
|
+
DROP TABLE IF EXISTS bench_messages;
|
|
420
|
+
CREATE TABLE bench_messages (
|
|
421
|
+
id SERIAL PRIMARY KEY,
|
|
422
|
+
content TEXT,
|
|
423
|
+
timestamp BIGINT
|
|
424
|
+
)
|
|
425
|
+
`);
|
|
426
|
+
|
|
427
|
+
const metrics = new Metrics();
|
|
428
|
+
metrics.start();
|
|
429
|
+
|
|
430
|
+
// Run operations (TRUE concurrent - pgserve handles this natively)
|
|
431
|
+
for (const op of scenario.operations) {
|
|
432
|
+
if (op.type === 'INSERT') {
|
|
433
|
+
const concurrent = op.concurrent || 1;
|
|
434
|
+
const perThread = Math.floor(op.count / concurrent);
|
|
435
|
+
|
|
436
|
+
const promises = [];
|
|
437
|
+
for (let i = 0; i < concurrent; i++) {
|
|
438
|
+
promises.push(
|
|
439
|
+
(async () => {
|
|
440
|
+
for (let j = 0; j < perThread; j++) {
|
|
441
|
+
const start = Date.now();
|
|
442
|
+
try {
|
|
443
|
+
await pool.query(
|
|
444
|
+
'INSERT INTO bench_messages (content, timestamp) VALUES ($1, $2)',
|
|
445
|
+
[`Message ${i}-${j}`, Date.now()]
|
|
446
|
+
);
|
|
447
|
+
metrics.addLatency(Date.now() - start);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
metrics.addError(error);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
})()
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
await Promise.all(promises);
|
|
457
|
+
} else if (op.type === 'SELECT') {
|
|
458
|
+
for (let i = 0; i < op.count; i++) {
|
|
459
|
+
const start = Date.now();
|
|
460
|
+
try {
|
|
461
|
+
await pool.query('SELECT * FROM bench_messages LIMIT 10');
|
|
462
|
+
metrics.addLatency(Date.now() - start);
|
|
463
|
+
} catch (error) {
|
|
464
|
+
metrics.addError(error);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
} else if (op.type === 'UPDATE') {
|
|
468
|
+
for (let i = 0; i < op.count; i++) {
|
|
469
|
+
const start = Date.now();
|
|
470
|
+
try {
|
|
471
|
+
await pool.query(
|
|
472
|
+
'UPDATE bench_messages SET content = $1 WHERE id = $2',
|
|
473
|
+
[`Updated ${i}`, (i % 100) + 1]
|
|
474
|
+
);
|
|
475
|
+
metrics.addLatency(Date.now() - start);
|
|
476
|
+
} catch (error) {
|
|
477
|
+
metrics.addError(error);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
metrics.end();
|
|
484
|
+
|
|
485
|
+
// Cleanup
|
|
354
486
|
await pool.end();
|
|
355
|
-
|
|
487
|
+
await server.stop();
|
|
488
|
+
|
|
489
|
+
return metrics.getReport();
|
|
490
|
+
} catch (error) {
|
|
491
|
+
console.error(' pgserve benchmark failed:', error.message);
|
|
492
|
+
if (server) {
|
|
493
|
+
try { await server.stop(); } catch (e) {}
|
|
494
|
+
}
|
|
495
|
+
return { throughput: 0, p50: 0, p99: 0, errors: 1, lockTimeouts: 0, totalOps: 0, skipped: true };
|
|
356
496
|
}
|
|
357
497
|
}
|
|
358
498
|
|
|
@@ -372,70 +512,69 @@ function generateReport(results) {
|
|
|
372
512
|
// Generate markdown
|
|
373
513
|
let md = '# Benchmark Results\n\n';
|
|
374
514
|
md += `**Date:** ${new Date().toLocaleString()}\n\n`;
|
|
515
|
+
md += '## Quick Start\n\n';
|
|
516
|
+
md += '```bash\n';
|
|
517
|
+
md += '# Zero install - just run!\n';
|
|
518
|
+
md += 'npx pgserve\n\n';
|
|
519
|
+
md += '# Connect from any PostgreSQL client\n';
|
|
520
|
+
md += 'psql postgresql://localhost:5432/mydb\n';
|
|
521
|
+
md += '```\n\n';
|
|
375
522
|
|
|
376
523
|
for (const scenario of results) {
|
|
377
524
|
md += `## ${scenario.name}\n\n`;
|
|
378
525
|
md += `${scenario.description}\n\n`;
|
|
379
526
|
|
|
527
|
+
const { sqlite, pglite, postgres, pgserve } = scenario;
|
|
528
|
+
|
|
380
529
|
md += '```\n';
|
|
381
|
-
md += '
|
|
382
|
-
md += '│
|
|
383
|
-
md += '
|
|
384
|
-
|
|
385
|
-
const sqlite = scenario.sqlite;
|
|
386
|
-
const pglite = scenario.pglite;
|
|
387
|
-
const postgres = scenario.postgres;
|
|
388
|
-
|
|
389
|
-
// Find winner for each metric
|
|
390
|
-
const maxThroughput = Math.max(sqlite.throughput, pglite.throughput, postgres.throughput);
|
|
391
|
-
const minP50 = Math.min(sqlite.p50, pglite.p50, postgres.p50);
|
|
392
|
-
const minP99 = Math.min(sqlite.p99, pglite.p99, postgres.p99);
|
|
393
|
-
const minErrors = Math.min(sqlite.errors, pglite.errors, postgres.errors);
|
|
394
|
-
|
|
395
|
-
const getThroughputWinner = () => {
|
|
396
|
-
if (postgres.throughput === maxThroughput) return 'PostgreSQL';
|
|
397
|
-
if (pglite.throughput === maxThroughput) return 'PGlite';
|
|
398
|
-
return 'SQLite';
|
|
399
|
-
};
|
|
530
|
+
md += '┌─────────────────┬──────────┬──────────┬──────────┬──────────┬──────────┐\n';
|
|
531
|
+
md += '│ Metric │ SQLite │ PGlite │ PostgreSQL│ pgserve │ Winner │\n';
|
|
532
|
+
md += '├─────────────────┼──────────┼──────────┼──────────┼──────────┼──────────┤\n';
|
|
400
533
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (pgl === Math.min(pg, pgl, sql)) return 'PGlite';
|
|
407
|
-
return 'SQLite';
|
|
408
|
-
};
|
|
534
|
+
// Find winners
|
|
535
|
+
const throughputs = { sqlite: sqlite.throughput, pglite: pglite.throughput, postgres: postgres.throughput, pgserve: pgserve.throughput };
|
|
536
|
+
const p50s = { sqlite: sqlite.p50, pglite: pglite.p50, postgres: postgres.p50, pgserve: pgserve.p50 };
|
|
537
|
+
const p99s = { sqlite: sqlite.p99, pglite: pglite.p99, postgres: postgres.p99, pgserve: pgserve.p99 };
|
|
538
|
+
const errors = { sqlite: sqlite.errors, pglite: pglite.errors, postgres: postgres.errors, pgserve: pgserve.errors };
|
|
409
539
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
540
|
+
const getMaxKey = (obj) => Object.entries(obj).reduce((a, b) => a[1] > b[1] ? a : b)[0];
|
|
541
|
+
const getMinKey = (obj) => Object.entries(obj).filter(([k,v]) => v > 0 || k === 'sqlite').reduce((a, b) => a[1] < b[1] ? a : b)[0];
|
|
542
|
+
const getMinErrorKey = (obj) => Object.entries(obj).reduce((a, b) => a[1] <= b[1] ? a : b)[0];
|
|
543
|
+
|
|
544
|
+
const nameMap = { sqlite: 'SQLite', pglite: 'PGlite', postgres: 'PostgreSQL', pgserve: 'pgserve' };
|
|
545
|
+
|
|
546
|
+
const pad = (s, n) => String(s).padEnd(n);
|
|
547
|
+
|
|
548
|
+
md += `│ Throughput (qps)│ ${pad(sqlite.throughput, 8)} │ ${pad(pglite.throughput, 8)} │ ${pad(postgres.throughput, 9)} │ ${pad(pgserve.throughput, 8)} │ ${pad(nameMap[getMaxKey(throughputs)], 8)} │\n`;
|
|
549
|
+
md += `│ P50 latency (ms)│ ${pad(sqlite.p50, 8)} │ ${pad(pglite.p50, 8)} │ ${pad(postgres.p50, 9)} │ ${pad(pgserve.p50, 8)} │ ${pad(nameMap[getMinKey(p50s)], 8)} │\n`;
|
|
550
|
+
md += `│ P99 latency (ms)│ ${pad(sqlite.p99, 8)} │ ${pad(pglite.p99, 8)} │ ${pad(postgres.p99, 9)} │ ${pad(pgserve.p99, 8)} │ ${pad(nameMap[getMinKey(p99s)], 8)} │\n`;
|
|
551
|
+
md += `│ Errors │ ${pad(sqlite.errors, 8)} │ ${pad(pglite.errors, 8)} │ ${pad(postgres.errors, 9)} │ ${pad(pgserve.errors, 8)} │ ${pad(nameMap[getMinErrorKey(errors)], 8)} │\n`;
|
|
552
|
+
md += '└─────────────────┴──────────┴──────────┴──────────┴──────────┴──────────┘\n';
|
|
416
553
|
md += '```\n\n';
|
|
417
554
|
|
|
418
555
|
// Analysis
|
|
419
|
-
const winner =
|
|
420
|
-
if (winner === '
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const vsSQL = ((pglite.throughput / sqlite.throughput - 1) * 100).toFixed(1);
|
|
426
|
-
const vsPG = ((pglite.throughput / postgres.throughput - 1) * 100).toFixed(1);
|
|
427
|
-
md += `💡 **PGlite is ${vsSQL}% faster than SQLite and ${vsPG}% faster than PostgreSQL Server**\n\n`;
|
|
556
|
+
const winner = nameMap[getMaxKey(throughputs)];
|
|
557
|
+
if (winner === 'pgserve') {
|
|
558
|
+
const vsSQLite = sqlite.throughput > 0 ? ((pgserve.throughput / sqlite.throughput - 1) * 100).toFixed(1) : 'N/A';
|
|
559
|
+
const vsPGlite = pglite.throughput > 0 ? ((pgserve.throughput / pglite.throughput - 1) * 100).toFixed(1) : 'N/A';
|
|
560
|
+
const vsPostgres = postgres.throughput > 0 ? ((pgserve.throughput / postgres.throughput - 1) * 100).toFixed(1) : 'N/A';
|
|
561
|
+
md += `**pgserve wins!** ${vsPGlite}% faster than PGlite for concurrent workloads.\n\n`;
|
|
428
562
|
} else {
|
|
429
|
-
|
|
430
|
-
const vsPG = ((sqlite.throughput / postgres.throughput - 1) * 100).toFixed(1);
|
|
431
|
-
md += `💡 **SQLite is ${vsPGL}% faster than PGlite and ${vsPG}% faster than PostgreSQL Server**\n\n`;
|
|
563
|
+
md += `**${winner} wins** this scenario.\n\n`;
|
|
432
564
|
}
|
|
433
565
|
}
|
|
434
566
|
|
|
567
|
+
md += '---\n\n';
|
|
568
|
+
md += '## Why pgserve?\n\n';
|
|
569
|
+
md += '- **TRUE Concurrency**: Native PostgreSQL process forking\n';
|
|
570
|
+
md += '- **Zero Config**: Just run `npx pgserve`\n';
|
|
571
|
+
md += '- **Auto-Provision**: Databases created on first connection\n';
|
|
572
|
+
md += '- **PostgreSQL 17.7**: Latest stable, native binaries\n';
|
|
573
|
+
|
|
435
574
|
const mdPath = path.join(RESULTS_DIR, 'benchmark-results.md');
|
|
436
575
|
fs.writeFileSync(mdPath, md);
|
|
437
576
|
|
|
438
|
-
console.log(`\
|
|
577
|
+
console.log(`\nResults saved to:`);
|
|
439
578
|
console.log(` JSON: ${jsonPath}`);
|
|
440
579
|
console.log(` Markdown: ${mdPath}\n`);
|
|
441
580
|
|
|
@@ -446,9 +585,10 @@ function generateReport(results) {
|
|
|
446
585
|
* Main runner
|
|
447
586
|
*/
|
|
448
587
|
async function main() {
|
|
449
|
-
console.log('
|
|
450
|
-
console.log('║
|
|
451
|
-
console.log('
|
|
588
|
+
console.log('╔════════════════════════════════════════════════════════════════╗');
|
|
589
|
+
console.log('║ pgserve Benchmark Suite ║');
|
|
590
|
+
console.log('║ Comparing: SQLite | PGlite | PostgreSQL | pgserve ║');
|
|
591
|
+
console.log('╚════════════════════════════════════════════════════════════════╝\n');
|
|
452
592
|
|
|
453
593
|
// Ensure results directory exists
|
|
454
594
|
if (!fs.existsSync(RESULTS_DIR)) {
|
|
@@ -464,26 +604,31 @@ async function main() {
|
|
|
464
604
|
const sqlite = await benchmarkSQLite(scenario);
|
|
465
605
|
const pglite = await benchmarkPGlite(scenario);
|
|
466
606
|
const postgres = await benchmarkPostgreSQL(scenario);
|
|
607
|
+
const pgserve = await benchmarkPgserve(scenario);
|
|
467
608
|
|
|
468
609
|
results.push({
|
|
469
610
|
name: scenario.name,
|
|
470
611
|
description: scenario.description,
|
|
471
612
|
sqlite,
|
|
472
613
|
pglite,
|
|
473
|
-
postgres
|
|
614
|
+
postgres,
|
|
615
|
+
pgserve
|
|
474
616
|
});
|
|
475
617
|
|
|
476
618
|
console.log(`\n SQLite: ${sqlite.throughput} qps, P50=${sqlite.p50}ms, errors=${sqlite.errors}`);
|
|
477
619
|
console.log(` PGlite: ${pglite.throughput} qps, P50=${pglite.p50}ms, errors=${pglite.errors}`);
|
|
478
620
|
console.log(` PostgreSQL: ${postgres.throughput} qps, P50=${postgres.p50}ms, errors=${postgres.errors}`);
|
|
621
|
+
console.log(` pgserve: ${pgserve.throughput} qps, P50=${pgserve.p50}ms, errors=${pgserve.errors}`);
|
|
479
622
|
}
|
|
480
623
|
|
|
481
624
|
console.log('\n📄 Generating report...\n');
|
|
482
|
-
|
|
625
|
+
generateReport(results);
|
|
483
626
|
|
|
484
|
-
console.log('
|
|
485
|
-
console.log('║
|
|
486
|
-
console.log('
|
|
627
|
+
console.log('╔════════════════════════════════════════════════════════════════╗');
|
|
628
|
+
console.log('║ Benchmarks Complete! ║');
|
|
629
|
+
console.log('║ ║');
|
|
630
|
+
console.log('║ Try it yourself: npx pgserve ║');
|
|
631
|
+
console.log('╚════════════════════════════════════════════════════════════════╝\n');
|
|
487
632
|
}
|
|
488
633
|
|
|
489
634
|
main().catch(console.error);
|