Skip to content

Commit 5bca05b

Browse files
authored
perf: a number of performance optimisations (#458)
* Precompute for performance * Use prepared statements * Cache context * More efficent 'beforeLocks' * Fast loop * Debug branching * Cache hOP * Marginally faster checks * Faster loops * Memo of size one * FieldNode always has a name * Make prepared statement cache configurable * graphql-parse-resolve-info turbo * One fewer closure * Upgrade pg-sql2 * Avoid ||= * Turbo entry point
1 parent 400dc75 commit 5bca05b

20 files changed

+337
-158
lines changed

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
node_modules
22
node7minus
33
node8plus
4+
build-turbo
45
examples
56
dist

babel.config.js

+30-21
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
11
module.exports = {
2-
"plugins": ["@babel/plugin-transform-modules-commonjs", "@babel/plugin-syntax-object-rest-spread"],
3-
"presets": [
4-
["@babel/env", {
5-
"targets": {
6-
"node": "8.6"
7-
}
8-
}],
9-
"@babel/flow"
2+
plugins: [
3+
"@babel/plugin-transform-modules-commonjs",
4+
"@babel/plugin-syntax-object-rest-spread",
105
],
11-
"env": {
12-
"test": {
13-
"presets": [
14-
["@babel/env", {
15-
"targets": {
16-
"node": "current"
17-
}
18-
}],
19-
"@babel/flow"
20-
]
21-
}
22-
}
23-
}
6+
presets: [
7+
[
8+
"@babel/env",
9+
{
10+
targets: {
11+
node: "8.6",
12+
},
13+
},
14+
],
15+
"@babel/flow",
16+
],
17+
env: {
18+
test: {
19+
presets: [
20+
[
21+
"@babel/env",
22+
{
23+
targets: {
24+
node: "current",
25+
},
26+
},
27+
],
28+
"@babel/flow",
29+
],
30+
},
31+
},
32+
};

packages/graphile-build-pg/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"jsonwebtoken": "^8.5.1",
4545
"lodash": ">=4 <5",
4646
"lru-cache": ">=4 <5",
47-
"pg-sql2": "2.2.2",
47+
"pg-sql2": "^2.2.3",
4848
"postgres-interval": "^1.2.0"
4949
},
5050
"peerDependencies": {

packages/graphile-build-pg/src/QueryBuilder.js

+18-21
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ class QueryBuilder {
100100
last: ?number,
101101
cursorComparator: ?CursorComparator,
102102
};
103+
lockContext: {
104+
queryBuilder: QueryBuilder,
105+
};
103106

104107
constructor(
105108
options: QueryBuilderOptions = {},
@@ -182,6 +185,9 @@ class QueryBuilder {
182185
this.lock("limit");
183186
this.lock("offset");
184187
});
188+
this.lockContext = Object.freeze({
189+
queryBuilder: this,
190+
});
185191
}
186192

187193
// ----------------------------------------
@@ -702,14 +708,13 @@ class QueryBuilder {
702708
}
703709
lock(type: string) {
704710
if (this.locks[type]) return;
705-
const getContext = () => ({
706-
queryBuilder: this,
707-
});
708-
const beforeLocks = this.data.beforeLock[type];
709-
if (beforeLocks && beforeLocks.length) {
710-
this.data.beforeLock[type] = null;
711-
for (const fn of beforeLocks) {
712-
fn();
711+
const context = this.lockContext;
712+
const { beforeLock } = this.data;
713+
let locks = beforeLock[type];
714+
if (locks) {
715+
beforeLock[type] = [];
716+
for (let i = 0, l = locks.length; i < l; i++) {
717+
locks[i]();
713718
}
714719
}
715720
if (type !== "select") {
@@ -720,7 +725,6 @@ class QueryBuilder {
720725
this.compiledData[type] = this.data[type];
721726
} else if (type === "whereBound") {
722727
// Handle properties separately
723-
const context = getContext();
724728
this.compiledData[type].lower = callIfNecessaryArray(
725729
this.data[type].lower,
726730
context
@@ -739,7 +743,6 @@ class QueryBuilder {
739743
// Assume that duplicate fields must be identical, don't output the same
740744
// key multiple times
741745
const seenFields = {};
742-
const context = getContext();
743746
const data = [];
744747
const selects = this.data[type];
745748

@@ -751,19 +754,18 @@ class QueryBuilder {
751754
// $FlowFixMe
752755
seenFields[columnName] = true;
753756
data.push([callIfNecessary(valueOrGenerator, context), columnName]);
754-
const newBeforeLocks = this.data.beforeLock[type];
755-
if (newBeforeLocks && newBeforeLocks.length) {
756-
this.data.beforeLock[type] = null;
757-
for (const fn of newBeforeLocks) {
758-
fn();
757+
locks = beforeLock[type];
758+
if (locks) {
759+
beforeLock[type] = [];
760+
for (let i = 0, l = locks.length; i < l; i++) {
761+
locks[i]();
759762
}
760763
}
761764
}
762765
}
763766
this.locks[type] = isDev ? new Error("Initally locked here").stack : true;
764767
this.compiledData[type] = data;
765768
} else if (type === "orderBy") {
766-
const context = getContext();
767769
this.compiledData[type] = this.data[type].map(([a, b, c]) => [
768770
callIfNecessary(a, context),
769771
b,
@@ -772,24 +774,19 @@ class QueryBuilder {
772774
} else if (type === "from") {
773775
if (this.data.from) {
774776
const f = this.data.from;
775-
const context = getContext();
776777
this.compiledData.from = [callIfNecessary(f[0], context), f[1]];
777778
}
778779
} else if (type === "join" || type === "where") {
779-
const context = getContext();
780780
this.compiledData[type] = callIfNecessaryArray(this.data[type], context);
781781
} else if (type === "selectCursor") {
782-
const context = getContext();
783782
this.compiledData[type] = callIfNecessary(this.data[type], context);
784783
} else if (type === "cursorPrefix") {
785784
this.compiledData[type] = this.data[type];
786785
} else if (type === "orderIsUnique") {
787786
this.compiledData[type] = this.data[type];
788787
} else if (type === "limit") {
789-
const context = getContext();
790788
this.compiledData[type] = callIfNecessary(this.data[type], context);
791789
} else if (type === "offset") {
792-
const context = getContext();
793790
this.compiledData[type] = callIfNecessary(this.data[type], context);
794791
} else if (type === "first") {
795792
this.compiledData[type] = this.data[type];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//@flow
2+
import { createHash } from "crypto";
3+
import LRU from "lru-cache";
4+
import type { PoolClient } from "pg";
5+
6+
const POSTGRAPHILE_PREPARED_STATEMENT_CACHE_SIZE =
7+
parseInt(process.env.POSTGRAPHILE_PREPARED_STATEMENT_CACHE_SIZE, 10) || 100;
8+
9+
let lastString: string;
10+
let lastHash: string;
11+
const hash = (str: string): string => {
12+
if (str !== lastString) {
13+
lastString = str;
14+
lastHash = createHash("sha1")
15+
.update(str)
16+
.digest("base64");
17+
}
18+
return lastHash;
19+
};
20+
21+
export default function pgPrepareAndRun(
22+
pgClient: PoolClient,
23+
text: string,
24+
// eslint-disable-next-line flowtype/no-weak-types
25+
values: any
26+
) {
27+
const connection = pgClient.connection;
28+
if (
29+
!values ||
30+
POSTGRAPHILE_PREPARED_STATEMENT_CACHE_SIZE < 1 ||
31+
!connection ||
32+
!connection.parsedStatements
33+
) {
34+
return pgClient.query(text, values);
35+
} else {
36+
const name = hash(text);
37+
if (!connection._graphilePreparedStatementCache) {
38+
connection._graphilePreparedStatementCache = LRU({
39+
max: POSTGRAPHILE_PREPARED_STATEMENT_CACHE_SIZE,
40+
dispose(key) {
41+
if (connection.parsedStatements[key]) {
42+
pgClient
43+
.query(`deallocate ${pgClient.escapeIdentifier(key)}`)
44+
.then(() => {
45+
delete connection.parsedStatements[key];
46+
})
47+
.catch(e => {
48+
// eslint-disable-next-line no-console
49+
console.error("Error releasing prepared query", e);
50+
});
51+
}
52+
},
53+
});
54+
}
55+
if (!connection._graphilePreparedStatementCache.get(name)) {
56+
// We're relying on dispose to clear out the old ones.
57+
connection._graphilePreparedStatementCache.set(name, true);
58+
}
59+
return pgClient.query({
60+
name,
61+
text,
62+
values,
63+
});
64+
}
65+
}

packages/graphile-build-pg/src/plugins/PgAllRows.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export default (async function PgAllRows(
2222
pgQueryFromResolveData: queryFromResolveData,
2323
pgAddStartEndCursor: addStartEndCursor,
2424
pgOmit: omit,
25+
pgPrepareAndRun,
2526
} = build;
2627
const {
2728
fieldWithHooks,
@@ -163,7 +164,11 @@ export default (async function PgAllRows(
163164
);
164165
const { text, values } = sql.compile(query);
165166
if (debugSql.enabled) debugSql(text);
166-
const result = await pgClient.query(text, values);
167+
const result = await pgPrepareAndRun(
168+
pgClient,
169+
text,
170+
values
171+
);
167172

168173
const liveCollection =
169174
resolveInfo.rootValue &&

packages/graphile-build-pg/src/plugins/PgBasicsPlugin.js

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import viaTemporaryTable from "./viaTemporaryTable";
3131
import chalk from "chalk";
3232
import pickBy from "lodash/pickBy";
3333
import PgLiveProvider from "../PgLiveProvider";
34+
import pgPrepareAndRun from "../pgPrepareAndRun";
3435

3536
const defaultPgColumnFilter = (_attr, _build, _context) => true;
3637
type Keys = Array<{
@@ -352,6 +353,7 @@ export default (function PgBasicsPlugin(
352353
describePgEntity,
353354
pgField,
354355
sqlCommentByAddingTags,
356+
pgPrepareAndRun,
355357
});
356358
},
357359
["PgBasics"]

packages/graphile-build-pg/src/plugins/PgConnectionArgOrderBy.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,12 @@ export default (function PgConnectionArgOrderBy(builder, { orderByNullsLast }) {
112112
const cursorPrefixFromOrderBy = orderBy => {
113113
if (orderBy) {
114114
let cursorPrefixes = [];
115-
for (const item of orderBy) {
115+
for (
116+
let itemIndex = 0, itemCount = orderBy.length;
117+
itemIndex < itemCount;
118+
itemIndex++
119+
) {
120+
const item = orderBy[itemIndex];
116121
if (item.alias) {
117122
cursorPrefixes.push(sql.literal(item.alias));
118123
}

0 commit comments

Comments
 (0)