Skip to content

Commit

Permalink
perf: add a few performance optimizations
Browse files Browse the repository at this point in the history
  • Loading branch information
boopathi committed Oct 10, 2024
1 parent a0fc533 commit 877c56d
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 55 deletions.
60 changes: 28 additions & 32 deletions src/ast.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import genFn from "generate-function";
import { genFn } from "./generate";
import {
type ArgumentNode,
type ASTNode,
Expand Down Expand Up @@ -41,15 +41,15 @@ export interface JitFieldNode extends FieldNode {
* @deprecated Use __internalShouldIncludePath instead
* @see __internalShouldIncludePath
*/
__internalShouldInclude?: string;
__internalShouldInclude?: string[];

// The shouldInclude logic is specific to the current path
// This is because the same fieldNode can be reached from different paths
// - for example, the same fragment used in two different places
__internalShouldIncludePath?: {
// Key is the stringified ObjectPath,
// Value is the shouldInclude logic for that path
[path: string]: string;
[path: string]: string[];
};
}

Expand Down Expand Up @@ -96,7 +96,7 @@ function collectFieldsImpl(
selectionSet: SelectionSetNode,
fields: FieldsAndNodes,
visitedFragmentNames: { [key: string]: boolean },
previousShouldInclude = "",
previousShouldInclude: string[] = [],
parentResponsePath = ""
): FieldsAndNodes {
for (const selection of selectionSet.selections) {
Expand Down Expand Up @@ -146,16 +146,16 @@ function collectFieldsImpl(

fieldNode.__internalShouldIncludePath[currentPath] =
joinShouldIncludeCompilations(
fieldNode.__internalShouldIncludePath?.[currentPath] ?? "",
fieldNode.__internalShouldIncludePath?.[currentPath] ?? [],
previousShouldInclude,
compiledSkipInclude
[compiledSkipInclude]
);
} else {
// @deprecated
fieldNode.__internalShouldInclude = joinShouldIncludeCompilations(
fieldNode.__internalShouldInclude ?? "",
fieldNode.__internalShouldInclude ?? [],
previousShouldInclude,
compiledSkipInclude
[compiledSkipInclude]
);
}
/**
Expand Down Expand Up @@ -199,7 +199,7 @@ function collectFieldsImpl(
// `should include`s from previous fragments
previousShouldInclude,
// current fragment's shouldInclude
compiledSkipInclude
[compiledSkipInclude]
),
parentResponsePath
);
Expand Down Expand Up @@ -237,7 +237,7 @@ function collectFieldsImpl(
// `should include`s from previous fragments
previousShouldInclude,
// current fragment's shouldInclude
compiledSkipInclude
[compiledSkipInclude]
),
parentResponsePath
);
Expand Down Expand Up @@ -335,15 +335,15 @@ function augmentFieldNodeTree(
joinShouldIncludeCompilations(
parentFieldNode.__internalShouldIncludePath?.[
parentResponsePath
] ?? "",
jitFieldNode.__internalShouldIncludePath?.[currentPath] ?? ""
] ?? [],
jitFieldNode.__internalShouldIncludePath?.[currentPath] ?? []
);
} else {
// @deprecated
jitFieldNode.__internalShouldInclude =
joinShouldIncludeCompilations(
parentFieldNode.__internalShouldInclude ?? "",
jitFieldNode.__internalShouldInclude ?? ""
parentFieldNode.__internalShouldInclude ?? [],
jitFieldNode.__internalShouldInclude ?? []
);
}
}
Expand Down Expand Up @@ -392,7 +392,7 @@ function augmentFieldNodeTree(
*
* @param compilations
*/
function joinShouldIncludeCompilations(...compilations: string[]) {
function joinShouldIncludeCompilations(...compilations: string[][]): string[] {
// remove "true" since we are joining with '&&' as `true && X` = `X`
// This prevents an explosion of `&& true` which could break
// V8's internal size limit for string.
Expand All @@ -404,16 +404,16 @@ function joinShouldIncludeCompilations(...compilations: string[]) {
// Failing to do this results in [RangeError: invalid array length]
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length

// remove empty strings
let filteredCompilations = compilations.filter((it) => it);
const conditionsSet = new Set<string>();
for (const conditions of compilations) {
for (const condition of conditions) {
if (condition !== "true") {
conditionsSet.add(condition);
}
}
}

// Split conditions by && and flatten it
filteredCompilations = ([] as string[]).concat(
...filteredCompilations.map((e) => e.split(" && ").map((it) => it.trim()))
);
// Deduplicate items
filteredCompilations = Array.from(new Set(filteredCompilations));
return filteredCompilations.join(" && ");
return Array.from(conditionsSet);
}

/**
Expand All @@ -427,8 +427,6 @@ function compileSkipInclude(
compilationContext: CompilationContext,
node: SelectionNode
): string {
const gen = genFn();

const { skipValue, includeValue } = compileSkipIncludeDirectiveValues(
compilationContext,
node
Expand All @@ -446,16 +444,14 @@ function compileSkipInclude(
* condition is true or the @include condition is false.
*/
if (skipValue != null && includeValue != null) {
gen(`${skipValue} === false && ${includeValue} === true`);
return `${skipValue} === false && ${includeValue} === true`;
} else if (skipValue != null) {
gen(`(${skipValue} === false)`);
return `(${skipValue} === false)`;
} else if (includeValue != null) {
gen(`(${includeValue} === true)`);
return `(${includeValue} === true)`;
} else {
gen(`true`);
return `true`;
}

return gen.toString();
}

/**
Expand Down
73 changes: 52 additions & 21 deletions src/execution.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type TypedDocumentNode } from "@graphql-typed-document-node/core";
import fastJson from "fast-json-stringify";
import genFn from "generate-function";
import { genFn } from "./generate";
import {
type ASTNode,
type DocumentNode,
Expand Down Expand Up @@ -65,8 +65,10 @@ import {
failToParseVariables
} from "./variables.js";
import { getGraphQLErrorOptions, getOperationRootType } from "./compat.js";
import memoize from "lodash.memoize";

const inspect = createInspect();
const joinOriginPaths = memoize(joinOriginPathsImpl);

export interface CompilerOptions {
customJSONSerializer: boolean;
Expand Down Expand Up @@ -129,6 +131,7 @@ interface DeferredField {
name: string;
responsePath: ObjectPath;
originPaths: string[];
originPathsFormatted: string;
destinationPaths: string[];
parentType: GraphQLObjectType;
fieldName: string;
Expand Down Expand Up @@ -517,7 +520,7 @@ function compileDeferredField(
): string {
const {
name,
originPaths,
originPathsFormatted,
destinationPaths,
fieldNodes,
fieldType,
Expand Down Expand Up @@ -562,7 +565,7 @@ function compileDeferredField(
responsePath
);
const emptyError = createErrorObject(context, fieldNodes, responsePath, '""');
const resolverParentPath = originPaths.join(".");
const resolverParentPath = originPathsFormatted;
const resolverCall = `${GLOBAL_EXECUTION_CONTEXT}.resolvers.${resolverName}(
${resolverParentPath},${topLevelArgs},${GLOBAL_CONTEXT_NAME}, ${executionInfo})`;
const resultParentPath = destinationPaths.join(".");
Expand Down Expand Up @@ -661,7 +664,7 @@ function compileType(
destinationPaths: string[],
previousPath: ObjectPath
): string {
const sourcePath = originPaths.join(".");
const sourcePath = joinOriginPaths(originPaths);
let body = `${sourcePath} == null ? `;
let errorDestination;
if (isNonNullType(type)) {
Expand Down Expand Up @@ -754,7 +757,7 @@ function compileLeafType(
context.options.disableLeafSerialization &&
(type instanceof GraphQLEnumType || isSpecifiedScalarType(type))
) {
body += `${originPaths.join(".")}`;
body += `${joinOriginPaths(originPaths)}`;
} else {
const serializerName = getSerializerName(type.name);
context.serializers[serializerName] = getSerializer(
Expand All @@ -775,8 +778,8 @@ function compileLeafType(
"message"
)});}
`);
body += `${GLOBAL_EXECUTION_CONTEXT}.serializers.${serializerName}(${GLOBAL_EXECUTION_CONTEXT}, ${originPaths.join(
"."
body += `${GLOBAL_EXECUTION_CONTEXT}.serializers.${serializerName}(${GLOBAL_EXECUTION_CONTEXT}, ${joinOriginPaths(
originPaths
)}, ${serializerErrorHandler}, ${parentIndexes})`;
}
return body;
Expand Down Expand Up @@ -815,15 +818,17 @@ function compileObjectType(
body(
`!${GLOBAL_EXECUTION_CONTEXT}.isTypeOfs["${
type.name
}IsTypeOf"](${originPaths.join(
"."
}IsTypeOf"](${joinOriginPaths(
originPaths
)}) ? (${errorDestination}.push(${createErrorObject(
context,
fieldNodes,
responsePath as any,
`\`Expected value of type "${
type.name
}" but got: $\{${GLOBAL_INSPECT_NAME}(${originPaths.join(".")})}.\``
}" but got: $\{${GLOBAL_INSPECT_NAME}(${joinOriginPaths(
originPaths
)})}.\``
)}), null) :`
);
}
Expand Down Expand Up @@ -872,15 +877,32 @@ function compileObjectType(
name
);

const fieldCondition = context.options.useExperimentalPathBasedSkipInclude
? fieldNodes
.map((it) => it.__internalShouldIncludePath?.[serializedResponsePath])
.filter((it) => it)
.join(" || ") || /* if(true) - default */ "true"
: fieldNodes
.map((it) => it.__internalShouldInclude)
.filter((it) => it)
.join(" || ") || /* if(true) - default */ "true";
const fieldConditionsList = (
context.options.useExperimentalPathBasedSkipInclude
? fieldNodes.map(
(it) => it.__internalShouldIncludePath?.[serializedResponsePath]
)
: fieldNodes.map((it) => it.__internalShouldInclude)
).filter(isNotNull);

let fieldCondition = fieldConditionsList
.map((it) => {
if (it.length > 0) {
return `(${it.join(" && ")})`;
}
// default: if there are no conditions, it means that the field
// is always included in the path
return "true";
})
.filter(isNotNull)
.join(" || ");

// if it's an empty string, it means that the field is always included
if (!fieldCondition) {
// if there are no conditions, it means that the field
// is always included in the path
fieldCondition = "true";
}

body(`
(
Expand All @@ -907,6 +929,7 @@ function compileObjectType(
name,
responsePath: addPath(responsePath, name),
originPaths,
originPathsFormatted: joinOriginPaths(originPaths),
destinationPaths,
parentType: type,
fieldName: field.name,
Expand Down Expand Up @@ -1046,8 +1069,8 @@ function compileAbstractType(
return null;
}
})(
${GLOBAL_EXECUTION_CONTEXT}.typeResolvers.${typeResolverName}(${originPaths.join(
"."
${GLOBAL_EXECUTION_CONTEXT}.typeResolvers.${typeResolverName}(${joinOriginPaths(
originPaths
)},
${GLOBAL_CONTEXT_NAME},
${getExecutionInfo(
Expand Down Expand Up @@ -1916,3 +1939,11 @@ function mapAsyncIterator<T, U, R = undefined>(
}
};
}

function joinOriginPathsImpl(originPaths: string[]) {
return originPaths.join(".");
}

function isNotNull<T>(it: T): it is Exclude<T, null | undefined> {
return it != null;
}
12 changes: 12 additions & 0 deletions src/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function genFn() {
let body = "";

function add(str: string) {
body += str + "\n";
return add;
}

add.toString = () => body;

return add;
}
2 changes: 1 addition & 1 deletion src/resolve-info.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import genFn from "generate-function";
import { genFn } from "./generate";
import {
doTypesOverlap,
type FieldNode,
Expand Down
2 changes: 1 addition & 1 deletion src/variables.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import genFn from "generate-function";
import { genFn } from "./generate";
import {
GraphQLBoolean,
GraphQLError,
Expand Down

0 comments on commit 877c56d

Please sign in to comment.