Skip to content

Allow disabling return value validation to improve performance - #1871

Open
mahmudsudo wants to merge 17 commits into
webonyx:masterfrom
mahmudsudo:master
Open

Allow disabling return value validation to improve performance#1871
mahmudsudo wants to merge 17 commits into
webonyx:masterfrom
mahmudsudo:master

Conversation

@mahmudsudo

Copy link
Copy Markdown

closes #1493

@spawnia
spawnia requested a review from Copilot March 14, 2026 20:33
@spawnia
spawnia marked this pull request as draft March 14, 2026 20:34
@spawnia

spawnia commented Mar 14, 2026

Copy link
Copy Markdown
Collaborator

Please fix CI and address the Copilot review once it's available.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new trustResult flag intended to improve execution performance by skipping several runtime validations/serialization steps and “trusting” resolver return values.

Changes:

  • Added a trustResult boolean flag to the Executor and GraphQL public APIs and threaded it through execution context.
  • Added trustResult support to ServerConfig and passed it through the server execution helper.
  • Added PHPUnit coverage for the new behavior and updated the generated class reference signatures.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/Executor/TrustResultTest.php Adds tests demonstrating which validations are skipped when trustResult=true.
src/Server/ServerConfig.php Adds trustResult config option, setter, and getter.
src/Server/Helper.php Passes trustResult from ServerConfig into GraphQL::promiseToExecute().
src/GraphQL.php Adds trustResult parameter to facade APIs and forwards it to the executor.
src/Executor/ReferenceExecutor.php Skips non-null, leaf serialization, list iterable validation, and isTypeOf checks when trusted.
src/Executor/Executor.php Adds trustResult parameter to executor APIs and forwards it into the implementation factory.
src/Executor/ExecutionContext.php Stores trustResult in the execution context.
docs/class-reference.md Updates documented method signatures and phpstan type aliases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread src/Executor/Executor.php
Comment thread src/Executor/ReferenceExecutor.php
Comment thread tests/Executor/TrustResultTest.php Outdated
Comment thread src/GraphQL.php
Comment thread src/GraphQL.php
Comment thread src/GraphQL.php
@mahmudsudo
mahmudsudo marked this pull request as ready for review March 14, 2026 21:37
@mahmudsudo

Copy link
Copy Markdown
Author

@spawnia fixed ci errors and copilot reviews

@spawnia spawnia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not categorize this pull request as a fix - it is a new feature.

Have you benchmarked this?

Comment thread src/Executor/Executor.php Outdated
Comment thread src/GraphQL.php Outdated
@mahmudsudo mahmudsudo changed the title fix: disabled return/resolved values validation to improve performance feat: disabled return/resolved values validation to improve performance Mar 14, 2026
@mahmudsudo

Copy link
Copy Markdown
Author

@spawnia

@spawnia spawnia changed the title feat: disabled return/resolved values validation to improve performance Allow disabling return value validation to improve performance Mar 15, 2026
@spawnia

spawnia commented Mar 15, 2026

Copy link
Copy Markdown
Collaborator

I ran two sets of benchmarks.

No regression when trustResult is disabled (default)

The if ($this->exeContext->trustResult) checks added to the hot execution path have no measurable cost. Comparing all existing benchmarks (BuildSchemaBench, DeferredBench, HugeSchemaBench, LexerBench, ScalarOverrideBench, StarWarsBench) between this branch and master, every delta falls within normal run-to-run noise (±1–2%). No consistent directional regression was observed.

Gains when trustResult is enabled

A new TrustResultBench was added (benchmarks/TrustResultBench.php) covering three scenarios:

Scenario Without trustResult With trustResult Improvement
500 items × 8 built-in scalar fields (4 000 serialize() calls) 32.98ms 31.54ms ~4.4%
100 items × 5 custom scalar fields (preg_replace in serialize()) 5.04ms 4.57ms ~9.4%
100 objects with isTypeOf callback 3.39ms 3.14ms ~7.3%
30 orders with nested objects (combined) 3.30ms 3.24ms ~1.8%

The benefit scales with how expensive each serialize() / isTypeOf() call is. Built-in scalar serialization is cheap (simple type coercion), so gains there are modest; custom scalars or non-trivial isTypeOf checks show more improvement.

Benchmarks were run without opcache on PHP 8.3.6.

@shmax

shmax commented Mar 15, 2026

Copy link
Copy Markdown
Contributor

@spawnia isn't this covering the same ground as #1730 ?

@spawnia

spawnia commented Mar 15, 2026

Copy link
Copy Markdown
Collaborator

@shmax No, #1730 is about inputs - this is about outputs.

@shmax

shmax commented Mar 15, 2026

Copy link
Copy Markdown
Contributor

Ah, gotcha. 👍

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

src/Executor/Executor.php:187

  • Executor::promiseToExecute() calls the configured implementation factory with 10 positional arguments. On PHP 8+ this will throw an ArgumentCountError for older userland factories that only declare the pre-existing parameters, contradicting the new test expecting extra args to be ignored. Consider invoking the factory with only the number of parameters it declares (unless variadic) to preserve BC while still passing $trustResult to newer factories.
        $executor = (self::$implementationFactory)(
            $promiseAdapter,
            $schema,
            $documentNode,
            $rootValue,
            $contextValue,
            $variableValues ?? [],
            $operationName,
            $fieldResolver ?? self::$defaultFieldResolver,
            $argsMapper ?? self::$defaultArgsMapper,
            $trustResult,
        );

final class TrustResultTest extends TestCase
{
/** @see https://github.com/webonyx/graphql-php/issues/1493 */
public function testTrustResultSkipsListIterableValidation(): void
Comment on lines +896 to 898
if ($completed === null && ! $this->exeContext->trustResult) {
throw new InvariantViolation("Cannot return null for non-nullable field \"{$info->parentType}.{$info->fieldName}\".");
}

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (4)

src/Executor/Executor.php:180

  • Executor::promiseToExecute() invokes the user-provided implementation factory with the new $trustResult argument unconditionally. Any existing custom factory that was defined with fewer parameters will throw an ArgumentCountError, which defeats the BC goal captured by testCustomImplementationFactoryIgnoresExtraArguments(). Consider only passing as many arguments as the factory declares (unless it is variadic).
        $executor = (self::$implementationFactory)(
            $promiseAdapter,
            $schema,
            $documentNode,
            $rootValue,

tests/Executor/TrustResultTest.php:15

  • This test name says iterable validation is skipped, but the assertions verify that the iterable validation error is still present even when trustResult=true. Rename the test to reflect the behavior being asserted.
    public function testTrustResultSkipsListIterableValidation(): void

src/GraphQL.php:77

  • The trustResult option description says it "skips normal type and serialization checks", but some runtime type invariants are still enforced (e.g. list fields must return an iterable, and abstract types still require valid runtime types). Tighten the wording to match the actual behavior to avoid misleading users.
     * trustResult:
     *    When true, assumes resolver results are already correctly typed and serialized
     *    and skips normal type and serialization checks. This is purely for
     *    performance optimization. The tradeoff is potentially corrupted results for the client
     *    if resolvers return malformed data. Only enable this when you are confident

docs/class-reference.md:61

  • The trustResult option description here matches src/GraphQL.php and currently claims it "skips normal type and serialization checks", but the implementation still enforces some invariants (e.g. list fields must return iterable). Update the docs to match actual behavior.
 * trustResult:
 *    When true, assumes resolver results are already correctly typed and serialized
 *    and skips normal type and serialization checks. This is purely for
 *    performance optimization. The tradeoff is potentially corrupted results for the client
 *    if resolvers return malformed data. Only enable this when you are confident

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve performance by disabling return/resolved values validation

5 participants