Skip to content

Commit 961ef45

Browse files
committed
perf(core): test PostgreSQL query pipelining
1 parent ff11a17 commit 961ef45

8 files changed

Lines changed: 202 additions & 14 deletions

File tree

bun.lock

Lines changed: 15 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

e2e-common/test-config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ export function getDbConfig(): DataSourceOptions {
113113
port: process.env.CI ? +(process.env.E2E_POSTGRES_PORT || 5432) : 5432,
114114
username: 'vendure',
115115
password: 'password',
116+
extra: {
117+
pipeline: true,
118+
},
116119
};
117120
case 'mariadb':
118121
return {

packages/core/e2e/database-transactions.e2e-spec.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mergeConfig } from '@vendure/core';
1+
import { mergeConfig, TransactionalConnection } from '@vendure/core';
22
import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
33
import { fail } from 'assert';
44
import path from 'path';
@@ -41,6 +41,18 @@ describe('Transaction infrastructure', () => {
4141
await server.destroy();
4242
});
4343

44+
itIfDb(['postgres'])('enables query pipelining on PostgreSQL connections', async () => {
45+
const connection = server.app.get(TransactionalConnection).rawConnection;
46+
const queryRunner = connection.createQueryRunner();
47+
48+
try {
49+
const databaseConnection = await queryRunner.connect();
50+
expect(databaseConnection.pipeline).toBe(true);
51+
} finally {
52+
await queryRunner.release();
53+
}
54+
});
55+
4456
it('non-failing mutation', async () => {
4557
const { createTestAdministrator } = await adminClient.query(createTestAdministratorDocument, {
4658
emailAddress: 'test1',
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/* eslint-disable no-console */
2+
import { Bench } from 'tinybench';
3+
import { DataSource, QueryRunner } from 'typeorm';
4+
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions';
5+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
6+
7+
import { getDbConfig } from '../../../e2e-common/test-config';
8+
9+
const BATCH_SIZE = 10;
10+
const expectedValues = Array.from({ length: BATCH_SIZE }, (_, index) => index);
11+
12+
describe.skipIf(process.env.DB !== 'postgres')('PostgreSQL query pipelining benchmark', () => {
13+
let sequentialDataSource: DataSource;
14+
let pipelinedDataSource: DataSource;
15+
let sequentialQueryRunner: QueryRunner;
16+
let pipelinedQueryRunner: QueryRunner;
17+
18+
beforeAll(async () => {
19+
const connectionOptions = getDbConfig();
20+
if (connectionOptions.type !== 'postgres') {
21+
throw new Error('This benchmark requires DB=postgres');
22+
}
23+
24+
sequentialDataSource = await createDataSource(connectionOptions, false);
25+
pipelinedDataSource = await createDataSource(connectionOptions, true);
26+
sequentialQueryRunner = sequentialDataSource.createQueryRunner();
27+
pipelinedQueryRunner = pipelinedDataSource.createQueryRunner();
28+
await Promise.all([sequentialQueryRunner.connect(), pipelinedQueryRunner.connect()]);
29+
});
30+
31+
afterAll(async () => {
32+
await Promise.all([sequentialQueryRunner?.release(), pipelinedQueryRunner?.release()]);
33+
await Promise.all([sequentialDataSource?.destroy(), pipelinedDataSource?.destroy()]);
34+
});
35+
36+
it('compares batches of concurrent queries on one TypeORM QueryRunner', async () => {
37+
const sequentialResult = await runBatch(sequentialQueryRunner);
38+
const pipelinedResult = await runBatch(pipelinedQueryRunner);
39+
expect(getValues(sequentialResult)).toEqual(expectedValues);
40+
expect(getValues(pipelinedResult)).toEqual(expectedValues);
41+
42+
const bench = new Bench({
43+
warmupTime: 500,
44+
time: 2000,
45+
});
46+
bench
47+
.add('pipeline off', () => runBatch(sequentialQueryRunner))
48+
.add('pipeline on', () => runBatch(pipelinedQueryRunner));
49+
50+
const tasks = await bench.run();
51+
const sequentialQps = getQueriesPerSecond(tasks[0].result?.hz);
52+
const pipelinedQps = getQueriesPerSecond(tasks[1].result?.hz);
53+
const speedup = pipelinedQps / sequentialQps;
54+
55+
console.table([
56+
{ mode: 'pipeline off', queriesPerSecond: Math.round(sequentialQps) },
57+
{ mode: 'pipeline on', queriesPerSecond: Math.round(pipelinedQps) },
58+
{ mode: 'ratio', queriesPerSecond: `${speedup.toFixed(2)}x` },
59+
]);
60+
61+
expect(sequentialQps).toBeGreaterThan(0);
62+
expect(pipelinedQps).toBeGreaterThan(0);
63+
});
64+
});
65+
66+
async function createDataSource(
67+
connectionOptions: PostgresConnectionOptions,
68+
pipeline: boolean,
69+
): Promise<DataSource> {
70+
const dataSource = new DataSource({
71+
...connectionOptions,
72+
database: 'postgres',
73+
entities: [],
74+
synchronize: false,
75+
extra: {
76+
...connectionOptions.extra,
77+
pipeline,
78+
},
79+
});
80+
await dataSource.initialize();
81+
return dataSource;
82+
}
83+
84+
function runBatch(queryRunner: QueryRunner): Promise<any[]> {
85+
return Promise.all(
86+
expectedValues.map(value => queryRunner.query('SELECT $1::integer AS value', [value])),
87+
);
88+
}
89+
90+
function getValues(results: any[]): number[] {
91+
return results.map(rows => rows[0].value);
92+
}
93+
94+
function getQueriesPerSecond(iterationsPerSecond: number | undefined): number {
95+
if (!iterationsPerSecond) {
96+
throw new Error('Benchmark did not produce a throughput result');
97+
}
98+
return iterationsPerSecond * BATCH_SIZE;
99+
}

packages/core/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"e2e": "cross-env PACKAGE=core vitest --config ../../e2e-common/vitest.config.mts --run",
2929
"e2e:watch": "cross-env PACKAGE=core vitest --config ../../e2e-common/vitest.config.mts",
3030
"bench": "cross-env PACKAGE=core vitest --config ../../e2e-common/vitest.config.bench.ts --run",
31+
"bench:pg-pipeline": "cross-env PACKAGE=core DB=postgres vitest --config ../../e2e-common/vitest.config.bench.ts --run e2e/postgres-pipelining.bench.ts --reporter=verbose",
3132
"i18n:extract": "node ./scripts/translate/i18n-tool.mjs extract",
3233
"i18n:apply": "node ./scripts/translate/i18n-tool.mjs apply",
3334
"ci": "npm run build"
@@ -99,7 +100,7 @@
99100
"glob": "^10.3.10",
100101
"ioredis": "^5.3.2",
101102
"mysql2": "^3.15.0",
102-
"pg": "^8.13.1",
103+
"pg": "^8.23.0",
103104
"rimraf": "^5.0.5",
104105
"sql.js": "1.13.0",
105106
"typescript": "5.8.2"

packages/dev-server/load-testing/load-test-config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ export function getPostgresConnectionOptions(databaseName: string) {
3434
username: 'admin',
3535
password: 'secret',
3636
database: databaseName,
37+
extra: {
38+
pipeline: true,
39+
},
3740
};
3841
}
3942

packages/testing/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"@types/pg": "^8.11.2",
5353
"@vendure/core": "3.7.2",
5454
"mysql2": "^3.15.0",
55-
"pg": "^8.11.3",
55+
"pg": "^8.23.0",
5656
"rimraf": "^5.0.5",
5757
"typescript": "5.8.2"
5858
}

0 commit comments

Comments
 (0)