Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance optimizations and caching #698

Merged
merged 30 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fc6b0e1
Combine addTypeNamespace() and addControllerNamespace() into addNames…
oprypkhantc Mar 28, 2024
5326b60
Refactor CachedDocBlockFactory to use existing caching infrastructure
oprypkhantc Mar 28, 2024
c6be5ab
Refactor field name
oprypkhantc Mar 29, 2024
bb8e81b
Refactor code to use ClassFinder instead of separating namespaces
oprypkhantc Mar 30, 2024
eb799e6
Merge branch 'refs/heads/master' into caching
oprypkhantc Apr 8, 2024
173a634
Make caches only recompute on file changes
oprypkhantc Apr 8, 2024
6f21eb2
Refactor EnumTypeMapper to use cached doc block factory
oprypkhantc Apr 8, 2024
3797994
Fix FileModificationClassFinderBoundCache
oprypkhantc Apr 10, 2024
7165342
Use Kcs ClassFinder cache
oprypkhantc Aug 25, 2024
821e85f
One more tiny optimization for unchanged cache
oprypkhantc Aug 25, 2024
2b60d83
Change name of ClassFinderBoundCache
oprypkhantc Aug 26, 2024
401cdbd
Merge remote-tracking branch 'upstream/master' into caching
oprypkhantc Aug 26, 2024
d52b18a
Code style
oprypkhantc Aug 26, 2024
3fc9932
PHPStan
oprypkhantc Aug 26, 2024
4e8e8b0
Some fixes and renames
oprypkhantc Aug 26, 2024
0e2218c
Fix broken class bound cache after merge
oprypkhantc Aug 27, 2024
28775bd
Fix some tests and PHPStan
oprypkhantc Aug 27, 2024
e0880c4
Fix all failing tests
oprypkhantc Aug 27, 2024
6472775
Fix one failing test on CI
oprypkhantc Aug 27, 2024
d20853c
Fix one failing test on CI, again
oprypkhantc Aug 27, 2024
294c733
Fix --prefer-lowest tests
oprypkhantc Aug 27, 2024
5830002
Some simplifications and tests
oprypkhantc Sep 2, 2024
87a992b
Simplify cached doc blocks
oprypkhantc Sep 2, 2024
8caa5c1
Simplify cached doc blocks
oprypkhantc Sep 2, 2024
0adf9d9
More tests for doc block factories
oprypkhantc Sep 2, 2024
df0ee58
Tests for the Discovery namespace
oprypkhantc Sep 2, 2024
dd5ba83
Fix the docs build on CI and broken links
oprypkhantc Sep 12, 2024
b251a7e
Add a changelog entry
oprypkhantc Sep 12, 2024
44cc773
Deprecate setGlobTTL() instead of removing it
oprypkhantc Sep 12, 2024
2bc9ecf
Code style
oprypkhantc Sep 13, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/doc_generation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: "Setup NodeJS"
uses: actions/setup-node@v4
with:
node-version: '16.x'
node-version: '20.x'

- name: "Yarn install"
run: yarn install
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"symfony/cache": "^4.3 || ^5 || ^6 || ^7",
"symfony/expression-language": "^4 || ^5 || ^6 || ^7",
"webonyx/graphql-php": "^v15.0",
"kcs/class-finder": "^0.5.0"
"kcs/class-finder": "^0.5.1"
},
"require-dev": {
"beberlei/porpaginas": "^1.2 || ^2.0",
Expand All @@ -34,7 +34,7 @@
"myclabs/php-enum": "^1.6.6",
"php-coveralls/php-coveralls": "^2.1",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.9",
"phpstan/phpstan": "^1.11",
"phpunit/phpunit": "^10.1 || ^11.0",
"symfony/var-dumper": "^5.4 || ^6.0 || ^7",
"thecodingmachine/phpstan-strict-rules": "^1.0"
Expand Down
3 changes: 1 addition & 2 deletions examples/no-framework/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@
]);

$factory = new SchemaFactory($cache, $container);
$factory->addControllerNamespace('App\\Controllers')
->addTypeNamespace('App');
$factory->addNamespace('App');

$schema = $factory->createSchema();

Expand Down
35 changes: 21 additions & 14 deletions src/AggregateControllerQueryProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,12 @@
use Psr\Container\ContainerInterface;
use TheCodingMachine\GraphQLite\Mappers\DuplicateMappingException;

use function array_filter;
use function array_intersect_key;
use function array_keys;
use function array_map;
use function array_merge;
use function array_sum;
use function array_values;
use function assert;
use function count;
use function reset;
use function sort;

/**
Expand Down Expand Up @@ -94,18 +90,29 @@ private function flattenList(array $list): array
}

// We have an issue, let's detect the duplicate
$duplicates = array_intersect_key(...array_values($list));
// Let's display an error from the first one.
$firstDuplicate = reset($duplicates);
assert($firstDuplicate instanceof FieldDefinition);
$queriesByName = [];
$duplicateClasses = null;
$duplicateQueryName = null;

$duplicateName = $firstDuplicate->name;
foreach ($list as $class => $queries) {
foreach ($queries as $query => $field) {
$duplicatedClass = $queriesByName[$query] ?? null;

$classes = array_keys(array_filter($list, static function (array $fields) use ($duplicateName) {
return isset($fields[$duplicateName]);
}));
sort($classes);
if (! $duplicatedClass) {
$queriesByName[$query] = $class;

throw DuplicateMappingException::createForQueryInTwoControllers($classes[0], $classes[1], $duplicateName);
continue;
}

$duplicateClasses = [$duplicatedClass, $class];
$duplicateQueryName = $query;
}
}

assert($duplicateClasses !== null && $duplicateQueryName !== null);

sort($duplicateClasses);

throw DuplicateMappingException::createForQueryInTwoControllers($duplicateClasses[0], $duplicateClasses[1], $duplicateQueryName);
}
}
24 changes: 24 additions & 0 deletions src/Cache/ClassBoundCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Cache;

use ReflectionClass;

interface ClassBoundCache
{
/**
* @param callable(): TReturn $resolver
*
* @return TReturn
*
* @template TReturn
*/
public function get(
ReflectionClass $reflectionClass,
callable $resolver,
string $key,
bool $withInheritance = false,
): mixed;
}
43 changes: 0 additions & 43 deletions src/Cache/ClassBoundCacheContract.php

This file was deleted.

15 changes: 0 additions & 15 deletions src/Cache/ClassBoundCacheContractFactory.php

This file was deleted.

12 changes: 0 additions & 12 deletions src/Cache/ClassBoundCacheContractFactoryInterface.php

This file was deleted.

13 changes: 0 additions & 13 deletions src/Cache/ClassBoundCacheContractInterface.php

This file was deleted.

86 changes: 86 additions & 0 deletions src/Cache/FilesSnapshot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Cache;

use ReflectionClass;

use function array_unique;
use function Safe\filemtime;

class FilesSnapshot
{
/** @param array<string, int> $dependencies */
private function __construct(
private readonly array $dependencies,
)
{
}

/** @param list<string> $files */
public static function for(array $files): self
{
$dependencies = [];

foreach (array_unique($files) as $file) {
$dependencies[$file] = filemtime($file);
}

return new self($dependencies);
}

public static function forClass(ReflectionClass $class, bool $withInheritance = false): self
{
return self::for(
self::dependencies($class, $withInheritance),
);
}

public static function alwaysUnchanged(): self
{
return new self([]);
}

/** @return list<string> */
private static function dependencies(ReflectionClass $class, bool $withInheritance = false): array
{
$filename = $class->getFileName();

// Internal classes are treated as always the same, e.g. you'll have to drop the cache between PHP versions.
if ($filename === false) {
return [];
}

$files = [$filename];

if (! $withInheritance) {
return $files;
}

if ($class->getParentClass() !== false) {
$files = [...$files, ...self::dependencies($class->getParentClass(), $withInheritance)];
}

foreach ($class->getTraits() as $trait) {
$files = [...$files, ...self::dependencies($trait, $withInheritance)];
}

foreach ($class->getInterfaces() as $interface) {
$files = [...$files, ...self::dependencies($interface, $withInheritance)];
}

return $files;
}

public function changed(): bool
{
foreach ($this->dependencies as $filename => $modificationTime) {
if ($modificationTime !== filemtime($filename)) {
return true;
}
}

return false;
}
}
41 changes: 41 additions & 0 deletions src/Cache/SnapshotClassBoundCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Cache;

use Psr\SimpleCache\CacheInterface;
use ReflectionClass;

use function str_replace;

class SnapshotClassBoundCache implements ClassBoundCache
{
/** @param callable(ReflectionClass, bool $withInheritance): FilesSnapshot $filesSnapshotFactory */
public function __construct(
private readonly CacheInterface $cache,
private readonly mixed $filesSnapshotFactory,
) {
}

public function get(ReflectionClass $reflectionClass, callable $resolver, string $key = '', bool $withInheritance = false): mixed
{
$cacheKey = $reflectionClass->getName() . '__' . $key;
$cacheKey = str_replace(['\\', '{', '}', '(', ')', '/', '@', ':'], '_', $cacheKey);

$item = $this->cache->get($cacheKey);

if ($item !== null && ! $item['snapshot']->changed()) {
return $item['data'];
}

$item = [
'data' => $resolver(),
'snapshot' => ($this->filesSnapshotFactory)($reflectionClass, $withInheritance),
];

$this->cache->set($cacheKey, $item);

return $item['data'];
}
}
41 changes: 41 additions & 0 deletions src/Discovery/Cache/ClassFinderComputedCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Discovery\Cache;

use ReflectionClass;
use TheCodingMachine\GraphQLite\Discovery\ClassFinder;

/**
* Cache that computes a final value based on class that exist in the application found with
* the {@see ClassFinder}, and one that allows invalidating only parts of the cache when those
* classes change, instead of having to invalidate the whole cache on every change.
*/
interface ClassFinderComputedCache
{
/**
* Compute the value of the cache. The $finder and $key are self-explanatory; the $map and $reduce need
* a bit of an explanation: $map is called with each reflection found by $finder, and expects any value to be returned.
* It will then be stored in a Map<string (filename), TEntry (return from $map)>. Once all classes are iterated,
* $reduce will then be called with that map, and it's final result is returned.
*
* Now the point of this is now whenever file A changes, we can automatically remove entries generated for it
* and simply call $map only for classes from file A, leaving all other entries untouched and not having to
* waste resources on the rest of them. We then only need to call the cheap $reduce and have the final result :)
*
* @param callable(ReflectionClass<object>): TEntry $map
* @param callable(array<string, TEntry>): TReturn $reduce
*
* @return TReturn
*
* @template TEntry of mixed
* @template TReturn of mixed
*/
public function compute(
ClassFinder $classFinder,
string $key,
callable $map,
callable $reduce,
): mixed;
}
Loading
Loading