From c49c3495977814ae616b4d3b136829809822f803 Mon Sep 17 00:00:00 2001 From: Nigro Simone Date: Wed, 19 Feb 2025 11:02:54 +0100 Subject: [PATCH 1/2] feat(benchmark): compare the latest version of pg against the current source code --- bench/.gitignore | 2 + bench/README.md | 8 +++ bench/cases/access.js | 27 +++++++++ bench/cases/base.js | 92 +++++++++++++++++++++++++++++++ bench/cases/clone_using_assign.js | 24 ++++++++ bench/cases/clone_using_spread.js | 24 ++++++++ bench/cases/index.js | 13 +++++ bench/cases/keys.js | 24 ++++++++ bench/cases/values.js | 24 ++++++++ bench/generators/base.js | 64 +++++++++++++++++++++ bench/generators/index.js | 7 +++ bench/generators/wide_rows.js | 58 +++++++++++++++++++ bench/index.js | 23 ++++++++ bench/lib/bencher.js | 42 ++++++++++++++ bench/lib/runner.js | 16 ++++++ bench/package.json | 18 ++++++ bench/versions.js | 7 +++ 17 files changed, 473 insertions(+) create mode 100644 bench/.gitignore create mode 100644 bench/README.md create mode 100644 bench/cases/access.js create mode 100644 bench/cases/base.js create mode 100644 bench/cases/clone_using_assign.js create mode 100644 bench/cases/clone_using_spread.js create mode 100644 bench/cases/index.js create mode 100644 bench/cases/keys.js create mode 100644 bench/cases/values.js create mode 100644 bench/generators/base.js create mode 100644 bench/generators/index.js create mode 100644 bench/generators/wide_rows.js create mode 100644 bench/index.js create mode 100644 bench/lib/bencher.js create mode 100644 bench/lib/runner.js create mode 100644 bench/package.json create mode 100644 bench/versions.js diff --git a/bench/.gitignore b/bench/.gitignore new file mode 100644 index 000000000..9ea26ec86 --- /dev/null +++ b/bench/.gitignore @@ -0,0 +1,2 @@ +node_modules/* +package-lock.json \ No newline at end of file diff --git a/bench/README.md b/bench/README.md new file mode 100644 index 000000000..c916f526e --- /dev/null +++ b/bench/README.md @@ -0,0 +1,8 @@ +Running benchmark +=== +This benchmark compare the latest version of pg against the current source code. +- pg-latest: is the latest version of pg published on npm +- pg-current: is the current source code +``` +npm run bench +``` \ No newline at end of file diff --git a/bench/cases/access.js b/bench/cases/access.js new file mode 100644 index 000000000..92823a876 --- /dev/null +++ b/bench/cases/access.js @@ -0,0 +1,27 @@ +import BaseCase from "./base.js"; +import { WideRowsGenerator } from "../generators/index.js"; + +class AccessCase extends BaseCase { + constructor(pgLib) { + super(pgLib, 'processing'); + } + async prepare() { + await super.prepare(WideRowsGenerator, { rows: 10000, fields: 100 }); + } + async implementation(generator) { + let result = await generator.query(`SELECT * FROM "${generator.id}"`); + this.startMeasurement('processing'); + for (let row of result.rows) { + this.runRow(row); + } + this.endMeasurement('processing'); + } + runRow(row) { + let result = ''; + for (let key in row) { + result += row[key]; + } + } +} + +export default AccessCase; \ No newline at end of file diff --git a/bench/cases/base.js b/bench/cases/base.js new file mode 100644 index 000000000..84d1f4a98 --- /dev/null +++ b/bench/cases/base.js @@ -0,0 +1,92 @@ +import * as Generators from "../generators/index.js"; +import _ from 'lodash'; + +class BaseCase { + #measurements = {}; + #mainMeasurement; + #generator; + #pgLib; + #pool; + #client; + constructor(pgLib, mainMeasurement = 'full') { + this.#pgLib = pgLib; + this.#mainMeasurement = mainMeasurement; + } + async prepare(generator, cfg = {}) { + if (generator) { + if (generator instanceof Generators.BaseGenerator) { + this.#generator = generator; + await this.#generator.run(); + } else if (typeof generator == 'function' && 'prototype' in generator) { + this.#generator = new generator(this.#client); + await this.#generator.run(cfg); + } + } + } + async #setup() { + this.#pool = new this.#pgLib.Pool({ + + }); + this.#client = await this.#pool.connect(); + await this.#client.query(`SET work_mem = '512MB'`); + } + async #teardown() { + await this.#client.release(); + await this.#pool.end(); + } + async run() { + this.startMeasurement('total'); + this.startMeasurement('setup'); + await this.#setup(); + this.endMeasurement('setup'); + this.startMeasurement('prepare'); + await this.prepare(); + this.endMeasurement('prepare'); + + this.startMeasurement('full'); + if (typeof this.implementation != 'function') { + throw new Error(`Implementation not provided`); + } + await this.implementation(this.#generator); + this.endMeasurement('full'); + + this.startMeasurement('clean'); + await this.clean(); + this.endMeasurement('clean'); + this.startMeasurement('teardown'); + await this.#teardown(); + this.endMeasurement('teardown'); + this.endMeasurement('total'); + let details = _.mapValues(this.#measurements, (obj, k) => _.round(obj.total, 2)); + + return { + main: details[this.#mainMeasurement], + details + }; + } + async clean() { + + } + startMeasurement(name) { + if (this.#measurements[name]) { + throw new Error(`Measurement ${name} was already started`); + } + this.#measurements[name] = { + start: process.hrtime.bigint(), + end: null, + total: null + }; + } + endMeasurement(name) { + if (!this.#measurements[name]) { + throw new Error(`Measurement ${name} was never started`); + } + if (this.#measurements[name].end) { + throw new Error(`Measurement ${name} has already ended`); + } + this.#measurements[name].end = process.hrtime.bigint(); + this.#measurements[name].total = new Number(this.#measurements[name].end - this.#measurements[name].start) / 1000 / 1000; + } +} + +export default BaseCase; \ No newline at end of file diff --git a/bench/cases/clone_using_assign.js b/bench/cases/clone_using_assign.js new file mode 100644 index 000000000..0403b622d --- /dev/null +++ b/bench/cases/clone_using_assign.js @@ -0,0 +1,24 @@ +import BaseCase from "./base.js"; +import { WideRowsGenerator } from "../generators/index.js"; + +class CloneUsingAssignCase extends BaseCase { + constructor(pgLib) { + super(pgLib, 'processing'); + } + async prepare() { + await super.prepare(WideRowsGenerator, { rows: 10000, fields: 100 }); + } + async implementation(generator) { + let result = await generator.query(`SELECT * FROM "${generator.id}"`); + this.startMeasurement('processing'); + for (let row of result.rows) { + this.runRow(row); + } + this.endMeasurement('processing'); + } + runRow(row) { + let clone = Object.assign({}, row); + } +} + +export default CloneUsingAssignCase; \ No newline at end of file diff --git a/bench/cases/clone_using_spread.js b/bench/cases/clone_using_spread.js new file mode 100644 index 000000000..96b33a25f --- /dev/null +++ b/bench/cases/clone_using_spread.js @@ -0,0 +1,24 @@ +import BaseCase from "./base.js"; +import { WideRowsGenerator } from "../generators/index.js"; + +class CloneUsingSpreadCase extends BaseCase { + constructor(pgLib) { + super(pgLib, 'processing'); + } + async prepare() { + await super.prepare(WideRowsGenerator, { rows: 10000, fields: 100 }); + } + async implementation(generator) { + let result = await generator.query(`SELECT * FROM "${generator.id}"`); + this.startMeasurement('processing'); + for (let row of result.rows) { + this.runRow(row); + } + this.endMeasurement('processing'); + } + runRow(row) { + let clone = { ... row }; + } +} + +export default CloneUsingSpreadCase; \ No newline at end of file diff --git a/bench/cases/index.js b/bench/cases/index.js new file mode 100644 index 000000000..981f6a9c8 --- /dev/null +++ b/bench/cases/index.js @@ -0,0 +1,13 @@ +import CloneUsingAssignCase from "./clone_using_assign.js"; +import CloneUsingSpreadCase from "./clone_using_spread.js"; +import AccessCase from "./access.js"; +import ValuesCase from "./values.js"; +import KeysCase from "./keys.js"; + +export { + CloneUsingAssignCase, + CloneUsingSpreadCase, + AccessCase, + ValuesCase, + KeysCase +}; \ No newline at end of file diff --git a/bench/cases/keys.js b/bench/cases/keys.js new file mode 100644 index 000000000..ea266bac0 --- /dev/null +++ b/bench/cases/keys.js @@ -0,0 +1,24 @@ +import BaseCase from "./base.js"; +import { WideRowsGenerator } from "../generators/index.js"; + +class KeysCase extends BaseCase { + constructor(pgLib) { + super(pgLib, 'processing'); + } + async prepare() { + await super.prepare(WideRowsGenerator, { rows: 10000, fields: 100 }); + } + async implementation(generator) { + let result = await generator.query(`SELECT * FROM "${generator.id}"`); + this.startMeasurement('processing'); + for (let row of result.rows) { + this.runRow(row); + } + this.endMeasurement('processing'); + } + runRow(row) { + return Object.keys(row); + } +} + +export default KeysCase; \ No newline at end of file diff --git a/bench/cases/values.js b/bench/cases/values.js new file mode 100644 index 000000000..43cc987dc --- /dev/null +++ b/bench/cases/values.js @@ -0,0 +1,24 @@ +import BaseCase from "./base.js"; +import { WideRowsGenerator } from "../generators/index.js"; + +class ValuesCase extends BaseCase { + constructor(pgLib) { + super(pgLib, 'processing'); + } + async prepare() { + await super.prepare(WideRowsGenerator, { rows: 10000, fields: 100 }); + } + async implementation(generator) { + let result = await generator.query(`SELECT * FROM "${generator.id}"`); + this.startMeasurement('processing'); + for (let row of result.rows) { + this.runRow(row); + } + this.endMeasurement('processing'); + } + runRow(row) { + return Object.values(row); + } +} + +export default ValuesCase; \ No newline at end of file diff --git a/bench/generators/base.js b/bench/generators/base.js new file mode 100644 index 000000000..80df1c987 --- /dev/null +++ b/bench/generators/base.js @@ -0,0 +1,64 @@ +import crypto from 'node:crypto'; +import _ from 'lodash'; +import { v4 as uuidv4 } from 'uuid'; + +class BaseGenerator { + #id; + #client; + constructor(client) { + this.#client = client; + this.#id = this.randomString(); + } + get id() { + return this.#id; + } + async run() { + throw new Error(`Run not implemented in generator`); + } + async cleanup() { + throw new Error(`Cleanup not implemented in generator`); + } + async query(query, values) { + let start = +new Date(); + //console.log(`> ${query.substr(0, 100)}`); + let result = await this.#client.query(query, values); + let end = +new Date(); + //console.log(`< ${query.substr(0, 100)}; ${end-start}ms`); + return result; + } + async create(tableName, fields) { + await this.query(`CREATE TABLE "${tableName}" (${_.map(fields, f => `${f.name} ${f.type}`).join(', ')})`); + } + async insert(tableName, fields, data, chunkSize = 10000) { + let chunks = _.chunk(data, chunkSize); + for (let chunk of chunks) { + let valuePlaceHolders = []; + let values = []; + let i = 1; + for (let field of fields) { + let fieldData = new Array(chunk.length) + for (let j = 0; j < chunk.length; j++) { + fieldData[j] = chunk[j][field.name]; + } + valuePlaceHolders.push(`$${i}::${field.type}[]`); + values.push(fieldData); + i++; + } + let start = +new Date(); + await this.query(`INSERT INTO "${tableName}" (${_.map(fields, f => f.name).join(', ')}) SELECT * FROM UNNEST (${valuePlaceHolders.join(', ')})`, values); + let end = +new Date(); + //console.log('actual insert took', end - start); + } + } + async drop(tableName) { + await this.query(`DROP TABLE "${tableName}"`); + } + uuid() { + return crypto.randomUUID(); + } + randomString(length = 16) { + return Math.random().toString(20).substr(2, 6); + } +} + +export default BaseGenerator; \ No newline at end of file diff --git a/bench/generators/index.js b/bench/generators/index.js new file mode 100644 index 000000000..0bd1223a2 --- /dev/null +++ b/bench/generators/index.js @@ -0,0 +1,7 @@ +import WideRowsGenerator from "./wide_rows.js"; +import BaseGenerator from "./base.js"; + +export { + BaseGenerator, + WideRowsGenerator +}; \ No newline at end of file diff --git a/bench/generators/wide_rows.js b/bench/generators/wide_rows.js new file mode 100644 index 000000000..121e18ebd --- /dev/null +++ b/bench/generators/wide_rows.js @@ -0,0 +1,58 @@ +import BaseGenerator from "./base.js"; + +class WideRowsGenerator extends BaseGenerator { + #typeConfigs = { + int: () => { + return Math.floor(Math.random() * 1000000); + }, + uuid: () => { + return this.uuid(); + }, + text: () => { + return this.uuid(100); + } + }; + constructor(client) { + super(client) + } + async run(config) { + if (!config) { + throw new Error(`Config not passed to run`); + } + if (!config.rows) { + throw new Error(`Rows not passed`); + } + if (!config.fields) { + throw new Error(`Fields not passed`); + } + let fields = []; + for (let i = 0; i < config.fields; i++) { + let typeIndex = i % Object.keys(this.#typeConfigs).length; + fields.push({ + name: `col_${i}`, + type: Object.keys(this.#typeConfigs)[typeIndex], + generator: this.#typeConfigs[Object.keys(this.#typeConfigs)[typeIndex]] + }); + } + await this.create(this.id, fields); + let data = []; + let beforeGenerate = +new Date(); + for (let i = 0; i < config.rows; i++) { + let obj = {}; + for (let field of fields) { + obj[field.name] = field.generator(); + } + data.push(obj); + } + let afterGenerate = +new Date(); + //console.log(`generating; ${afterGenerate-beforeGenerate}ms`); + await this.insert(this.id, fields, data); + let afterInsert = +new Date(); + //console.log(`inserting; ${afterInsert-afterGenerate}ms`); + } + async cleanup() { + await this.drop(this.id); + } +} + +export default WideRowsGenerator; \ No newline at end of file diff --git a/bench/index.js b/bench/index.js new file mode 100644 index 000000000..90fc0a06f --- /dev/null +++ b/bench/index.js @@ -0,0 +1,23 @@ +import Bencher from "./lib/bencher.js"; +import * as versions from './versions.js'; +import _ from 'lodash'; + +let measurementsByVersions = {}; + +for (let version in versions.versions) { + console.log(`> ${version}`); + let start = +new Date(); + measurementsByVersions[version] = await new Bencher(version).run(); + let end = +new Date(); + console.log(`< ${version}; ${end-start}ms`); +} + +let first = Object.keys(measurementsByVersions)[0]; +let cases = Object.keys(measurementsByVersions[first]); +for (let caseName of cases) { + console.log(caseName, _.mapValues(measurementsByVersions, r => r[caseName].main)); +} + +/*console.log(util.inspect(measurementsByVersions, { + depth: null +}));*/ \ No newline at end of file diff --git a/bench/lib/bencher.js b/bench/lib/bencher.js new file mode 100644 index 000000000..9c2e2ea96 --- /dev/null +++ b/bench/lib/bencher.js @@ -0,0 +1,42 @@ +import * as Cases from "../cases/index.js"; +import cp from 'node:child_process'; +import * as versions from '../versions.js'; + +class Bencher { + #pgVersion; + constructor(pgVersion) { + if (!pgVersion) { + throw new Error(`pgVersion not passed`); + } + this.#pgVersion = pgVersion; + } + async run() { + let result = {}; + for (let caseType in Cases) { + result[caseType] = await this.#runCase(caseType); + } + return result; + } + async #runCase(name) { + return await this.#runInCp(name); + } + #runInCp(caseName) { + return new Promise((resolve, reject) => { + cp.exec(`node lib/runner.js ${caseName} ${this.#pgVersion}`, (err, stdout, stderr) => { + if (err) { + return reject(new Error(`Error running case ${caseName} for version ${this.#pgVersion}; ${err.message}`)); + } + if (stderr) { + return reject(new Error(`Error while running case ${caseName} for version ${this.#pgVersion}; ${stderr}`)); + } + try { + return resolve(JSON.parse(stdout)); + } catch (e) { + return reject(new Error(`Error after running case ${caseName} for version ${this.#pgVersion}; ${e.message}`)); + } + }) + }); + } +} + +export default Bencher; \ No newline at end of file diff --git a/bench/lib/runner.js b/bench/lib/runner.js new file mode 100644 index 000000000..20dd4a59c --- /dev/null +++ b/bench/lib/runner.js @@ -0,0 +1,16 @@ +import * as cases from "../cases/index.js"; +import * as versions from '../versions.js'; + +const caseName = process.argv[2]; +if (!cases[caseName]) { + throw new Error(`Case: ${caseName} not found`); +} + +const pgVersion = process.argv[3]; +if (!versions.versions[pgVersion]) { + throw new Error(`Version: ${pgVersion} not found`); +} + +const inst = new cases[caseName](versions.versions[pgVersion]); +const result = await inst.run(); +process.stdout.write(JSON.stringify(result)); \ No newline at end of file diff --git a/bench/package.json b/bench/package.json new file mode 100644 index 000000000..25659ab50 --- /dev/null +++ b/bench/package.json @@ -0,0 +1,18 @@ +{ + "name": "node-postgres-bench", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "bench": "node index.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "lodash": "^4.17.21", + "pg-latest": "npm:pg@latest", + "uuid": "^9.0.0" + } +} diff --git a/bench/versions.js b/bench/versions.js new file mode 100644 index 000000000..39a710628 --- /dev/null +++ b/bench/versions.js @@ -0,0 +1,7 @@ +import pg_latest from 'pg-latest'; +import pg_current from '../packages/pg/lib/index.js'; + +export const versions = { + 'pg-latest': pg_latest, + 'pg-current': pg_current +}; From f77390cda93074fa1d070c1bfb1dd54c42af6a94 Mon Sep 17 00:00:00 2001 From: Nigro Simone Date: Tue, 8 Apr 2025 19:22:52 +0200 Subject: [PATCH 2/2] perf: avoid useless operation and unused arguments --- packages/pg/lib/client.js | 4 ++-- packages/pg/lib/query.js | 9 ++++----- packages/pg/lib/result.js | 4 ---- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/pg/lib/client.js b/packages/pg/lib/client.js index 4ccaffeac..3553be08a 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -299,7 +299,7 @@ class Client extends EventEmitter { this.secretKey = msg.secretKey } - _handleReadyForQuery(msg) { + _handleReadyForQuery() { if (this._connecting) { this._connecting = false this._connected = true @@ -318,7 +318,7 @@ class Client extends EventEmitter { this.activeQuery = null this.readyForQuery = true if (activeQuery) { - activeQuery.handleReadyForQuery(this.connection) + activeQuery.handleReadyForQuery() } this._pulseQueryQueue() } diff --git a/packages/pg/lib/query.js b/packages/pg/lib/query.js index 06b582f6f..0e460ef1c 100644 --- a/packages/pg/lib/query.js +++ b/packages/pg/lib/query.js @@ -80,12 +80,11 @@ class Query extends EventEmitter { } handleDataRow(msg) { - let row - if (this._canceledDueToError) { return } + let row try { row = this._result.parseRow(msg.fields) } catch (err) { @@ -119,7 +118,7 @@ class Query extends EventEmitter { } } - handleError(err, connection) { + handleError(err) { // need to sync after error during a prepared statement if (this._canceledDueToError) { err = this._canceledDueToError @@ -133,9 +132,9 @@ class Query extends EventEmitter { this.emit('error', err) } - handleReadyForQuery(con) { + handleReadyForQuery() { if (this._canceledDueToError) { - return this.handleError(this._canceledDueToError, con) + return this.handleError(this._canceledDueToError) } if (this.callback) { try { diff --git a/packages/pg/lib/result.js b/packages/pg/lib/result.js index 2e4fca3f8..ae98c2bff 100644 --- a/packages/pg/lib/result.js +++ b/packages/pg/lib/result.js @@ -53,8 +53,6 @@ class Result { var rawValue = rowData[i] if (rawValue !== null) { row[i] = this._parsers[i](rawValue) - } else { - row[i] = null } } return row @@ -67,8 +65,6 @@ class Result { var field = this.fields[i].name if (rawValue !== null) { row[field] = this._parsers[i](rawValue) - } else { - row[field] = null } } return row