Skip to content

Commit

Permalink
perf: optimize JSON direct syntactic analysis
Browse files Browse the repository at this point in the history
Refs #691
  • Loading branch information
char0n committed Oct 25, 2021
1 parent 5e106c2 commit ad3d886
Show file tree
Hide file tree
Showing 11 changed files with 51 additions and 54 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules/
.idea/
*.iml
.vscode/settings.json
isolate-*.log
24 changes: 10 additions & 14 deletions packages/apidom-ast/src/visitor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { prop, pipe, curryN } from 'ramda';
import { isFunction, isString, isNotNil } from 'ramda-adjunct';

/**
* SPDX-FileCopyrightText: Copyright (c) GraphQL Contributors
*
Expand All @@ -11,25 +8,25 @@ import { isFunction, isString, isNotNil } from 'ramda-adjunct';
export const getVisitFn = (visitor: any, type: string, isLeaving: boolean) => {
const typeVisitor = visitor[type];

if (isNotNil(typeVisitor)) {
if (!isLeaving && isFunction(typeVisitor)) {
if (typeVisitor != null) {
if (!isLeaving && typeof typeVisitor === 'function') {
// { Type() {} }
return typeVisitor;
}
const typeSpecificVisitor = isLeaving ? typeVisitor.leave : typeVisitor.enter;
if (isFunction(typeSpecificVisitor)) {
if (typeof typeSpecificVisitor === 'function') {
// { Type: { enter() {}, leave() {} } }
return typeSpecificVisitor;
}
} else {
const specificVisitor = isLeaving ? visitor.leave : visitor.enter;
if (isNotNil(specificVisitor)) {
if (isFunction(specificVisitor)) {
if (specificVisitor != null) {
if (typeof specificVisitor === 'function') {
// { enter() {}, leave() {} }
return specificVisitor;
}
const specificTypeVisitor = specificVisitor[type];
if (isFunction(specificTypeVisitor)) {
if (typeof specificTypeVisitor === 'function') {
// { enter: { Type() {} }, leave: { Type() {} } }
return specificTypeVisitor;
}
Expand All @@ -42,11 +39,10 @@ export const getVisitFn = (visitor: any, type: string, isLeaving: boolean) => {
export const BREAK = {};

// getNodeType :: Node -> String
export const getNodeType = prop('type');
export const getNodeType = (node: any) => node?.type;

// isNode :: Node -> Boolean
// @ts-ignore
export const isNode = curryN(1, pipe(getNodeType, isString));
export const isNode = (node: any) => typeof getNodeType(node) === 'string';

/**
* Creates a new visitor instance which delegates to many visitors to run in
Expand All @@ -65,7 +61,7 @@ export const mergeAll = (
for (let i = 0; i < visitors.length; i += 1) {
if (skipping[i] == null) {
const fn = visitFnGetter(visitors[i], nodeTypeGetter(node), /* isLeaving */ false);
if (isFunction(fn)) {
if (typeof fn === 'function') {
const result = fn.call(visitors[i], node, ...rest);
if (result === false) {
skipping[i] = node;
Expand All @@ -83,7 +79,7 @@ export const mergeAll = (
for (let i = 0; i < visitors.length; i += 1) {
if (skipping[i] == null) {
const fn = visitFnGetter(visitors[i], nodeTypeGetter(node), /* isLeaving */ true);
if (isFunction(fn)) {
if (typeof fn === 'function') {
const result = fn.call(visitors[i], node, ...rest);
if (result === BREAK) {
skipping[i] = BREAK;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const options = {
name: 'parse',
defer: true,
minSamples: 600,
expected: '17.21 ops/sec ±2.09% (646 runs sampled)',
expected: '41.22 ops/sec ±1.71% (658 runs sampled)',
async fn(deferred) {
await parse(source);
deferred.resolve();
Expand All @@ -23,10 +23,10 @@ const options = {
/**
* # Analysis of ApiDOM stages
*
* Parse stage: 58,1 ms
* Parse stage: 14,56 ms
* Lexical Analysis phase: 0,77 ms
* Syntactic Analysis phase: 46,49 ms
* Refract stage: 2,85 ms
* Syntactic Analysis phase: 8,13 ms
* Refract stage: 9,7 ms
*/

module.exports = options;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ require('@babel/register')({ extensions: ['.js', '.ts'], rootMode: 'upward' });
const fs = require('fs');
const path = require('path');
const Benchmark = require('benchmark');
const { ObjectElement } = require('apidom-core');
const { AsyncApi2Element } = require('apidom-ns-asyncapi-2');
const { ObjectElement } = require('@swagger-api/apidom-core');
const { AsyncApi2Element } = require('@swagger-api/apidom-ns-asyncapi-2');

const fixturePath = path.join(__dirname, 'fixtures/asyncapi.json');
const source = fs.readFileSync(fixturePath).toString();
Expand All @@ -14,7 +14,7 @@ const genericObjectElement = new ObjectElement(pojo);
const options = {
name: 'refract',
minSamples: 600,
expected: '350 ops/sec ±1.29% (679 runs sampled)',
expected: '103 ops/sec ±0.92% (668 runs sampled)',
fn() {
AsyncApi2Element.refract(genericObjectElement);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const options = {
name: 'syntactic-analysis',
defer: true,
minSamples: 600,
expected: '21.51 ops/sec ±1.97% (642 runs sampled)',
expected: '123 ops/sec ±1.38% (676 runs sampled)',
async fn(deferred) {
const cst = await cstP;
syntacticAnalysis(cst);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import stampit from 'stampit';
import { either, hasIn, prop } from 'ramda';
import { invokeArgs, isFalse, isFunction } from 'ramda-adjunct';
import { Tree as NodeTree, SyntaxNode as NodeSyntaxNode } from 'tree-sitter';
import { Tree as WebTree, SyntaxNode as WebSyntaxNode } from 'web-tree-sitter';
import { visit, getNodeType as getCSTNodeType, isNode as isCSTNode } from '@swagger-api/apidom-ast';
Expand Down Expand Up @@ -50,7 +48,7 @@ const getNodeType = (node: any) => {
};

// @ts-ignore
const isNode = either(isElement, isCSTNode);
const isNode = (element: any) => isElement(element) || isCSTNode(element);

const Visitor = stampit({
props: {
Expand Down Expand Up @@ -101,11 +99,11 @@ const Visitor = stampit({
};

const getFieldFromNode = (fieldName: string, node: SyntaxNode): SyntaxNode | null => {
return hasIn(`${fieldName}Node`, node)
return `${fieldName}Node` in node
? // @ts-ignore
prop(`${fieldName}Node`, node)
: hasIn('childForFieldName', node)
? invokeArgs(['childForFieldName'], [fieldName], node)
node[`${fieldName}Node`]
: 'childForFieldName' in node
? node.childForFieldName?.(fieldName)
: null;
};

Expand All @@ -119,7 +117,7 @@ const Visitor = stampit({
// in `SyntaxNode.isNamed` property. web-tree-sitter has it defined as method
// whether tree-sitter node binding has it defined as a boolean property.
if (
((isFunction(node.isNamed) && !node.isNamed()) || isFalse(node.isNamed)) &&
((typeof node.isNamed === 'function' && !node.isNamed()) || node.isNamed === false) &&
node.isMissing()
) {
// collect annotations from missing literals
Expand Down Expand Up @@ -185,23 +183,24 @@ const Visitor = stampit({
element.content.value = getFieldFromNode('value', node);
maybeAddSourceMap(node, element);

// process possible errors here that may be present in pair node children as we're using direct field access
node.children
// @ts-ignore
.filter((child: SyntaxNode) => child.type === 'ERROR')
.forEach((errorNode: SyntaxNode) => {
this.ERROR(errorNode, node, [], [node]);
});
/**
* Process possible errors here that may be present in pair node children as we're using direct field access.
* There are usually 3 children here found: "key", ":", "value".
*/
if (node.children.length > 3) {
node.children
// @ts-ignore
.filter((child: SyntaxNode) => child.type === 'ERROR')
.forEach((errorNode: SyntaxNode) => {
this.ERROR(errorNode, node, [], [node]);
});
}

return element;
};

this.string = function string(node: SyntaxNode) {
// @ts-ignore
const content = node.namedChildren.reduce((acc, child) => `${acc}${child.text}`, '');
const element = new StringElement(content);
maybeAddSourceMap(node, element);
return element;
return new StringElement(node.text.slice(1, -1));
};

this.number = function number(node: SyntaxNode) {
Expand Down Expand Up @@ -268,6 +267,7 @@ const analyze = (cst: Tree, { sourceMap = false } = {}): ParseResultElement => {
keyMap,
// @ts-ignore
nodeTypeGetter: getNodeType,
// @ts-ignore
nodePredicate: isNode,
state: {
sourceMap,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ const source = fs.readFileSync(fixturePath).toString();
const options = {
name: 'parse-syntactic-analysis-direct',
defer: true,
minSamples: 600,
expected: '12.69 ops/sec ±1.02% (657 runs sampled)',
minSamples: 200,
expected: '45.50 ops/sec ±1.23% (669 runs sampled)',
async fn(deferred) {
await parse(source, { syntacticAnalysis: 'direct' });
deferred.resolve();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const options = {
name: 'lexical-analysis',
defer: true,
minSamples: 1400,
expected: '700 ops/sec ±2.15% (1476 runs sampled)',
expected: '780 ops/sec ±1.18% (1474 runs sampled)',
async fn(deferred) {
await lexicalAnalysis(source);
deferred.resolve();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const options = {
name: 'parse',
defer: true,
minSamples: 600,
expected: '8.88 ops/sec ±1.09% (642 runs sampled)',
expected: '25.06 ops/sec ±1.16% (656 runs sampled)',
async fn(deferred) {
await parse(source);
deferred.resolve();
Expand All @@ -23,12 +23,12 @@ const options = {
/**
* # Analysis of ApiDOM stages
*
* Parse stage: 112,61 ms
* Lexical Analysis phase: 1,42 ms
* Syntactic Analysis phase: 85,91 ms
* Traversing time: 0,27 ms
* Building time: 85,64 ms
* Refract stage: 18,18 ms
* Parse stage: 24,15 ms
* Lexical Analysis phase: 1,28 ms
* Syntactic Analysis phase: 14,99 ms
* Traversing time: 0,59 ms
* Building time: 14,39 ms
* Refract stage: 15,75 ms
*/

module.exports = options;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const genericObjectElement = new ObjectElement(pojo);
const options = {
name: 'refract',
minSamples: 600,
expected: '55.02 ops/sec ±1.39% (651 runs sampled)',
expected: '63.49 ops/sec ±1.08% (660 runs sampled)',
fn() {
OpenApi3_1Element.refract(genericObjectElement);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const options = {
name: 'syntactic-analysis',
defer: true,
minSamples: 600,
expected: '11.64 ops/sec ±1.38% (654 runs sampled)',
expected: '66.69 ops/sec ±1.30% (671 runs sampled)',
async fn(deferred) {
const cst = await cstP;
syntacticAnalysis(cst);
Expand Down

0 comments on commit ad3d886

Please sign in to comment.