Skip to content

Commit 9577041

Browse files
committed
[Validation] Parallelize validation rules.
This provides a performance improvement and simplification to the validator by providing two new generic visitor utilities. One for tracking a TypeInfo instance alongside a visitor instance, and another for stepping through multiple visitors in parallel. The two can be composed together. Rather than 23 passes of AST visitation with one rule each, this now performs one pass of AST visitation with 23 rules. Since visitation is costly but rules are inexpensive, this nets out to a much faster overall validation, especially noticable for very large queries.
1 parent 439a3e2 commit 9577041

File tree

2 files changed

+99
-67
lines changed

2 files changed

+99
-67
lines changed

src/language/visitor.js

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ export function visit(root, visitor, keyMap) {
208208
if (!isNode(node)) {
209209
throw new Error('Invalid AST Node: ' + JSON.stringify(node));
210210
}
211-
var visitFn = getVisitFn(visitor, isLeaving, node.kind);
211+
var visitFn = getVisitFn(visitor, node.kind, isLeaving);
212212
if (visitFn) {
213213
result = visitFn.call(visitor, node, key, parent, path, ancestors);
214214

@@ -263,7 +263,83 @@ function isNode(maybeNode) {
263263
return maybeNode && typeof maybeNode.kind === 'string';
264264
}
265265

266-
export function getVisitFn(visitor, isLeaving, kind) {
266+
267+
/**
268+
* Creates a new visitor instance which delegates to many visitors to run in
269+
* parallel. Each visitor will be visited for each node before moving on.
270+
*
271+
* Visitors must not directly modify the AST nodes and only returning false to
272+
* skip sub-branches is supported.
273+
*/
274+
export function visitInParallel(visitors) {
275+
const skipping = new Array(visitors.length);
276+
277+
return {
278+
enter(node) {
279+
for (let i = 0; i < visitors.length; i++) {
280+
if (!skipping[i]) {
281+
const fn = getVisitFn(visitors[i], node.kind, /* isLeaving */ false);
282+
if (fn) {
283+
const result = fn.apply(visitors[i], arguments);
284+
if (result === false) {
285+
skipping[i] = node;
286+
}
287+
}
288+
}
289+
}
290+
},
291+
leave(node) {
292+
for (let i = 0; i < visitors.length; i++) {
293+
if (!skipping[i]) {
294+
const fn = getVisitFn(visitors[i], node.kind, /* isLeaving */ true);
295+
if (fn) {
296+
fn.apply(visitors[i], arguments);
297+
}
298+
} else {
299+
skipping[i] = null;
300+
}
301+
}
302+
}
303+
};
304+
}
305+
306+
307+
/**
308+
* Creates a new visitor instance which maintains a provided TypeInfo instance
309+
* along with visiting visitor.
310+
*
311+
* Visitors must not directly modify the AST nodes and only returning false to
312+
* skip sub-branches is supported.
313+
*/
314+
export function visitWithTypeInfo(typeInfo, visitor) {
315+
return {
316+
enter(node) {
317+
typeInfo.enter(node);
318+
const fn = getVisitFn(visitor, node.kind, /* isLeaving */ false);
319+
if (fn) {
320+
const result = fn.apply(visitor, arguments);
321+
if (result === false) {
322+
typeInfo.leave(node);
323+
return false;
324+
}
325+
}
326+
},
327+
leave(node) {
328+
const fn = getVisitFn(visitor, node.kind, /* isLeaving */ true);
329+
if (fn) {
330+
fn.apply(visitor, arguments);
331+
}
332+
typeInfo.leave(node);
333+
}
334+
};
335+
}
336+
337+
338+
/**
339+
* Given a visitor instance, if it is leaving or not, and a node kind, return
340+
* the function the visitor runtime should call.
341+
*/
342+
function getVisitFn(visitor, kind, isLeaving) {
267343
var kindVisitor = visitor[kind];
268344
if (kindVisitor) {
269345
if (!isLeaving && typeof kindVisitor === 'function') {
@@ -275,18 +351,18 @@ export function getVisitFn(visitor, isLeaving, kind) {
275351
// { Kind: { enter() {}, leave() {} } }
276352
return kindSpecificVisitor;
277353
}
278-
return;
279-
}
280-
var specificVisitor = isLeaving ? visitor.leave : visitor.enter;
281-
if (specificVisitor) {
282-
if (typeof specificVisitor === 'function') {
283-
// { enter() {}, leave() {} }
284-
return specificVisitor;
285-
}
286-
var specificKindVisitor = specificVisitor[kind];
287-
if (typeof specificKindVisitor === 'function') {
288-
// { enter: { Kind() {} }, leave: { Kind() {} } }
289-
return specificKindVisitor;
354+
} else {
355+
var specificVisitor = isLeaving ? visitor.leave : visitor.enter;
356+
if (specificVisitor) {
357+
if (typeof specificVisitor === 'function') {
358+
// { enter() {}, leave() {} }
359+
return specificVisitor;
360+
}
361+
var specificKindVisitor = specificVisitor[kind];
362+
if (typeof specificKindVisitor === 'function') {
363+
// { enter: { Kind() {} }, leave: { Kind() {} } }
364+
return specificKindVisitor;
365+
}
290366
}
291367
}
292368
}

src/validation/validate.js

Lines changed: 9 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import invariant from '../jsutils/invariant';
1212
import { GraphQLError } from '../error';
13-
import { visit, getVisitFn } from '../language/visitor';
13+
import { visit, visitInParallel, visitWithTypeInfo } from '../language/visitor';
1414
import * as Kind from '../language/kinds';
1515
import type {
1616
Document,
@@ -74,47 +74,10 @@ export function visitUsingRules(
7474
documentAST: Document,
7575
rules: Array<any>
7676
): Array<GraphQLError> {
77-
var context = new ValidationContext(schema, documentAST, typeInfo);
78-
79-
function visitInstance(ast, instance) {
80-
visit(ast, {
81-
enter(node) {
82-
// Collect type information about the current position in the AST.
83-
typeInfo.enter(node);
84-
85-
// Get the visitor function from the validation instance, and if it
86-
// exists, call it with the visitor arguments.
87-
var enter = getVisitFn(instance, false, node.kind);
88-
var result = enter ? enter.apply(instance, arguments) : undefined;
89-
90-
// If the result is "false", we're not visiting any descendent nodes,
91-
// but need to update typeInfo.
92-
if (result === false) {
93-
typeInfo.leave(node);
94-
}
95-
96-
return result;
97-
},
98-
leave(node) {
99-
// Get the visitor function from the validation instance, and if it
100-
// exists, call it with the visitor arguments.
101-
var leave = getVisitFn(instance, true, node.kind);
102-
var result = leave ? leave.apply(instance, arguments) : undefined;
103-
104-
// Update typeInfo.
105-
typeInfo.leave(node);
106-
107-
return result;
108-
}
109-
});
110-
}
111-
77+
const context = new ValidationContext(schema, documentAST, typeInfo);
78+
const visitors = rules.map(rule => rule(context));
11279
// Visit the whole document with each instance of all provided rules.
113-
var instances = rules.map(rule => rule(context));
114-
instances.forEach(instance => {
115-
visitInstance(documentAST, instance);
116-
});
117-
80+
visit(documentAST, visitWithTypeInfo(typeInfo, visitInParallel(visitors)));
11881
return context.getErrors();
11982
}
12083

@@ -233,19 +196,12 @@ export class ValidationContext {
233196
if (!usages) {
234197
usages = [];
235198
const typeInfo = new TypeInfo(this._schema);
236-
visit(node, {
237-
enter(subnode) {
238-
typeInfo.enter(subnode);
239-
if (subnode.kind === Kind.VARIABLE_DEFINITION) {
240-
return false;
241-
} else if (subnode.kind === Kind.VARIABLE) {
242-
usages.push({ node: subnode, type: typeInfo.getInputType() });
243-
}
244-
},
245-
leave(subnode) {
246-
typeInfo.leave(subnode);
199+
visit(node, visitWithTypeInfo(typeInfo, {
200+
VariableDefinition: () => false,
201+
Variable(variable) {
202+
usages.push({ node: variable, type: typeInfo.getInputType() });
247203
}
248-
});
204+
}));
249205
this._variableUsages.set(node, usages);
250206
}
251207
return usages;

0 commit comments

Comments
 (0)