Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
8a60c6d
Optimize deferred
spawnia Dec 5, 2025
19efb10
Autofix
autofix-ci[bot] Dec 5, 2025
91e0500
extend test
spawnia Dec 9, 2025
dc45cf7
Optimize Deferred memory usage with compact queue storage
spawnia Dec 9, 2025
0fafd82
Merge branch 'master' into optimize-deferred
spawnia Dec 9, 2025
fc5dcd3
Merge branch 'master' into optimize-deferred
spawnia Dec 9, 2025
87866bd
Apply latest rector changes
spawnia Dec 9, 2025
42256fc
clean up comments
spawnia Dec 9, 2025
1954f81
link issue
spawnia Dec 9, 2025
69a13ad
Use SplFixedArray for waiting queue items
spawnia Dec 11, 2025
26d7319
Merge branch 'master' into optimize-deferred
spawnia Dec 12, 2025
2c8e59d
Merge branch 'master' into optimize-deferred
spawnia Dec 15, 2025
60525b4
rename $q to $queue
spawnia Dec 15, 2025
97c08cf
remove dead code
spawnia Dec 15, 2025
a5cda31
Simplify queue to hold only SyncPromise instances
spawnia Dec 15, 2025
f80ea17
readable ternary
spawnia Dec 15, 2025
1a93c00
extract + better comment
spawnia Dec 15, 2025
816f773
Merge branch 'master' into optimize-deferred
spawnia Dec 15, 2025
7a9a686
extract queue
spawnia Dec 15, 2025
df37bfb
clean up properties
spawnia Dec 15, 2025
eeb60e9
order methods
spawnia Dec 15, 2025
b73c8dc
format
spawnia Dec 15, 2025
b6b095d
split classes
spawnia Dec 15, 2025
aeef4ce
Merge branch 'master' into optimize-deferred
spawnia Dec 15, 2025
b542f16
simplify ChildSyncPromise, benchmark is the same
spawnia Dec 15, 2025
eb4f736
comment RootSyncPromise
spawnia Dec 15, 2025
a405427
Merge branch 'master' into optimize-deferred
spawnia Dec 16, 2025
4cd7423
changelog
spawnia Dec 16, 2025
5ed1514
add DeferredBench, track memory
spawnia Dec 16, 2025
1427bfa
more revs for benchmarks
spawnia Dec 16, 2025
dfd1f05
Optimize SyncPromise with hybrid closure approach
spawnia Dec 16, 2025
ce0db16
format
spawnia Dec 16, 2025
ad597a8
fix test phpstan
spawnia Dec 16, 2025
77c1725
reorder
spawnia Dec 16, 2025
31d24a1
format
spawnia Dec 16, 2025
d679079
simplify
spawnia Dec 16, 2025
9eee711
reduce variance
spawnia Dec 16, 2025
ba40eea
temporary compat layer
spawnia Dec 16, 2025
0b897dd
reorder methods
spawnia Dec 16, 2025
e971258
cs-fixer
spawnia Dec 16, 2025
c35557c
remove unnecessary null-check
spawnia Dec 16, 2025
956d933
always bench without assertions
spawnia Dec 16, 2025
9b56e21
simplify
spawnia Dec 16, 2025
0530d67
unalias $this
spawnia Dec 16, 2025
1a0e0e2
Merge branch 'master' into optimize-deferred
spawnia Dec 17, 2025
d9718dc
inline
spawnia Dec 17, 2025
9b70533
document Deferred
spawnia Dec 17, 2025
6c76d05
Move $executor to Deferred
spawnia Dec 17, 2025
2b48656
clean up message
spawnia Dec 17, 2025
c271430
docs
spawnia Dec 17, 2025
eff1d5c
improve adoptedPromise usage naming
spawnia Dec 17, 2025
b941c37
int state
spawnia Dec 17, 2025
3e77e0b
single batch
spawnia Dec 17, 2025
f2e5221
phpstan-ignore
spawnia Dec 18, 2025
6aa7d57
nullable
spawnia Dec 18, 2025
09518ec
remove workaround
spawnia Dec 18, 2025
24db74b
clean up
spawnia Dec 18, 2025
1e73362
prepare release
spawnia Dec 18, 2025
f03150e
Merge branch 'master' into optimize-deferred
spawnia Dec 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ You can find and compare releases at the [GitHub release page](https://github.co

## Unreleased

## v15.29.0

### Changed

- Optimize `Deferred` execution https://github.com/webonyx/graphql-php/pull/1805

### Deprecated

- Deprecate `GraphQL\Deferred::create()` in favor of constructor https://github.com/webonyx/graphql-php/pull/1805

## v15.28.0

### Changed
Expand Down
80 changes: 80 additions & 0 deletions benchmarks/DeferredBench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php declare(strict_types=1);

namespace GraphQL\Benchmarks;

use GraphQL\Deferred;
use GraphQL\Executor\Promise\Adapter\SyncPromiseQueue;

/**
* @OutputTimeUnit("microseconds", precision=3)
*
* @Warmup(5)
*
* @Revs(200)
*
* @Iterations(10)
*/
class DeferredBench
{
public function benchSingleDeferred(): void
{
new Deferred(static fn () => 'value');
SyncPromiseQueue::run();
}

public function benchNestedDeferred(): void
{
new Deferred(static fn () => new Deferred(static fn () => null));
SyncPromiseQueue::run();
}

public function benchChain5(): void
{
$deferred = new Deferred(static fn () => 'value');
$deferred->then(static fn ($v) => $v)
->then(static fn ($v) => $v)
->then(static fn ($v) => $v)
->then(static fn ($v) => $v)
->then(static fn ($v) => $v);
SyncPromiseQueue::run();
}

public function benchChain100(): void
{
$deferred = new Deferred(static fn () => 'value');
$promise = $deferred;
for ($i = 0; $i < 100; ++$i) {
$promise = $promise->then(static fn ($v) => $v);
}
SyncPromiseQueue::run();
}

public function benchManyDeferreds(): void
{
$fn = static fn () => null;
for ($i = 0; $i < 1000; ++$i) {
new Deferred($fn);
}
SyncPromiseQueue::run();
}

public function benchManyNestedDeferreds(): void
{
for ($i = 0; $i < 5000; ++$i) {
new Deferred(static fn () => new Deferred(static fn () => null));
}
SyncPromiseQueue::run();
}

public function bench1000Chains(): void
{
$promises = [];
for ($i = 0; $i < 1000; ++$i) {
$d = new Deferred(static fn () => $i);
$promises[] = $d->then(static fn ($v) => $v)
->then(static fn ($v) => $v)
->then(static fn ($v) => $v);
}
SyncPromiseQueue::run();
}
}
7 changes: 4 additions & 3 deletions benchmarks/HugeSchemaBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
*
* @OutputTimeUnit("milliseconds", precision=3)
*
* @Warmup(1)
* @Warmup(2)
*
* @Revs(5)
* @Revs(10)
*
* @Iterations(1)
* @Iterations(3)
*/
class HugeSchemaBench
{
Expand Down Expand Up @@ -50,6 +50,7 @@ public function benchSchema(): void
->getTypeMap();
}

/** @Revs(1000) */
public function benchSchemaLazy(): void
{
$this->createLazySchema();
Expand Down
6 changes: 3 additions & 3 deletions benchmarks/StarWarsBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
*
* @OutputTimeUnit("milliseconds", precision=3)
*
* @Warmup(2)
* @Warmup(5)
*
* @Revs(10)
* @Revs(100)
*
* @Iterations(2)
* @Iterations(10)
*/
class StarWarsBench
{
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
},
"scripts": {
"baseline": "phpstan --generate-baseline",
"bench": "phpbench run",
"bench": "php -d zend.assertions=-1 vendor/bin/phpbench run --report=default",
"check": [
"@fix",
"@stan",
Expand All @@ -78,6 +78,6 @@
"php-cs-fixer": "make php-cs-fixer",
"rector": "rector process",
"stan": "phpstan --verbose",
"test": "php -d zend.exception_ignore_args=Off -d zend.assertions=On -d assert.active=On -d assert.exception=On vendor/bin/phpunit"
"test": "php -d zend.exception_ignore_args=0 -d zend.assertions=1 -d assert.active=1 -d assert.exception=1 vendor/bin/phpunit"
}
}
19 changes: 19 additions & 0 deletions docs/class-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1778,6 +1778,25 @@ function createRejected(Throwable $reason): GraphQL\Executor\Promise\Promise
function all(iterable $promisesOrValues): GraphQL\Executor\Promise\Promise
```

## GraphQL\Deferred

User-facing promise class for deferred field resolution.

@phpstan-type Executor callable(): mixed

### GraphQL\Deferred Methods

```php
/**
* Create a new Deferred promise and enqueue its execution.
*
* @api
*
* @param Executor $executor
*/
function __construct(callable $executor)
```

## GraphQL\Validator\DocumentValidator

Implements the "Validation" section of the spec.
Expand Down
1 change: 1 addition & 0 deletions generate-class-reference.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
GraphQL\Executor\ScopedContext::class => [],
GraphQL\Executor\ExecutionResult::class => [],
GraphQL\Executor\Promise\PromiseAdapter::class => [],
GraphQL\Deferred::class => [],
GraphQL\Validator\DocumentValidator::class => [],
GraphQL\Error\Error::class => ['constants' => true],
GraphQL\Error\Warning::class => ['constants' => true],
Expand Down
8 changes: 7 additions & 1 deletion phpbench.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,11 @@
"runner.path": "benchmarks",
"runner.file_pattern": "*Bench.php",
"runner.retry_threshold": 5,
"runner.time_unit": "milliseconds"
"runner.time_unit": "milliseconds",
"runner.progress": "plain",
"report.generators": {
"default": {
"extends": "aggregate"
}
}
}
48 changes: 41 additions & 7 deletions src/Deferred.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,55 @@
namespace GraphQL;

use GraphQL\Executor\Promise\Adapter\SyncPromise;
use GraphQL\Executor\Promise\Adapter\SyncPromiseQueue;

/**
* @phpstan-import-type Executor from SyncPromise
* User-facing promise class for deferred field resolution.
*
* @phpstan-type Executor callable(): mixed
*/
class Deferred extends SyncPromise
{
/** @param Executor $executor */
public static function create(callable $executor): self
/**
* Executor for deferred promises.
*
* @var (callable(): mixed)|null
*/
protected $executor;

/**
* Create a new Deferred promise and enqueue its execution.
*
* @api
*
* @param Executor $executor
*/
public function __construct(callable $executor)
{
return new self($executor);
$this->executor = $executor;

SyncPromiseQueue::enqueue(function (): void {
$executor = $this->executor;
assert($executor !== null, 'Always set in constructor, this callback runs only once.');
$this->executor = null;

try {
$this->resolve($executor());
} catch (\Throwable $e) {
$this->reject($e);
}
});
}

/** @param Executor $executor */
public function __construct(callable $executor)
/**
* Alias for __construct.
*
* @param Executor $executor
*
* @deprecated TODO remove in next major version, use new Deferred() instead
*/
public static function create(callable $executor): self
{
parent::__construct($executor);
return new self($executor);
}
}
7 changes: 3 additions & 4 deletions src/Executor/Promise/Adapter/AmpPromiseAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,9 @@ public function then(Promise $promise, ?callable $onFulfilled = null, ?callable
}
};

$adoptedPromise = $promise->adoptedPromise;
assert($adoptedPromise instanceof AmpPromise);

$adoptedPromise->onResolve($onResolve);
$ampPromise = $promise->adoptedPromise;
assert($ampPromise instanceof AmpPromise);
$ampPromise->onResolve($onResolve);

return new Promise($deferred->promise(), $this);
}
Expand Down
22 changes: 11 additions & 11 deletions src/Executor/Promise/Adapter/ReactPromiseAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,34 @@ public function convertThenable($thenable): Promise
/** @throws InvariantViolation */
public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise
{
$adoptedPromise = $promise->adoptedPromise;
assert($adoptedPromise instanceof ReactPromiseInterface);
$reactPromise = $promise->adoptedPromise;
assert($reactPromise instanceof ReactPromiseInterface);

return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this);
return new Promise($reactPromise->then($onFulfilled, $onRejected), $this);
}

/** @throws InvariantViolation */
public function create(callable $resolver): Promise
{
$promise = new ReactPromise($resolver);
$reactPromise = new ReactPromise($resolver);

return new Promise($promise, $this);
return new Promise($reactPromise, $this);
}

/** @throws InvariantViolation */
public function createFulfilled($value = null): Promise
{
$promise = resolve($value);
$reactPromise = resolve($value);

return new Promise($promise, $this);
return new Promise($reactPromise, $this);
}

/** @throws InvariantViolation */
public function createRejected(\Throwable $reason): Promise
{
$promise = reject($reason);
$reactPromise = reject($reason);

return new Promise($promise, $this);
return new Promise($reactPromise, $this);
}

/** @throws InvariantViolation */
Expand All @@ -70,11 +70,11 @@ public function all(iterable $promisesOrValues): Promise
$promisesOrValuesArray = is_array($promisesOrValues)
? $promisesOrValues
: iterator_to_array($promisesOrValues);
$promise = all($promisesOrValuesArray)->then(static fn ($values): array => array_map(
$reactPromise = all($promisesOrValuesArray)->then(static fn (array $values): array => array_map(
static fn ($key) => $values[$key],
array_keys($promisesOrValuesArray),
));

return new Promise($promise, $this);
return new Promise($reactPromise, $this);
}
}
Loading
Loading