Skip to content

Commit 3b3da9e

Browse files
committed
Several performance improvements (#8)
1 parent d982bad commit 3b3da9e

File tree

3 files changed

+211
-55
lines changed

3 files changed

+211
-55
lines changed

src/Executor/ExecutionContext.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ class ExecutionContext
4343
*/
4444
public $errors;
4545

46+
/**
47+
* @var array
48+
*/
49+
public $memoized = [];
50+
4651
public function __construct($schema, $fragments, $root, $operation, $variables, $errors)
4752
{
4853
$this->schema = $schema;

src/Executor/Executor.php

Lines changed: 111 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -359,58 +359,95 @@ private static function getFieldEntryKey(Field $node)
359359
private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $source, $fieldASTs)
360360
{
361361
$fieldAST = $fieldASTs[0];
362-
$fieldName = $fieldAST->name->value;
363362

364-
$fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName);
363+
$uid = self::getFieldUid($fieldAST);
365364

366-
if (!$fieldDef) {
367-
return self::$UNDEFINED;
365+
// Get memoized variables if they exist
366+
if (isset($exeContext->memoized['resolveField'][$uid])) {
367+
$memoized = $exeContext->memoized['resolveField'][$uid];
368+
$fieldDef = $memoized['fieldDef'];
369+
$returnType = $fieldDef->getType();
370+
$args = $memoized['args'];
371+
$info = $memoized['info'];
368372
}
373+
else {
374+
$fieldName = $fieldAST->name->value;
369375

370-
$returnType = $fieldDef->getType();
376+
$fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName);
371377

372-
if (isset($fieldDef->resolveFn)) {
373-
$resolveFn = $fieldDef->resolveFn;
374-
} else if (isset($parentType->resolveFieldFn)) {
375-
$resolveFn = $parentType->resolveFieldFn;
378+
if (!$fieldDef) {
379+
return self::$UNDEFINED;
380+
}
381+
382+
$returnType = $fieldDef->getType();
383+
384+
// Build hash of arguments from the field.arguments AST, using the
385+
// variables scope to fulfill any variable references.
386+
// TODO: find a way to memoize, in case this field is within a List type.
387+
$args = Values::getArgumentValues(
388+
$fieldDef->args,
389+
$fieldAST->arguments,
390+
$exeContext->variableValues
391+
);
392+
393+
// The resolve function's optional third argument is a collection of
394+
// information about the current execution state.
395+
$info = new ResolveInfo([
396+
'fieldName' => $fieldName,
397+
'fieldASTs' => $fieldASTs,
398+
'returnType' => $returnType,
399+
'parentType' => $parentType,
400+
'schema' => $exeContext->schema,
401+
'fragments' => $exeContext->fragments,
402+
'rootValue' => $exeContext->rootValue,
403+
'operation' => $exeContext->operation,
404+
'variableValues' => $exeContext->variableValues,
405+
]);
406+
407+
// Memoizing results for same query field
408+
// (useful for lists when several values are resolved against the same field)
409+
if ($returnType instanceof ObjectType) {
410+
$memoized = $exeContext->memoized['resolveField'][$uid] = [
411+
'fieldDef' => $fieldDef,
412+
'args' => $args,
413+
'info' => $info,
414+
'results' => new \SplObjectStorage
415+
];
416+
}
417+
}
418+
419+
// When source value is object it is possible to memoize certain subset of results
420+
$isObject = is_object($source);
421+
422+
if ($isObject && isset($memoized['results'][$source])) {
423+
$result = $exeContext->memoized['resolveField'][$uid]['results'][$source];
376424
} else {
377-
$resolveFn = self::$defaultResolveFn;
378-
}
379-
380-
// Build hash of arguments from the field.arguments AST, using the
381-
// variables scope to fulfill any variable references.
382-
// TODO: find a way to memoize, in case this field is within a List type.
383-
$args = Values::getArgumentValues(
384-
$fieldDef->args,
385-
$fieldAST->arguments,
386-
$exeContext->variableValues
387-
);
388-
389-
// The resolve function's optional third argument is a collection of
390-
// information about the current execution state.
391-
$info = new ResolveInfo([
392-
'fieldName' => $fieldName,
393-
'fieldASTs' => $fieldASTs,
394-
'returnType' => $returnType,
395-
'parentType' => $parentType,
396-
'schema' => $exeContext->schema,
397-
'fragments' => $exeContext->fragments,
398-
'rootValue' => $exeContext->rootValue,
399-
'operation' => $exeContext->operation,
400-
'variableValues' => $exeContext->variableValues,
401-
]);
402-
403-
// Get the resolve function, regardless of if its result is normal
404-
// or abrupt (error).
405-
$result = self::resolveOrError($resolveFn, $source, $args, $info);
406-
407-
return self::completeValueCatchingError(
408-
$exeContext,
409-
$returnType,
410-
$fieldASTs,
411-
$info,
412-
$result
413-
);
425+
if (isset($fieldDef->resolveFn)) {
426+
$resolveFn = $fieldDef->resolveFn;
427+
} else if (isset($parentType->resolveFieldFn)) {
428+
$resolveFn = $parentType->resolveFieldFn;
429+
} else {
430+
$resolveFn = self::$defaultResolveFn;
431+
}
432+
433+
// Get the resolve function, regardless of if its result is normal
434+
// or abrupt (error).
435+
$result = self::resolveOrError($resolveFn, $source, $args, $info);
436+
437+
$result = self::completeValueCatchingError(
438+
$exeContext,
439+
$returnType,
440+
$fieldASTs,
441+
$info,
442+
$result
443+
);
444+
445+
if ($isObject && isset($memoized['results'])) {
446+
$exeContext->memoized['resolveField'][$uid]['results'][$source] = $result;
447+
}
448+
}
449+
450+
return $result;
414451
}
415452

416453
// Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField`
@@ -554,15 +591,23 @@ private static function completeValue(ExecutionContext $exeContext, Type $return
554591
$subFieldASTs = new \ArrayObject();
555592
$visitedFragmentNames = new \ArrayObject();
556593
for ($i = 0; $i < count($fieldASTs); $i++) {
557-
$selectionSet = $fieldASTs[$i]->selectionSet;
558-
if ($selectionSet) {
559-
$subFieldASTs = self::collectFields(
560-
$exeContext,
561-
$runtimeType,
562-
$selectionSet,
563-
$subFieldASTs,
564-
$visitedFragmentNames
565-
);
594+
// Get memoized value if it exists
595+
$uid = self::getFieldUid($fieldASTs[$i]);
596+
if (isset($exeContext->memoized['collectSubFields'][$uid][$runtimeType->name])) {
597+
$subFieldASTs = $exeContext->memoized['collectSubFields'][$uid][$runtimeType->name];
598+
}
599+
else {
600+
$selectionSet = $fieldASTs[$i]->selectionSet;
601+
if ($selectionSet) {
602+
$subFieldASTs = self::collectFields(
603+
$exeContext,
604+
$runtimeType,
605+
$selectionSet,
606+
$subFieldASTs,
607+
$visitedFragmentNames
608+
);
609+
$exeContext->memoized['collectSubFields'][$uid][$runtimeType->name] = $subFieldASTs;
610+
}
566611
}
567612
}
568613

@@ -622,4 +667,15 @@ private static function getFieldDef(Schema $schema, ObjectType $parentType, $fie
622667
$tmp = $parentType->getFields();
623668
return isset($tmp[$fieldName]) ? $tmp[$fieldName] : null;
624669
}
670+
671+
/**
672+
* Get an unique identifier for a FieldAST.
673+
*
674+
* @param object $fieldAST
675+
* @return string
676+
*/
677+
private static function getFieldUid($fieldAST)
678+
{
679+
return $fieldAST->loc->start . '-' . $fieldAST->loc->end;
680+
}
625681
}

tests/Executor/ExecutorTest.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,4 +546,99 @@ public function testSubstitutesArgumentWithDefaultValue()
546546

547547
$this->assertEquals($expected, $result->toArray());
548548
}
549+
550+
public function testResolvedValueIsMemoized()
551+
{
552+
$doc = '
553+
query Q {
554+
a {
555+
b {
556+
c
557+
d
558+
}
559+
}
560+
}
561+
';
562+
563+
$memoizedValue = new \ArrayObject([
564+
'b' => 'id1'
565+
]);
566+
567+
$A = null;
568+
569+
$Test = new ObjectType([
570+
'name' => 'Test',
571+
'fields' => [
572+
'a' => [
573+
'type' => function() use (&$A) {return Type::listOf($A);},
574+
'resolve' => function() use ($memoizedValue) {
575+
return [
576+
$memoizedValue,
577+
new \ArrayObject([
578+
'b' => 'id2',
579+
]),
580+
$memoizedValue,
581+
new \ArrayObject([
582+
'b' => 'id2',
583+
])
584+
];
585+
}
586+
]
587+
]
588+
]);
589+
590+
$callCounts = ['id1' => 0, 'id2' => 0];
591+
592+
$A = new ObjectType([
593+
'name' => 'A',
594+
'fields' => [
595+
'b' => [
596+
'type' => new ObjectType([
597+
'name' => 'B',
598+
'fields' => [
599+
'c' => ['type' => Type::string()],
600+
'd' => ['type' => Type::string()]
601+
]
602+
]),
603+
'resolve' => function($value) use (&$callCounts) {
604+
$callCounts[$value['b']]++;
605+
606+
switch ($value['b']) {
607+
case 'id1':
608+
return [
609+
'c' => 'c1',
610+
'd' => 'd1'
611+
];
612+
case 'id2':
613+
return [
614+
'c' => 'c2',
615+
'd' => 'd2'
616+
];
617+
}
618+
}
619+
]
620+
]
621+
]);
622+
623+
// Test that value resolved once is memoized for same query field
624+
$schema = new Schema($Test);
625+
626+
$query = Parser::parse($doc);
627+
$result = Executor::execute($schema, $query);
628+
$expected = [
629+
'data' => [
630+
'a' => [
631+
['b' => ['c' => 'c1', 'd' => 'd1']],
632+
['b' => ['c' => 'c2', 'd' => 'd2']],
633+
['b' => ['c' => 'c1', 'd' => 'd1']],
634+
['b' => ['c' => 'c2', 'd' => 'd2']],
635+
]
636+
]
637+
];
638+
639+
$this->assertEquals($expected, $result->toArray());
640+
641+
$this->assertSame($callCounts['id1'], 1); // Result for id1 is expected to be memoized after first call
642+
$this->assertSame($callCounts['id2'], 2);
643+
}
549644
}

0 commit comments

Comments
 (0)