Skip to content

Commit f4efee9

Browse files
visitor: speed up visitInParallel by dropping support for unknown nodes (#3270)
1 parent 0091421 commit f4efee9

File tree

7 files changed

+157
-102
lines changed

7 files changed

+157
-102
lines changed

benchmark/visit-benchmark.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use strict';
2+
3+
const { parse } = require('graphql/language/parser.js');
4+
const { visit } = require('graphql/language/visitor.js');
5+
6+
const { bigSchemaSDL } = require('./fixtures.js');
7+
8+
const documentAST = parse(bigSchemaSDL);
9+
10+
const visitor = {
11+
enter() {
12+
/* do nothing */
13+
},
14+
leave() {
15+
/* do nothing */
16+
},
17+
};
18+
19+
module.exports = {
20+
name: 'Visit all AST nodes',
21+
count: 10,
22+
measure() {
23+
visit(documentAST, visitor);
24+
},
25+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use strict';
2+
3+
const { parse } = require('graphql/language/parser.js');
4+
const { visit, visitInParallel } = require('graphql/language/visitor.js');
5+
6+
const { bigSchemaSDL } = require('./fixtures.js');
7+
8+
const documentAST = parse(bigSchemaSDL);
9+
10+
const visitors = new Array(50).fill({
11+
enter() {
12+
/* do nothing */
13+
},
14+
leave() {
15+
/* do nothing */
16+
},
17+
});
18+
19+
module.exports = {
20+
name: 'Visit all AST nodes in parallel',
21+
count: 10,
22+
measure() {
23+
visit(documentAST, visitInParallel(visitors));
24+
},
25+
};

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ export {
209209
visit,
210210
visitInParallel,
211211
getVisitFn,
212+
getEnterLeaveForKind,
212213
BREAK,
213214
Kind,
214215
DirectiveLocation,

src/language/__tests__/visitor-test.ts

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -420,49 +420,6 @@ describe('Visitor', () => {
420420
]);
421421
});
422422

423-
it('visit nodes with unknown kinds but does not traverse deeper', () => {
424-
const customAST = parse('{ a }');
425-
// @ts-expect-error
426-
customAST.definitions[0].selectionSet.selections.push({
427-
kind: 'CustomField',
428-
name: { kind: 'Name', value: 'NamedNodeToBeSkipped' },
429-
selectionSet: {
430-
kind: 'SelectionSet',
431-
selections: [
432-
{
433-
kind: 'CustomField',
434-
name: { kind: 'Name', value: 'NamedNodeToBeSkipped' },
435-
},
436-
],
437-
},
438-
});
439-
440-
const visited: Array<any> = [];
441-
visit(customAST, {
442-
enter(node) {
443-
visited.push(['enter', node.kind, getValue(node)]);
444-
},
445-
leave(node) {
446-
visited.push(['leave', node.kind, getValue(node)]);
447-
},
448-
});
449-
450-
expect(visited).to.deep.equal([
451-
['enter', 'Document', undefined],
452-
['enter', 'OperationDefinition', undefined],
453-
['enter', 'SelectionSet', undefined],
454-
['enter', 'Field', undefined],
455-
['enter', 'Name', 'a'],
456-
['leave', 'Name', 'a'],
457-
['leave', 'Field', undefined],
458-
['enter', 'CustomField', undefined],
459-
['leave', 'CustomField', undefined],
460-
['leave', 'SelectionSet', undefined],
461-
['leave', 'OperationDefinition', undefined],
462-
['leave', 'Document', undefined],
463-
]);
464-
});
465-
466423
it('visits only the specified `Kind` in visitorKeyMap', () => {
467424
const visited: Array<any> = [];
468425

src/language/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ export type { ParseOptions } from './parser';
1818

1919
export { print } from './printer';
2020

21-
export { visit, visitInParallel, getVisitFn, BREAK } from './visitor';
21+
export {
22+
visit,
23+
visitInParallel,
24+
getVisitFn,
25+
getEnterLeaveForKind,
26+
BREAK,
27+
} from './visitor';
2228
export type { ASTVisitor, ASTVisitFn, ASTVisitorKeyMap } from './visitor';
2329

2430
export { Location, Token } from './ast';

src/language/visitor.ts

Lines changed: 96 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { inspect } from '../jsutils/inspect';
2-
import type { Maybe } from '../jsutils/Maybe';
32

43
import type { ASTNode, ASTKindToNode } from './ast';
54
import { isNode } from './ast';
5+
import { Kind } from './kinds';
66

77
/**
88
* A visitor is provided to visit, it contains the collection of
@@ -256,6 +256,15 @@ export function visit(
256256
visitor: ASTVisitor | ASTReducer<any>,
257257
visitorKeys: ASTVisitorKeyMap = QueryDocumentKeys,
258258
): any {
259+
const enterLeaveMap = new Map<
260+
keyof ASTKindToNode,
261+
EnterLeaveVisitor<ASTNode>
262+
>();
263+
264+
for (const kind of Object.values(Kind)) {
265+
enterLeaveMap.set(kind, getEnterLeaveForKind(visitor, kind));
266+
}
267+
259268
/* eslint-disable no-undef-init */
260269
let stack: any = undefined;
261270
let inArray = Array.isArray(root);
@@ -318,29 +327,30 @@ export function visit(
318327
if (!isNode(node)) {
319328
throw new Error(`Invalid AST Node: ${inspect(node)}.`);
320329
}
321-
const visitFn = getVisitFn(visitor, node.kind, isLeaving);
322-
if (visitFn) {
323-
result = visitFn.call(visitor, node, key, parent, path, ancestors);
330+
const visitFn = isLeaving
331+
? enterLeaveMap.get(node.kind)?.leave
332+
: enterLeaveMap.get(node.kind)?.enter;
324333

325-
if (result === BREAK) {
326-
break;
327-
}
334+
result = visitFn?.call(visitor, node, key, parent, path, ancestors);
335+
336+
if (result === BREAK) {
337+
break;
338+
}
328339

329-
if (result === false) {
330-
if (!isLeaving) {
340+
if (result === false) {
341+
if (!isLeaving) {
342+
path.pop();
343+
continue;
344+
}
345+
} else if (result !== undefined) {
346+
edits.push([key, result]);
347+
if (!isLeaving) {
348+
if (isNode(result)) {
349+
node = result;
350+
} else {
331351
path.pop();
332352
continue;
333353
}
334-
} else if (result !== undefined) {
335-
edits.push([key, result]);
336-
if (!isLeaving) {
337-
if (isNode(result)) {
338-
node = result;
339-
} else {
340-
path.pop();
341-
continue;
342-
}
343-
}
344354
}
345355
}
346356
}
@@ -380,16 +390,31 @@ export function visit(
380390
export function visitInParallel(
381391
visitors: ReadonlyArray<ASTVisitor>,
382392
): ASTVisitor {
383-
const skipping = new Array(visitors.length);
384-
385-
return {
386-
enter(...args) {
387-
const node = args[0];
388-
for (let i = 0; i < visitors.length; i++) {
389-
if (skipping[i] == null) {
390-
const fn = getVisitFn(visitors[i], node.kind, /* isLeaving */ false);
391-
if (fn) {
392-
const result = fn.apply(visitors[i], args);
393+
const skipping = new Array(visitors.length).fill(null);
394+
const mergedVisitor = Object.create(null);
395+
396+
for (const kind of Object.values(Kind)) {
397+
let hasVisitor = false;
398+
const enterList = new Array(visitors.length).fill(undefined);
399+
const leaveList = new Array(visitors.length).fill(undefined);
400+
401+
for (let i = 0; i < visitors.length; ++i) {
402+
const { enter, leave } = getEnterLeaveForKind(visitors[i], kind);
403+
hasVisitor ||= enter != null || leave != null;
404+
enterList[i] = enter;
405+
leaveList[i] = leave;
406+
}
407+
408+
if (!hasVisitor) {
409+
continue;
410+
}
411+
412+
const mergedEnterLeave: EnterLeaveVisitor<ASTNode> = {
413+
enter(...args) {
414+
const node = args[0];
415+
for (let i = 0; i < visitors.length; i++) {
416+
if (skipping[i] === null) {
417+
const result = enterList[i]?.apply(visitors[i], args);
393418
if (result === false) {
394419
skipping[i] = node;
395420
} else if (result === BREAK) {
@@ -399,50 +424,66 @@ export function visitInParallel(
399424
}
400425
}
401426
}
402-
}
403-
},
404-
leave(...args) {
405-
const node = args[0];
406-
for (let i = 0; i < visitors.length; i++) {
407-
if (skipping[i] == null) {
408-
const fn = getVisitFn(visitors[i], node.kind, /* isLeaving */ true);
409-
if (fn) {
410-
const result = fn.apply(visitors[i], args);
427+
},
428+
leave(...args) {
429+
const node = args[0];
430+
for (let i = 0; i < visitors.length; i++) {
431+
if (skipping[i] === null) {
432+
const result = leaveList[i]?.apply(visitors[i], args);
411433
if (result === BREAK) {
412434
skipping[i] = BREAK;
413435
} else if (result !== undefined && result !== false) {
414436
return result;
415437
}
438+
} else if (skipping[i] === node) {
439+
skipping[i] = null;
416440
}
417-
} else if (skipping[i] === node) {
418-
skipping[i] = null;
419441
}
420-
}
421-
},
422-
};
442+
},
443+
};
444+
445+
mergedVisitor[kind] = mergedEnterLeave;
446+
}
447+
448+
return mergedVisitor;
423449
}
424450

425451
/**
426-
* Given a visitor instance, if it is leaving or not, and a node kind, return
427-
* the function the visitor runtime should call.
452+
* Given a visitor instance and a node kind, return EnterLeaveVisitor for that kind.
428453
*/
429-
export function getVisitFn(
454+
export function getEnterLeaveForKind(
430455
visitor: ASTVisitor,
431456
kind: keyof ASTKindToNode,
432-
isLeaving: boolean,
433-
): Maybe<ASTVisitFn<ASTNode>> {
457+
): EnterLeaveVisitor<ASTNode> {
434458
const kindVisitor:
435459
| ASTVisitFn<ASTNode>
436460
| EnterLeaveVisitor<ASTNode>
437461
| undefined = (visitor as any)[kind];
438-
if (kindVisitor) {
439-
if (typeof kindVisitor === 'function') {
440-
// { Kind() {} }
441-
return isLeaving ? undefined : kindVisitor;
442-
}
462+
463+
if (typeof kindVisitor === 'object') {
443464
// { Kind: { enter() {}, leave() {} } }
444-
return isLeaving ? kindVisitor.leave : kindVisitor.enter;
465+
return kindVisitor;
466+
} else if (typeof kindVisitor === 'function') {
467+
// { Kind() {} }
468+
return { enter: kindVisitor, leave: undefined };
445469
}
470+
446471
// { enter() {}, leave() {} }
447-
return isLeaving ? (visitor as any).leave : (visitor as any).enter;
472+
return { enter: (visitor as any).enter, leave: (visitor as any).leave };
473+
}
474+
475+
/**
476+
* Given a visitor instance, if it is leaving or not, and a node kind, return
477+
* the function the visitor runtime should call.
478+
*
479+
* @deprecated Please use `getEnterLeaveForKind` instead. Will be removed in v17
480+
*/
481+
// istanbul ignore next (Deprecated code)
482+
export function getVisitFn(
483+
visitor: ASTVisitor,
484+
kind: keyof ASTKindToNode,
485+
isLeaving: boolean,
486+
): ASTVisitFn<ASTNode> | undefined {
487+
const { enter, leave } = getEnterLeaveForKind(visitor, kind);
488+
return isLeaving ? leave : enter;
448489
}

src/utilities/TypeInfo.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { ASTVisitor } from '../language/visitor';
22
import type { ASTNode, FieldNode } from '../language/ast';
33
import { Kind } from '../language/kinds';
44
import { isNode } from '../language/ast';
5-
import { getVisitFn } from '../language/visitor';
5+
import { getEnterLeaveForKind } from '../language/visitor';
66

77
import type { Maybe } from '../jsutils/Maybe';
88

@@ -344,7 +344,7 @@ export function visitWithTypeInfo(
344344
enter(...args) {
345345
const node = args[0];
346346
typeInfo.enter(node);
347-
const fn = getVisitFn(visitor, node.kind, /* isLeaving */ false);
347+
const fn = getEnterLeaveForKind(visitor, node.kind).enter;
348348
if (fn) {
349349
const result = fn.apply(visitor, args);
350350
if (result !== undefined) {
@@ -358,7 +358,7 @@ export function visitWithTypeInfo(
358358
},
359359
leave(...args) {
360360
const node = args[0];
361-
const fn = getVisitFn(visitor, node.kind, /* isLeaving */ true);
361+
const fn = getEnterLeaveForKind(visitor, node.kind).leave;
362362
let result;
363363
if (fn) {
364364
result = fn.apply(visitor, args);

0 commit comments

Comments
 (0)