mirror of
https://github.com/retailcrm/graphql-php.git
synced 2025-02-06 07:49:24 +03:00
Several performance improvements (#8)
This commit is contained in:
parent
d982bad63a
commit
3b3da9e066
@ -43,6 +43,11 @@ class ExecutionContext
|
|||||||
*/
|
*/
|
||||||
public $errors;
|
public $errors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $memoized = [];
|
||||||
|
|
||||||
public function __construct($schema, $fragments, $root, $operation, $variables, $errors)
|
public function __construct($schema, $fragments, $root, $operation, $variables, $errors)
|
||||||
{
|
{
|
||||||
$this->schema = $schema;
|
$this->schema = $schema;
|
||||||
|
@ -359,58 +359,95 @@ class Executor
|
|||||||
private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $source, $fieldASTs)
|
private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $source, $fieldASTs)
|
||||||
{
|
{
|
||||||
$fieldAST = $fieldASTs[0];
|
$fieldAST = $fieldASTs[0];
|
||||||
$fieldName = $fieldAST->name->value;
|
|
||||||
|
|
||||||
$fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName);
|
$uid = self::getFieldUid($fieldAST);
|
||||||
|
|
||||||
if (!$fieldDef) {
|
// Get memoized variables if they exist
|
||||||
return self::$UNDEFINED;
|
if (isset($exeContext->memoized['resolveField'][$uid])) {
|
||||||
|
$memoized = $exeContext->memoized['resolveField'][$uid];
|
||||||
|
$fieldDef = $memoized['fieldDef'];
|
||||||
|
$returnType = $fieldDef->getType();
|
||||||
|
$args = $memoized['args'];
|
||||||
|
$info = $memoized['info'];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$fieldName = $fieldAST->name->value;
|
||||||
|
|
||||||
|
$fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName);
|
||||||
|
|
||||||
|
if (!$fieldDef) {
|
||||||
|
return self::$UNDEFINED;
|
||||||
|
}
|
||||||
|
|
||||||
|
$returnType = $fieldDef->getType();
|
||||||
|
|
||||||
|
// Build hash of arguments from the field.arguments AST, using the
|
||||||
|
// variables scope to fulfill any variable references.
|
||||||
|
// TODO: find a way to memoize, in case this field is within a List type.
|
||||||
|
$args = Values::getArgumentValues(
|
||||||
|
$fieldDef->args,
|
||||||
|
$fieldAST->arguments,
|
||||||
|
$exeContext->variableValues
|
||||||
|
);
|
||||||
|
|
||||||
|
// The resolve function's optional third argument is a collection of
|
||||||
|
// information about the current execution state.
|
||||||
|
$info = new ResolveInfo([
|
||||||
|
'fieldName' => $fieldName,
|
||||||
|
'fieldASTs' => $fieldASTs,
|
||||||
|
'returnType' => $returnType,
|
||||||
|
'parentType' => $parentType,
|
||||||
|
'schema' => $exeContext->schema,
|
||||||
|
'fragments' => $exeContext->fragments,
|
||||||
|
'rootValue' => $exeContext->rootValue,
|
||||||
|
'operation' => $exeContext->operation,
|
||||||
|
'variableValues' => $exeContext->variableValues,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Memoizing results for same query field
|
||||||
|
// (useful for lists when several values are resolved against the same field)
|
||||||
|
if ($returnType instanceof ObjectType) {
|
||||||
|
$memoized = $exeContext->memoized['resolveField'][$uid] = [
|
||||||
|
'fieldDef' => $fieldDef,
|
||||||
|
'args' => $args,
|
||||||
|
'info' => $info,
|
||||||
|
'results' => new \SplObjectStorage
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$returnType = $fieldDef->getType();
|
// When source value is object it is possible to memoize certain subset of results
|
||||||
|
$isObject = is_object($source);
|
||||||
|
|
||||||
if (isset($fieldDef->resolveFn)) {
|
if ($isObject && isset($memoized['results'][$source])) {
|
||||||
$resolveFn = $fieldDef->resolveFn;
|
$result = $exeContext->memoized['resolveField'][$uid]['results'][$source];
|
||||||
} else if (isset($parentType->resolveFieldFn)) {
|
|
||||||
$resolveFn = $parentType->resolveFieldFn;
|
|
||||||
} else {
|
} else {
|
||||||
$resolveFn = self::$defaultResolveFn;
|
if (isset($fieldDef->resolveFn)) {
|
||||||
|
$resolveFn = $fieldDef->resolveFn;
|
||||||
|
} else if (isset($parentType->resolveFieldFn)) {
|
||||||
|
$resolveFn = $parentType->resolveFieldFn;
|
||||||
|
} else {
|
||||||
|
$resolveFn = self::$defaultResolveFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the resolve function, regardless of if its result is normal
|
||||||
|
// or abrupt (error).
|
||||||
|
$result = self::resolveOrError($resolveFn, $source, $args, $info);
|
||||||
|
|
||||||
|
$result = self::completeValueCatchingError(
|
||||||
|
$exeContext,
|
||||||
|
$returnType,
|
||||||
|
$fieldASTs,
|
||||||
|
$info,
|
||||||
|
$result
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($isObject && isset($memoized['results'])) {
|
||||||
|
$exeContext->memoized['resolveField'][$uid]['results'][$source] = $result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build hash of arguments from the field.arguments AST, using the
|
return $result;
|
||||||
// variables scope to fulfill any variable references.
|
|
||||||
// TODO: find a way to memoize, in case this field is within a List type.
|
|
||||||
$args = Values::getArgumentValues(
|
|
||||||
$fieldDef->args,
|
|
||||||
$fieldAST->arguments,
|
|
||||||
$exeContext->variableValues
|
|
||||||
);
|
|
||||||
|
|
||||||
// The resolve function's optional third argument is a collection of
|
|
||||||
// information about the current execution state.
|
|
||||||
$info = new ResolveInfo([
|
|
||||||
'fieldName' => $fieldName,
|
|
||||||
'fieldASTs' => $fieldASTs,
|
|
||||||
'returnType' => $returnType,
|
|
||||||
'parentType' => $parentType,
|
|
||||||
'schema' => $exeContext->schema,
|
|
||||||
'fragments' => $exeContext->fragments,
|
|
||||||
'rootValue' => $exeContext->rootValue,
|
|
||||||
'operation' => $exeContext->operation,
|
|
||||||
'variableValues' => $exeContext->variableValues,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Get the resolve function, regardless of if its result is normal
|
|
||||||
// or abrupt (error).
|
|
||||||
$result = self::resolveOrError($resolveFn, $source, $args, $info);
|
|
||||||
|
|
||||||
return self::completeValueCatchingError(
|
|
||||||
$exeContext,
|
|
||||||
$returnType,
|
|
||||||
$fieldASTs,
|
|
||||||
$info,
|
|
||||||
$result
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField`
|
// Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField`
|
||||||
@ -554,15 +591,23 @@ class Executor
|
|||||||
$subFieldASTs = new \ArrayObject();
|
$subFieldASTs = new \ArrayObject();
|
||||||
$visitedFragmentNames = new \ArrayObject();
|
$visitedFragmentNames = new \ArrayObject();
|
||||||
for ($i = 0; $i < count($fieldASTs); $i++) {
|
for ($i = 0; $i < count($fieldASTs); $i++) {
|
||||||
$selectionSet = $fieldASTs[$i]->selectionSet;
|
// Get memoized value if it exists
|
||||||
if ($selectionSet) {
|
$uid = self::getFieldUid($fieldASTs[$i]);
|
||||||
$subFieldASTs = self::collectFields(
|
if (isset($exeContext->memoized['collectSubFields'][$uid][$runtimeType->name])) {
|
||||||
$exeContext,
|
$subFieldASTs = $exeContext->memoized['collectSubFields'][$uid][$runtimeType->name];
|
||||||
$runtimeType,
|
}
|
||||||
$selectionSet,
|
else {
|
||||||
$subFieldASTs,
|
$selectionSet = $fieldASTs[$i]->selectionSet;
|
||||||
$visitedFragmentNames
|
if ($selectionSet) {
|
||||||
);
|
$subFieldASTs = self::collectFields(
|
||||||
|
$exeContext,
|
||||||
|
$runtimeType,
|
||||||
|
$selectionSet,
|
||||||
|
$subFieldASTs,
|
||||||
|
$visitedFragmentNames
|
||||||
|
);
|
||||||
|
$exeContext->memoized['collectSubFields'][$uid][$runtimeType->name] = $subFieldASTs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -622,4 +667,15 @@ class Executor
|
|||||||
$tmp = $parentType->getFields();
|
$tmp = $parentType->getFields();
|
||||||
return isset($tmp[$fieldName]) ? $tmp[$fieldName] : null;
|
return isset($tmp[$fieldName]) ? $tmp[$fieldName] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an unique identifier for a FieldAST.
|
||||||
|
*
|
||||||
|
* @param object $fieldAST
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function getFieldUid($fieldAST)
|
||||||
|
{
|
||||||
|
return $fieldAST->loc->start . '-' . $fieldAST->loc->end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -546,4 +546,99 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
$this->assertEquals($expected, $result->toArray());
|
$this->assertEquals($expected, $result->toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testResolvedValueIsMemoized()
|
||||||
|
{
|
||||||
|
$doc = '
|
||||||
|
query Q {
|
||||||
|
a {
|
||||||
|
b {
|
||||||
|
c
|
||||||
|
d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
';
|
||||||
|
|
||||||
|
$memoizedValue = new \ArrayObject([
|
||||||
|
'b' => 'id1'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$A = null;
|
||||||
|
|
||||||
|
$Test = new ObjectType([
|
||||||
|
'name' => 'Test',
|
||||||
|
'fields' => [
|
||||||
|
'a' => [
|
||||||
|
'type' => function() use (&$A) {return Type::listOf($A);},
|
||||||
|
'resolve' => function() use ($memoizedValue) {
|
||||||
|
return [
|
||||||
|
$memoizedValue,
|
||||||
|
new \ArrayObject([
|
||||||
|
'b' => 'id2',
|
||||||
|
]),
|
||||||
|
$memoizedValue,
|
||||||
|
new \ArrayObject([
|
||||||
|
'b' => 'id2',
|
||||||
|
])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$callCounts = ['id1' => 0, 'id2' => 0];
|
||||||
|
|
||||||
|
$A = new ObjectType([
|
||||||
|
'name' => 'A',
|
||||||
|
'fields' => [
|
||||||
|
'b' => [
|
||||||
|
'type' => new ObjectType([
|
||||||
|
'name' => 'B',
|
||||||
|
'fields' => [
|
||||||
|
'c' => ['type' => Type::string()],
|
||||||
|
'd' => ['type' => Type::string()]
|
||||||
|
]
|
||||||
|
]),
|
||||||
|
'resolve' => function($value) use (&$callCounts) {
|
||||||
|
$callCounts[$value['b']]++;
|
||||||
|
|
||||||
|
switch ($value['b']) {
|
||||||
|
case 'id1':
|
||||||
|
return [
|
||||||
|
'c' => 'c1',
|
||||||
|
'd' => 'd1'
|
||||||
|
];
|
||||||
|
case 'id2':
|
||||||
|
return [
|
||||||
|
'c' => 'c2',
|
||||||
|
'd' => 'd2'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Test that value resolved once is memoized for same query field
|
||||||
|
$schema = new Schema($Test);
|
||||||
|
|
||||||
|
$query = Parser::parse($doc);
|
||||||
|
$result = Executor::execute($schema, $query);
|
||||||
|
$expected = [
|
||||||
|
'data' => [
|
||||||
|
'a' => [
|
||||||
|
['b' => ['c' => 'c1', 'd' => 'd1']],
|
||||||
|
['b' => ['c' => 'c2', 'd' => 'd2']],
|
||||||
|
['b' => ['c' => 'c1', 'd' => 'd1']],
|
||||||
|
['b' => ['c' => 'c2', 'd' => 'd2']],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $result->toArray());
|
||||||
|
|
||||||
|
$this->assertSame($callCounts['id1'], 1); // Result for id1 is expected to be memoized after first call
|
||||||
|
$this->assertSame($callCounts['id2'], 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user