Skip to content

Commit 07988f9

Browse files
authored
Speed up bind functionality (#2286)
Move from 3 loops (prepareValue, check for buffers, write param types, write param values) to a single loop. This speeds up the insert benchmark by around 100 queries per second. Performance improvement depends on number of parameters being bound.
1 parent 78a14a1 commit 07988f9

File tree

6 files changed

+125
-82
lines changed

6 files changed

+125
-82
lines changed

packages/pg-protocol/src/buffer-writer.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export class Writer {
55
private offset: number = 5
66
private headerPosition: number = 0
77
constructor(private size = 256) {
8-
this.buffer = Buffer.alloc(size)
8+
this.buffer = Buffer.allocUnsafe(size)
99
}
1010

1111
private ensure(size: number): void {
@@ -15,7 +15,7 @@ export class Writer {
1515
// exponential growth factor of around ~ 1.5
1616
// https://stackoverflow.com/questions/2269063/buffer-growth-strategy
1717
var newSize = oldBuffer.length + (oldBuffer.length >> 1) + size
18-
this.buffer = Buffer.alloc(newSize)
18+
this.buffer = Buffer.allocUnsafe(newSize)
1919
oldBuffer.copy(this.buffer)
2020
}
2121
}

packages/pg-protocol/src/outbound-serializer.test.ts

+29
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ describe('serializer', () => {
110110
var expectedBuffer = new BufferList()
111111
.addCString('bang') // portal name
112112
.addCString('woo') // statement name
113+
.addInt16(4)
114+
.addInt16(0)
115+
.addInt16(0)
116+
.addInt16(0)
113117
.addInt16(0)
114118
.addInt16(4)
115119
.addInt32(1)
@@ -125,6 +129,31 @@ describe('serializer', () => {
125129
})
126130
})
127131

132+
it('with custom valueMapper', function () {
133+
const actual = serialize.bind({
134+
portal: 'bang',
135+
statement: 'woo',
136+
values: ['1', 'hi', null, 'zing'],
137+
valueMapper: () => null,
138+
})
139+
var expectedBuffer = new BufferList()
140+
.addCString('bang') // portal name
141+
.addCString('woo') // statement name
142+
.addInt16(4)
143+
.addInt16(0)
144+
.addInt16(0)
145+
.addInt16(0)
146+
.addInt16(0)
147+
.addInt16(4)
148+
.addInt32(-1)
149+
.addInt32(-1)
150+
.addInt32(-1)
151+
.addInt32(-1)
152+
.addInt16(0)
153+
.join(true, 'B')
154+
assert.deepEqual(actual, expectedBuffer)
155+
})
156+
128157
it('with named statement, portal, and buffer value', function () {
129158
const actual = serialize.bind({
130159
portal: 'bang',

packages/pg-protocol/src/serializer.ts

+45-35
Original file line numberDiff line numberDiff line change
@@ -101,56 +101,66 @@ const parse = (query: ParseOpts): Buffer => {
101101
return writer.flush(code.parse)
102102
}
103103

104+
type ValueMapper = (param: any, index: number) => any
105+
104106
type BindOpts = {
105107
portal?: string
106108
binary?: boolean
107109
statement?: string
108110
values?: any[]
111+
// optional map from JS value to postgres value per parameter
112+
valueMapper?: ValueMapper
113+
}
114+
115+
const paramWriter = new Writer()
116+
117+
// make this a const enum so typescript will inline the value
118+
const enum ParamType {
119+
STRING = 0,
120+
BINARY = 1,
121+
}
122+
123+
const writeValues = function (values: any[], valueMapper?: ValueMapper): void {
124+
for (let i = 0; i < values.length; i++) {
125+
const mappedVal = valueMapper ? valueMapper(values[i], i) : values[i]
126+
if (mappedVal == null) {
127+
// add the param type (string) to the writer
128+
writer.addInt16(ParamType.STRING)
129+
// write -1 to the param writer to indicate null
130+
paramWriter.addInt32(-1)
131+
} else if (mappedVal instanceof Buffer) {
132+
// add the param type (binary) to the writer
133+
writer.addInt16(ParamType.BINARY)
134+
// add the buffer to the param writer
135+
paramWriter.addInt32(mappedVal.length)
136+
paramWriter.add(mappedVal)
137+
} else {
138+
// add the param type (string) to the writer
139+
writer.addInt16(ParamType.STRING)
140+
paramWriter.addInt32(Buffer.byteLength(mappedVal))
141+
paramWriter.addString(mappedVal)
142+
}
143+
}
109144
}
110145

111146
const bind = (config: BindOpts = {}): Buffer => {
112147
// normalize config
113148
const portal = config.portal || ''
114149
const statement = config.statement || ''
115150
const binary = config.binary || false
116-
var values = config.values || emptyArray
117-
var len = values.length
151+
const values = config.values || emptyArray
152+
const len = values.length
118153

119-
var useBinary = false
120-
// TODO(bmc): all the loops in here aren't nice, we can do better
121-
for (var j = 0; j < len; j++) {
122-
useBinary = useBinary || values[j] instanceof Buffer
123-
}
154+
writer.addCString(portal).addCString(statement)
155+
writer.addInt16(len)
124156

125-
var buffer = writer.addCString(portal).addCString(statement)
126-
if (!useBinary) {
127-
buffer.addInt16(0)
128-
} else {
129-
buffer.addInt16(len)
130-
for (j = 0; j < len; j++) {
131-
buffer.addInt16(values[j] instanceof Buffer ? 1 : 0)
132-
}
133-
}
134-
buffer.addInt16(len)
135-
for (var i = 0; i < len; i++) {
136-
var val = values[i]
137-
if (val === null || typeof val === 'undefined') {
138-
buffer.addInt32(-1)
139-
} else if (val instanceof Buffer) {
140-
buffer.addInt32(val.length)
141-
buffer.add(val)
142-
} else {
143-
buffer.addInt32(Buffer.byteLength(val))
144-
buffer.addString(val)
145-
}
146-
}
157+
writeValues(values, config.valueMapper)
147158

148-
if (binary) {
149-
buffer.addInt16(1) // format codes to use binary
150-
buffer.addInt16(1)
151-
} else {
152-
buffer.addInt16(0) // format codes to use text
153-
}
159+
writer.addInt16(len)
160+
writer.add(paramWriter.flush())
161+
162+
// format code
163+
writer.addInt16(binary ? ParamType.BINARY : ParamType.STRING)
154164
return writer.flush(code.bind)
155165
}
156166

packages/pg/bench.js

+31-28
Original file line numberDiff line numberDiff line change
@@ -45,37 +45,40 @@ const run = async () => {
4545
console.log('warmup done')
4646
const seconds = 5
4747

48-
let queries = await bench(client, params, seconds * 1000)
49-
console.log('')
50-
console.log('little queries:', queries)
51-
console.log('qps', queries / seconds)
52-
console.log('on my laptop best so far seen 733 qps')
48+
for (let i = 0; i < 4; i++) {
49+
let queries = await bench(client, params, seconds * 1000)
50+
console.log('')
51+
console.log('little queries:', queries)
52+
console.log('qps', queries / seconds)
53+
console.log('on my laptop best so far seen 733 qps')
5354

54-
console.log('')
55-
queries = await bench(client, seq, seconds * 1000)
56-
console.log('sequence queries:', queries)
57-
console.log('qps', queries / seconds)
58-
console.log('on my laptop best so far seen 1309 qps')
55+
console.log('')
56+
queries = await bench(client, seq, seconds * 1000)
57+
console.log('sequence queries:', queries)
58+
console.log('qps', queries / seconds)
59+
console.log('on my laptop best so far seen 1309 qps')
5960

60-
console.log('')
61-
queries = await bench(client, insert, seconds * 1000)
62-
console.log('insert queries:', queries)
63-
console.log('qps', queries / seconds)
64-
console.log('on my laptop best so far seen 6303 qps')
61+
console.log('')
62+
queries = await bench(client, insert, seconds * 1000)
63+
console.log('insert queries:', queries)
64+
console.log('qps', queries / seconds)
65+
console.log('on my laptop best so far seen 6445 qps')
6566

66-
console.log('')
67-
console.log('Warming up bytea test')
68-
await client.query({
69-
text: 'INSERT INTO buf(name, data) VALUES ($1, $2)',
70-
values: ['test', Buffer.allocUnsafe(104857600)],
71-
})
72-
console.log('bytea warmup done')
73-
const start = Date.now()
74-
const results = await client.query('SELECT * FROM buf')
75-
const time = Date.now() - start
76-
console.log('bytea time:', time, 'ms')
77-
console.log('bytea length:', results.rows[0].data.byteLength, 'bytes')
78-
console.log('on my laptop best so far seen 1107ms and 104857600 bytes')
67+
console.log('')
68+
console.log('Warming up bytea test')
69+
await client.query({
70+
text: 'INSERT INTO buf(name, data) VALUES ($1, $2)',
71+
values: ['test', Buffer.allocUnsafe(104857600)],
72+
})
73+
console.log('bytea warmup done')
74+
const start = Date.now()
75+
const results = await client.query('SELECT * FROM buf')
76+
const time = Date.now() - start
77+
console.log('bytea time:', time, 'ms')
78+
console.log('bytea length:', results.rows[0].data.byteLength, 'bytes')
79+
console.log('on my laptop best so far seen 1107ms and 104857600 bytes')
80+
await new Promise((resolve) => setTimeout(resolve, 250))
81+
}
7982

8083
await client.end()
8184
await client.end()

packages/pg/lib/query.js

+14-14
Original file line numberDiff line numberDiff line change
@@ -197,22 +197,22 @@ class Query extends EventEmitter {
197197
})
198198
}
199199

200-
if (this.values) {
201-
try {
202-
this.values = this.values.map(utils.prepareValue)
203-
} catch (err) {
204-
this.handleError(err, connection)
205-
return
206-
}
200+
// because we're mapping user supplied values to
201+
// postgres wire protocol compatible values it could
202+
// throw an exception, so try/catch this section
203+
try {
204+
connection.bind({
205+
portal: this.portal,
206+
statement: this.name,
207+
values: this.values,
208+
binary: this.binary,
209+
valueMapper: utils.prepareValue,
210+
})
211+
} catch (err) {
212+
this.handleError(err, connection)
213+
return
207214
}
208215

209-
connection.bind({
210-
portal: this.portal,
211-
statement: this.name,
212-
values: this.values,
213-
binary: this.binary,
214-
})
215-
216216
connection.describe({
217217
type: 'P',
218218
name: this.portal || '',

packages/pg/lib/utils.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ function arrayString(val) {
3838
// note: you can override this function to provide your own conversion mechanism
3939
// for complex types, etc...
4040
var prepareValue = function (val, seen) {
41+
// null and undefined are both null for postgres
42+
if (val == null) {
43+
return null
44+
}
4145
if (val instanceof Buffer) {
4246
return val
4347
}
@@ -58,9 +62,6 @@ var prepareValue = function (val, seen) {
5862
if (Array.isArray(val)) {
5963
return arrayString(val)
6064
}
61-
if (val === null || typeof val === 'undefined') {
62-
return null
63-
}
6465
if (typeof val === 'object') {
6566
return prepareObject(val, seen)
6667
}

0 commit comments

Comments
 (0)