Skip to content

Commit

Permalink
feat(performance): optimize resolvers execution paths (#488)
Browse files Browse the repository at this point in the history
Co-authors:

@croquiscom
* optimize applyMiddlewares for simple case
* optimize createBasicFieldResolver
* add isPromise
* optimize getParams
* optimize createHandlerResolver
* add benchmark for large array with field resolver

@MichalLytek
* Add benchmark results
* Rename util to isPromiseLike
* Merge "large" benchmarks with "array" and update results
* Update performance docs
* Add entry to changelog
  • Loading branch information
sixmen authored and MichalLytek committed Jan 3, 2020
1 parent ba96c58 commit d6b348d
Show file tree
Hide file tree
Showing 15 changed files with 555 additions and 85 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- update `TypeResolver` interface to match with `GraphQLTypeResolver` from `graphql-js`
- add basic support for directives with `@Directive()` decorator (#369)
- add possibility to tune up the performance and disable auth & middlewares stack for simple field resolvers (#479)
- optimize resolvers execution paths to speed up a lot basic scenarios (#488)
### Fixes
- refactor union types function syntax handling to prevent possible errors with circular refs
- fix transforming and validating nested inputs and arrays (#462)
Expand Down
65 changes: 65 additions & 0 deletions benchmarks/array/graphql-js/async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
GraphQLNonNull,
GraphQLBoolean,
GraphQLInt,
GraphQLList,
} from "graphql";

import { runBenchmark, ARRAY_ITEMS } from "../run";

const SampleObjectType: GraphQLObjectType = new GraphQLObjectType({
name: "SampleObject",
fields: () => ({
stringField: {
type: new GraphQLNonNull(GraphQLString),
resolve: async source => {
return source.stringField;
},
},
numberField: {
type: new GraphQLNonNull(GraphQLInt),
resolve: async source => {
return source.numberField;
},
},
booleanField: {
type: new GraphQLNonNull(GraphQLBoolean),
resolve: async source => {
return source.booleanField;
},
},
nestedField: {
type: SampleObjectType,
resolve: async source => {
return source.nestedField;
},
},
}),
});

const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: "Query",
fields: {
multipleNestedObjects: {
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(SampleObjectType))),
resolve: () =>
Array.from({ length: ARRAY_ITEMS }, (_, index) => ({
stringField: "stringField",
booleanField: true,
numberField: index,
nestedField: {
stringField: "stringField",
booleanField: true,
numberField: index,
},
})),
},
},
}),
});

runBenchmark(schema).catch(console.error);
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import {
GraphQLList,
} from "graphql";

import { runBenchmark, ARRAY_ITEMS } from "./run";
import { runBenchmark, ARRAY_ITEMS } from "../run";

const SampleObjectType: GraphQLObjectType = new GraphQLObjectType({
name: "SampleObject",
fields: () => ({
sampleField: {
stringField: {
type: new GraphQLNonNull(GraphQLString),
},
numberField: {
Expand Down
31 changes: 25 additions & 6 deletions benchmarks/array/results.txt
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
Core i7 2700K @ 3.5GHz
Windows 10 x64
10 000 array items | 100 iterations
25 000 array items | 50 iterations
Node.js v13.5

-----
Node.js v13.2
TypeGraphQL

standard
- 42.551s
- 15.518s

using sync field resolvers
- 18.180s

using async field resolvers
- 39.934s

using getters
- 31.207s

standard with global middleware
- 62.664s

with `simpleResolvers: true`
- 11.841s
- 14.980s

-----
`graphql-js`

standard
- 13.276s

graphql-js
- 9.963s
async field resolvers
- 25.630s
4 changes: 2 additions & 2 deletions benchmarks/array/run.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { GraphQLSchema, execute } from "graphql";
import { gql } from "apollo-server";

const BENCHMARK_ITERATIONS = 100;
export const ARRAY_ITEMS = 10000;
const BENCHMARK_ITERATIONS = 50;
export const ARRAY_ITEMS = 25000;

export async function runBenchmark(schema: GraphQLSchema) {
const multipleNestedObjectsQuery = gql`
Expand Down
78 changes: 78 additions & 0 deletions benchmarks/array/type-graphql/async-field-resolvers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import "reflect-metadata";
import {
buildSchema,
Field,
ObjectType,
Resolver,
Query,
Int,
FieldResolver,
Root,
} from "../../../build/package/dist";

import { runBenchmark, ARRAY_ITEMS } from "../run";

@ObjectType()
class SampleObject {
@Field()
stringField!: string;

@Field(type => Int)
numberField!: number;

@Field()
booleanField!: boolean;

@Field({ nullable: true })
nestedField?: SampleObject;
}

@Resolver(SampleObject)
class SampleResolver {
@Query(returns => [SampleObject])
multipleNestedObjects(): SampleObject[] {
return Array.from(
{ length: ARRAY_ITEMS },
(_, index): SampleObject => ({
stringField: "stringField",
booleanField: true,
numberField: index,
nestedField: {
stringField: "stringField",
booleanField: true,
numberField: index,
},
}),
);
}

@FieldResolver()
async stringField(@Root() source: SampleObject) {
return source.stringField;
}

@FieldResolver()
async numberField(@Root() source: SampleObject) {
return source.numberField;
}

@FieldResolver()
async booleanField(@Root() source: SampleObject) {
return source.booleanField;
}

@FieldResolver()
async nestedField(@Root() source: SampleObject) {
return source.nestedField;
}
}

async function main() {
const schema = await buildSchema({
resolvers: [SampleResolver],
});

await runBenchmark(schema);
}

main().catch(console.error);
65 changes: 65 additions & 0 deletions benchmarks/array/type-graphql/simple-resolvers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import "reflect-metadata";
import {
buildSchema,
Field,
ObjectType,
Resolver,
Query,
Int,
MiddlewareFn,
} from "../../../build/package/dist";

import { runBenchmark, ARRAY_ITEMS } from "../run";

@ObjectType({ simpleResolvers: true })
class SampleObject {
@Field()
stringField!: string;

@Field(type => Int)
numberField!: number;

@Field()
booleanField!: boolean;

@Field({ nullable: true })
nestedField?: SampleObject;
}

@Resolver()
class SampleResolver {
@Query(returns => [SampleObject])
multipleNestedObjects(): SampleObject[] {
return Array.from(
{ length: ARRAY_ITEMS },
(_, index): SampleObject => ({
stringField: "stringField",
booleanField: true,
numberField: index,
nestedField: {
stringField: "stringField",
booleanField: true,
numberField: index,
},
}),
);
}
}

const log = (...args: any[]) => void 0; // noop

const loggingMiddleware: MiddlewareFn = ({ info }, next) => {
log(`${info.parentType.name}.${info.fieldName} accessed`);
return next();
};

async function main() {
const schema = await buildSchema({
resolvers: [SampleResolver],
globalMiddlewares: [loggingMiddleware],
});

await runBenchmark(schema);
}

main().catch(console.error);
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import "reflect-metadata";
import { buildSchema, Field, ObjectType, Resolver, Query, Int } from "../../build/package";
import { buildSchema, Field, ObjectType, Resolver, Query, Int } from "../../../build/package/dist";

import { runBenchmark, ARRAY_ITEMS } from "./run";
import { runBenchmark, ARRAY_ITEMS } from "../run";

@ObjectType({ simpleResolvers: true })
@ObjectType()
class SampleObject {
@Field()
stringField!: string;
Expand Down
78 changes: 78 additions & 0 deletions benchmarks/array/type-graphql/sync-field-resolvers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import "reflect-metadata";
import {
buildSchema,
Field,
ObjectType,
Resolver,
Query,
Int,
FieldResolver,
Root,
} from "../../../build/package/dist";

import { runBenchmark, ARRAY_ITEMS } from "../run";

@ObjectType()
class SampleObject {
@Field()
stringField!: string;

@Field(type => Int)
numberField!: number;

@Field()
booleanField!: boolean;

@Field({ nullable: true })
nestedField?: SampleObject;
}

@Resolver(SampleObject)
class SampleResolver {
@Query(returns => [SampleObject])
multipleNestedObjects(): SampleObject[] {
return Array.from(
{ length: ARRAY_ITEMS },
(_, index): SampleObject => ({
stringField: "stringField",
booleanField: true,
numberField: index,
nestedField: {
stringField: "stringField",
booleanField: true,
numberField: index,
},
}),
);
}

@FieldResolver()
stringField(@Root() source: SampleObject) {
return source.stringField;
}

@FieldResolver()
numberField(@Root() source: SampleObject) {
return source.numberField;
}

@FieldResolver()
booleanField(@Root() source: SampleObject) {
return source.booleanField;
}

@FieldResolver()
nestedField(@Root() source: SampleObject) {
return source.nestedField;
}
}

async function main() {
const schema = await buildSchema({
resolvers: [SampleResolver],
});

await runBenchmark(schema);
}

main().catch(console.error);
Loading

0 comments on commit d6b348d

Please sign in to comment.