From 3f909e3e112dfebcbf534af306cfa2bd8724a409 Mon Sep 17 00:00:00 2001 From: Vladimir Razuvaev Date: Fri, 24 Feb 2017 16:22:33 +0700 Subject: [PATCH 1/4] Separate utility for `extractTypes` --- src/Type/EagerResolution.php | 51 +---- src/Utils/TypeInfo.php | 62 ++++++ tests/Utils/ExtractTypesTest.php | 338 +++++++++++++++++++++++++++++++ 3 files changed, 403 insertions(+), 48 deletions(-) create mode 100644 tests/Utils/ExtractTypesTest.php diff --git a/src/Type/EagerResolution.php b/src/Type/EagerResolution.php index e7a9aa9..ed15faf 100644 --- a/src/Type/EagerResolution.php +++ b/src/Type/EagerResolution.php @@ -36,10 +36,11 @@ class EagerResolution implements Resolution */ public function __construct(array $initialTypes) { + $typeMap = []; foreach ($initialTypes as $type) { - $this->extractTypes($type); + $typeMap = Utils\TypeInfo::extractTypes($type, $typeMap); } - $this->typeMap += Type::getInternalTypes(); + $this->typeMap = $typeMap + Type::getInternalTypes(); // Keep track of all possible types for abstract types foreach ($this->typeMap as $typeName => $type) { @@ -112,50 +113,4 @@ class EagerResolution implements Resolution 'possibleTypeMap' => $possibleTypesMap ]; } - - /** - * @param $type - * @return array - */ - private function extractTypes($type) - { - if (!$type) { - return $this->typeMap; - } - - if ($type instanceof WrappingType) { - return $this->extractTypes($type->getWrappedType(true)); - } - - if (!empty($this->typeMap[$type->name])) { - Utils::invariant( - $this->typeMap[$type->name] === $type, - "Schema must contain unique named types but contains multiple types named \"$type\"." - ); - return $this->typeMap; - } - $this->typeMap[$type->name] = $type; - - $nestedTypes = []; - - if ($type instanceof UnionType) { - $nestedTypes = $type->getTypes(); - } - if ($type instanceof ObjectType) { - $nestedTypes = array_merge($nestedTypes, $type->getInterfaces()); - } - if ($type instanceof ObjectType || $type instanceof InterfaceType || $type instanceof InputObjectType) { - foreach ((array) $type->getFields() as $fieldName => $field) { - if (isset($field->args)) { - $fieldArgTypes = array_map(function(FieldArgument $arg) { return $arg->getType(); }, $field->args); - $nestedTypes = array_merge($nestedTypes, $fieldArgTypes); - } - $nestedTypes[] = $field->getType(); - } - } - foreach ($nestedTypes as $type) { - $this->extractTypes($type); - } - return $this->typeMap; - } } diff --git a/src/Utils/TypeInfo.php b/src/Utils/TypeInfo.php index e052c04..929caa1 100644 --- a/src/Utils/TypeInfo.php +++ b/src/Utils/TypeInfo.php @@ -20,6 +20,8 @@ use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; +use GraphQL\Type\Definition\UnionType; +use GraphQL\Type\Definition\WrappingType; use GraphQL\Type\Introspection; use GraphQL\Utils; @@ -162,6 +164,66 @@ class TypeInfo return $schema->getType($inputTypeNode->name->value); } + /** + * Given root type scans through all fields to find nested types. Returns array where keys are for type name + * and value contains corresponding type instance. + * + * Example output: + * [ + * 'String' => $instanceOfStringType, + * 'MyType' => $instanceOfMyType, + * ... + * ] + * + * @param Type $type + * @param array|null $typeMap + * @return array + */ + public static function extractTypes($type, array $typeMap = null) + { + if (!$typeMap) { + $typeMap = []; + } + if (!$type) { + return $typeMap; + } + + if ($type instanceof WrappingType) { + return self::extractTypes($type->getWrappedType(true), $typeMap); + } + + if (!empty($typeMap[$type->name])) { + Utils::invariant( + $typeMap[$type->name] === $type, + "Schema must contain unique named types but contains multiple types named \"$type\"." + ); + return $typeMap; + } + $typeMap[$type->name] = $type; + + $nestedTypes = []; + + if ($type instanceof UnionType) { + $nestedTypes = $type->getTypes(); + } + if ($type instanceof ObjectType) { + $nestedTypes = array_merge($nestedTypes, $type->getInterfaces()); + } + if ($type instanceof ObjectType || $type instanceof InterfaceType || $type instanceof InputObjectType) { + foreach ((array) $type->getFields() as $fieldName => $field) { + if (isset($field->args)) { + $fieldArgTypes = array_map(function(FieldArgument $arg) { return $arg->getType(); }, $field->args); + $nestedTypes = array_merge($nestedTypes, $fieldArgTypes); + } + $nestedTypes[] = $field->getType(); + } + } + foreach ($nestedTypes as $type) { + $typeMap = self::extractTypes($type, $typeMap); + } + return $typeMap; + } + /** * Not exactly the same as the executor's definition of getFieldDef, in this * statically evaluated environment we do not always have an Object type, diff --git a/tests/Utils/ExtractTypesTest.php b/tests/Utils/ExtractTypesTest.php new file mode 100644 index 0000000..4d19cc0 --- /dev/null +++ b/tests/Utils/ExtractTypesTest.php @@ -0,0 +1,338 @@ +node = new InterfaceType([ + 'name' => 'Node', + 'fields' => [ + 'id' => Type::string() + ] + ]); + + $this->content = new InterfaceType([ + 'name' => 'Content', + 'fields' => function() { + return [ + 'title' => Type::string(), + 'body' => Type::string(), + 'author' => $this->user, + 'comments' => Type::listOf($this->comment), + 'categories' => Type::listOf($this->category) + ]; + } + ]); + + $this->blogStory = new ObjectType([ + 'name' => 'BlogStory', + 'interfaces' => [ + $this->node, + $this->content + ], + 'fields' => function() { + return [ + $this->node->getField('id'), + $this->content->getField('title'), + $this->content->getField('body'), + $this->content->getField('author'), + $this->content->getField('comments'), + $this->content->getField('categories') + ]; + }, + ]); + + $this->link = new ObjectType([ + 'name' => 'Link', + 'interfaces' => [ + $this->node, + $this->content + ], + 'fields' => function() { + return [ + $this->node->getField('id'), + $this->content->getField('title'), + $this->content->getField('body'), + $this->content->getField('author'), + $this->content->getField('comments'), + $this->content->getField('categories'), + 'url' => Type::string() + ]; + }, + ]); + + $this->video = new ObjectType([ + 'name' => 'Video', + 'interfaces' => [ + $this->node, + $this->content + ], + 'fields' => function() { + return [ + $this->node->getField('id'), + $this->content->getField('title'), + $this->content->getField('body'), + $this->content->getField('author'), + $this->content->getField('comments'), + $this->content->getField('categories'), + 'streamUrl' => Type::string(), + 'downloadUrl' => Type::string(), + 'metadata' => $this->videoMetadata = new ObjectType([ + 'name' => 'VideoMetadata', + 'fields' => [ + 'lat' => Type::float(), + 'lng' => Type::float() + ] + ]) + ]; + } + ]); + + $this->comment = new ObjectType([ + 'name' => 'Comment', + 'interfaces' => [ + $this->node + ], + 'fields' => function() { + return [ + $this->node->getField('id'), + 'author' => $this->user, + 'text' => Type::string(), + 'replies' => Type::listOf($this->comment), + 'parent' => $this->comment, + 'content' => $this->content + ]; + } + ]); + + $this->user = new ObjectType([ + 'name' => 'User', + 'interfaces' => [ + $this->node + ], + 'fields' => function() { + return [ + $this->node->getField('id'), + 'name' => Type::string(), + ]; + } + ]); + + $this->category = new ObjectType([ + 'name' => 'Category', + 'interfaces' => [ + $this->node + ], + 'fields' => function() { + return [ + $this->node->getField('id'), + 'name' => Type::string() + ]; + } + ]); + + $this->mention = new UnionType([ + 'name' => 'Mention', + 'types' => [ + $this->user, + $this->category + ] + ]); + + $this->query = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'viewer' => $this->user, + 'latestContent' => $this->content, + 'node' => $this->node, + 'mentions' => Type::listOf($this->mention) + ] + ]); + + $this->mutation = new ObjectType([ + 'name' => 'Mutation', + 'fields' => [ + 'postStory' => [ + 'type' => $this->postStoryMutation = new ObjectType([ + 'name' => 'PostStoryMutation', + 'fields' => [ + 'story' => $this->blogStory + ] + ]), + 'args' => [ + 'input' => Type::nonNull($this->postStoryMutationInput = new InputObjectType([ + 'name' => 'PostStoryMutationInput', + 'fields' => [ + 'title' => Type::string(), + 'body' => Type::string(), + 'author' => Type::id(), + 'category' => Type::id() + ] + ])), + 'clientRequestId' => Type::string() + ] + ], + 'postComment' => [ + 'type' => $this->postCommentMutation = new ObjectType([ + 'name' => 'PostCommentMutation', + 'fields' => [ + 'comment' => $this->comment + ] + ]), + 'args' => [ + 'input' => Type::nonNull($this->postCommentMutationInput = new InputObjectType([ + 'name' => 'PostCommentMutationInput', + 'fields' => [ + 'text' => Type::nonNull(Type::string()), + 'author' => Type::nonNull(Type::id()), + 'content' => Type::id(), + 'parent' => Type::id() + ] + ])), + 'clientRequestId' => Type::string() + ] + ] + ] + ]); + } + + public function testExtractTypesFromQuery() + { + $expectedTypeMap = [ + 'Query' => $this->query, + 'User' => $this->user, + 'Node' => $this->node, + 'String' => Type::string(), + 'Content' => $this->content, + 'Comment' => $this->comment, + 'Mention' => $this->mention, + 'Category' => $this->category, + ]; + + $actualTypeMap = TypeInfo::extractTypes($this->query); + $this->assertEquals($expectedTypeMap, $actualTypeMap); + } + + public function testExtractTypesFromMutation() + { + $expectedTypeMap = [ + 'Mutation' => $this->mutation, + 'User' => $this->user, + 'Node' => $this->node, + 'String' => Type::string(), + 'Content' => $this->content, + 'Comment' => $this->comment, + 'BlogStory' => $this->blogStory, + 'Category' => $this->category, + 'PostStoryMutationInput' => $this->postStoryMutationInput, + 'ID' => Type::id(), + 'PostStoryMutation' => $this->postStoryMutation, + 'PostCommentMutationInput' => $this->postCommentMutationInput, + 'PostCommentMutation' => $this->postCommentMutation, + ]; + + $actualTypeMap = TypeInfo::extractTypes($this->mutation); + $this->assertEquals($expectedTypeMap, $actualTypeMap); + } + + public function testThrowsOnMultipleTypesWithSameName() + { + $otherUserType = new ObjectType([ + 'name' => 'User', + 'fields' => [] + ]); + + $queryType = new ObjectType([ + 'name' => 'Test', + 'fields' => [ + 'otherUser' => $otherUserType, + 'user' => $this->user + ] + ]); + + $this->setExpectedException( + '\GraphQL\Error\InvariantViolation', + "Schema must contain unique named types but contains multiple types named \"User\"" + ); + TypeInfo::extractTypes($queryType); + } +} From 34ca9315330b090b2fae1ce5b21a4c0c07aeecde Mon Sep 17 00:00:00 2001 From: Vladimir Razuvaev Date: Fri, 24 Feb 2017 17:10:10 +0700 Subject: [PATCH 2/4] Sanity check to ensure that GraphQL query is string --- src/GraphQL.php | 4 ++-- src/Language/Source.php | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/GraphQL.php b/src/GraphQL.php index 8574236..d0d3ec6 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -18,7 +18,7 @@ class GraphQL { /** * @param Schema $schema - * @param $requestString + * @param string|DocumentNode $requestString * @param mixed $rootValue * @param array|null $variableValues * @param string|null $operationName @@ -41,7 +41,7 @@ class GraphQL /** * @param Schema $schema - * @param $requestString + * @param string|DocumentNode $requestString * @param null $rootValue * @param null $variableValues * @param null $operationName diff --git a/src/Language/Source.php b/src/Language/Source.php index 5083f40..d831b29 100644 --- a/src/Language/Source.php +++ b/src/Language/Source.php @@ -1,6 +1,8 @@ body = $body; $this->length = mb_strlen($body, 'UTF-8'); $this->name = $name ?: 'GraphQL'; From 66acb73a47ab88261048bb2ceb27db5807c5fc61 Mon Sep 17 00:00:00 2001 From: Vladimir Razuvaev Date: Sat, 25 Feb 2017 02:27:53 +0700 Subject: [PATCH 3/4] Added first meaningful benchmarks to have some grounds for future performance optimizations --- benchmarks/HugeSchemaBench.php | 90 +++++++++++++++ benchmarks/LexerBench.php | 2 +- benchmarks/StarWarsBench.php | 97 ++++++++++++++++ benchmarks/Utils/QueryGenerator.php | 100 +++++++++++++++++ benchmarks/Utils/SchemaGenerator.php | 159 +++++++++++++++++++++++++++ composer.json | 3 +- 6 files changed, 449 insertions(+), 2 deletions(-) create mode 100644 benchmarks/HugeSchemaBench.php create mode 100644 benchmarks/StarWarsBench.php create mode 100644 benchmarks/Utils/QueryGenerator.php create mode 100644 benchmarks/Utils/SchemaGenerator.php diff --git a/benchmarks/HugeSchemaBench.php b/benchmarks/HugeSchemaBench.php new file mode 100644 index 0000000..d641bee --- /dev/null +++ b/benchmarks/HugeSchemaBench.php @@ -0,0 +1,90 @@ +schemaBuilder = new SchemaGenerator([ + 'totalTypes' => 600, + 'fieldsPerType' => 8, + 'listFieldsPerType' => 2, + 'nestingLevel' => 10 + ]); + + $this->schema = $this->schemaBuilder->buildSchema(); + + $queryBuilder = new QueryGenerator($this->schema, 0.05); + $this->descriptor = $this->schema->getDescriptor(); + $this->smallQuery = $queryBuilder->buildQuery(); + } + + public function benchSchema() + { + $this->schemaBuilder + ->buildSchema(); + } + + public function benchSchemaLazy() + { + $this->createLazySchema(); + } + + public function benchSmallQuery() + { + $result = GraphQL::execute($this->schema, $this->smallQuery); + } + + public function benchSmallQueryLazy() + { + $schema = $this->createLazySchema(); + $result = GraphQL::execute($schema, $this->smallQuery); + } + + private function createLazySchema() + { + $strategy = new LazyResolution( + $this->descriptor, + function($name) { + return $this->schemaBuilder->loadType($name); + } + ); + + return new Schema([ + 'query' => $this->schemaBuilder->buildQueryType(), + 'typeResolution' => $strategy, + ]); + } +} diff --git a/benchmarks/LexerBench.php b/benchmarks/LexerBench.php index 5f83cd8..ca79169 100644 --- a/benchmarks/LexerBench.php +++ b/benchmarks/LexerBench.php @@ -29,7 +29,7 @@ class LexerBench $lexer = new Lexer($this->introQuery); do { - $token = $lexer->nextToken(); + $token = $lexer->advance(); } while ($token->kind !== Token::EOF); } } diff --git a/benchmarks/StarWarsBench.php b/benchmarks/StarWarsBench.php new file mode 100644 index 0000000..1690808 --- /dev/null +++ b/benchmarks/StarWarsBench.php @@ -0,0 +1,97 @@ +introQuery = Introspection::getIntrospectionQuery(); + } + + public function benchSchema() + { + StarWarsSchema::build(); + } + + public function benchHeroQuery() + { + $q = ' + query HeroNameQuery { + hero { + name + } + } + '; + + GraphQL::execute( + StarWarsSchema::build(), + $q + ); + } + + public function benchNestedQuery() + { + $q = ' + query NestedQuery { + hero { + name + friends { + name + appearsIn + friends { + name + } + } + } + } + '; + GraphQL::execute( + StarWarsSchema::build(), + $q + ); + } + + public function benchQueryWithFragment() + { + $q = ' + query UseFragment { + luke: human(id: "1000") { + ...HumanFragment + } + leia: human(id: "1003") { + ...HumanFragment + } + } + + fragment HumanFragment on Human { + name + homePlanet + } + '; + + GraphQL::execute( + StarWarsSchema::build(), + $q + ); + } + + public function benchStarWarsIntrospectionQuery() + { + GraphQL::execute( + StarWarsSchema::build(), + $this->introQuery + ); + } +} diff --git a/benchmarks/Utils/QueryGenerator.php b/benchmarks/Utils/QueryGenerator.php new file mode 100644 index 0000000..f89b5be --- /dev/null +++ b/benchmarks/Utils/QueryGenerator.php @@ -0,0 +1,100 @@ +schema = $schema; + + Utils::invariant(0 < $percentOfLeafFields && $percentOfLeafFields <= 1); + + $totalFields = 0; + foreach ($schema->getTypeMap() as $type) { + if ($type instanceof ObjectType) { + $totalFields += count($type->getFields()); + } + } + + $this->maxLeafFields = max(1, round($totalFields * $percentOfLeafFields)); + $this->currentLeafFields = 0; + } + + public function buildQuery() + { + $qtype = $this->schema->getQueryType(); + + $ast = new DocumentNode([ + 'definitions' => [ + new OperationDefinitionNode([ + 'name' => new NameNode(['value' => 'TestQuery']), + 'operation' => 'query', + 'selectionSet' => $this->buildSelectionSet($qtype->getFields()) + ]) + ] + ]); + + return Printer::doPrint($ast); + } + + /** + * @param FieldDefinition[] $fields + * @return SelectionSetNode + */ + public function buildSelectionSet($fields) + { + $selections[] = new FieldNode([ + 'name' => new NameNode(['value' => '__typename']) + ]); + $this->currentLeafFields++; + + foreach ($fields as $field) { + if ($this->currentLeafFields >= $this->maxLeafFields) { + break; + } + + $type = $field->getType(); + + if ($type instanceof WrappingType) { + $type = $type->getWrappedType(true); + } + + if ($type instanceof ObjectType || $type instanceof InterfaceType) { + $selectionSet = $this->buildSelectionSet($type->getFields()); + } else { + $selectionSet = null; + $this->currentLeafFields++; + } + + $selections[] = new FieldNode([ + 'name' => new NameNode(['value' => $field->name]), + 'selectionSet' => $selectionSet + ]); + } + + $selectionSet = new SelectionSetNode([ + 'selections' => $selections + ]); + + return $selectionSet; + } +} diff --git a/benchmarks/Utils/SchemaGenerator.php b/benchmarks/Utils/SchemaGenerator.php new file mode 100644 index 0000000..6cc9145 --- /dev/null +++ b/benchmarks/Utils/SchemaGenerator.php @@ -0,0 +1,159 @@ + 100, + 'nestingLevel' => 10, + 'fieldsPerType' => 10, + 'listFieldsPerType' => 2 + ]; + + private $typeIndex = 0; + + private $objectTypes = []; + + /** + * BenchmarkSchemaBuilder constructor. + * @param array $config + */ + public function __construct(array $config) + { + $this->config = array_merge($this->config, $config); + } + + public function buildSchema() + { + return new Schema([ + 'query' => $this->buildQueryType() + ]); + } + + public function buildQueryType() + { + $this->typeIndex = 0; + $this->objectTypes = []; + + return $this->createType(0); + } + + public function loadType($name) + { + $tokens = explode('_', $name); + $nestingLevel = (int) $tokens[1]; + + return $this->createType($nestingLevel, $name); + } + + protected function createType($nestingLevel, $typeName = null) + { + if ($this->typeIndex > $this->config['totalTypes']) { + throw new \Exception( + "Cannot create new type: there are already {$this->typeIndex} ". + "which exceeds allowed number of {$this->config['totalTypes']} types total" + ); + } + + $this->typeIndex++; + if (!$typeName) { + $typeName = 'Level_' . $nestingLevel . '_Type' . $this->typeIndex; + } + + $type = new ObjectType([ + 'name' => $typeName, + 'fields' => function() use ($typeName, $nestingLevel) { + return $this->createTypeFields($typeName, $nestingLevel + 1); + } + ]); + + $this->objectTypes[$typeName] = $type; + return $type; + } + + protected function getFieldTypeAndName($nestingLevel, $fieldIndex) + { + if ($nestingLevel >= $this->config['nestingLevel']) { + $fieldType = Type::string(); + $fieldName = 'leafField' . $fieldIndex; + } else if ($this->typeIndex >= $this->config['totalTypes']) { + $fieldType = $this->objectTypes[array_rand($this->objectTypes)]; + $fieldName = 'randomTypeField' . $fieldIndex; + } else { + $fieldType = $this->createType($nestingLevel); + $fieldName = 'field' . $fieldIndex; + } + + return [$fieldType, $fieldName]; + } + + protected function createTypeFields($typeName, $nestingLevel) + { + $fields = []; + for ($index = 0; $index < $this->config['fieldsPerType']; $index++) { + list($type, $name) = $this->getFieldTypeAndName($nestingLevel, $index); + $fields[] = [ + 'name' => $name, + 'type' => $type, + 'resolve' => [$this, 'resolveField'] + ]; + } + for ($index = 0; $index < $this->config['listFieldsPerType']; $index++) { + list($type, $name) = $this->getFieldTypeAndName($nestingLevel, $index); + $name = 'listOf' . ucfirst($name); + + $fields[] = [ + 'name' => $name, + 'type' => Type::listOf($type), + 'args' => $this->createFieldArgs($name, $typeName), + 'resolve' => function() { + return [ + 'string1', + 'string2', + 'string3', + 'string4', + 'string5', + ]; + } + ]; + } + return $fields; + } + + protected function createFieldArgs($fieldName, $typeName) + { + return [ + 'argString' => [ + 'type' => Type::string() + ], + 'argEnum' => [ + 'type' => new EnumType([ + 'name' => $typeName . $fieldName . 'Enum', + 'values' => [ + "ONE", "TWO", "THREE" + ] + ]) + ], + 'argInputObject' => [ + 'type' => new InputObjectType([ + 'name' => $typeName . $fieldName . 'Input', + 'fields' => [ + 'field1' => Type::string(), + 'field2' => Type::int() + ] + ]) + ] + ]; + } + + public function resolveField($value, $args, $context, $resolveInfo) + { + return $resolveInfo->fieldName . '-value'; + } +} diff --git a/composer.json b/composer.json index 9fd5e42..a50e944 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "ext-mbstring": "*" }, "require-dev": { - "phpunit/phpunit": "^4.8" + "phpunit/phpunit": "^4.8", + "phpbench/phpbench": "^0.13.0" }, "config": { "bin-dir": "bin" From 752668df8d2c8d0a931b9b82fc33aeb0785d1ab0 Mon Sep 17 00:00:00 2001 From: Vladimir Razuvaev Date: Sat, 25 Feb 2017 02:55:52 +0700 Subject: [PATCH 4/4] Removed phpbench from composer to satisfy PHP5.4 requirement --- composer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index a50e944..9fd5e42 100644 --- a/composer.json +++ b/composer.json @@ -13,8 +13,7 @@ "ext-mbstring": "*" }, "require-dev": { - "phpunit/phpunit": "^4.8", - "phpbench/phpbench": "^0.13.0" + "phpunit/phpunit": "^4.8" }, "config": { "bin-dir": "bin"