Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [ 8.2 ]
php: [ 8.2, 8.3, 8.4 ]

steps:
- name: Checkout code
Expand Down Expand Up @@ -50,7 +50,7 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
php-version: 8.4
extensions: dom, curl, libxml, mbstring, zip
tools: composer:v2
coverage: none
Expand All @@ -76,7 +76,7 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [ 8.2 ]
php: [ 8.2, 8.3, 8.4 ]
steps:
- name: Checkout code
uses: actions/checkout@v3
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
!.idea/fileTemplates

# Dependencies
/benchmark/typhoon/vendor/
/benchmark/typhoon/composer.lock
/vendor/
/composer.lock

Expand Down
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ Here are some of the commands that you'll need:
- run tests with phpunit: `docker run -it --rm -v $PWD:/app -w /app chialab/php-dev:8.2 composer test`
- reformat using php-cs-fixer: `docker run -it --rm -v $PWD:/app -w /app chialab/php-dev:8.2 composer cs-fix`
- analyse with phpstan: `docker run -it --rm -v $PWD:/app -w /app chialab/php-dev:8.2 composer phpstan`
- benchmark: ` docker run -it --rm -v $"($env.PWD):/app" -v $"($env.PWD)/misc/opcache.ini:/usr/local/etc/php/conf.d/docker-php-ext-opcache.ini" -w /app chialab/php-dev:8.2`
- benchmark: `docker run -it --rm -v $"($env.PWD):/app" -v $"($env.PWD)/misc/opcache.ini:/usr/local/etc/php/conf.d/docker-php-ext-opcache.ini" -w /app chialab/php-dev:8.2`
- typhoon benchmark: `docker run -it --rm -v $"($env.PWD)/benchmark/typhoon:/app" -v $"($env.PWD)/misc/opcache.ini:/usr/local/etc/php/conf.d/docker-php-ext-opcache.ini" -v $"($env.PWD)/tests/Stubs:/app/tests/Stubs" -w /app chialab/php-dev:8.2 composer benchmark`
72 changes: 51 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,37 +64,67 @@ full Native PHP reflection and Roave/BetterReflection are generally faster, but
mind this also has to parse AST and DocBlocks to extract generics and types. Still,
I believe it to be fast enough to actually be used in production if you enable the cache.

Here is a reference benchmark, performed on an M1 MacBook Pro with OpCache:
Here is a reference benchmark, performed on an M1 MacBook Pro with OpCache and JIT:

```
~/Projects/Personal/good-php/reflection> docker run -it --rm -v $"($env.PWD):/app" -v $"($env.PWD)/misc/opcache.ini:/usr/local/etc/php/conf.d/docker-php-ext-opcache.ini" -w /app chialab/php-dev:8.2 composer benchmark
> Composer\Config::disableProcessTimeout
> vendor/bin/phpbench run tests/Benchmark
PHPBench (1.4.0) running benchmarks... #standwithukraine
with configuration file: /app/phpbench.json
with PHP version 8.2.27, xdebug ✔, opcache ✔

\Tests\Benchmark\ThisReflectionBench

benchWarmWithMemoryCache # only name....I49 - Mo0.011ms15.06%) [3.856mb / 4.779mb]
benchWarmWithMemoryCache # everything...I49 - Mo0.137ms5.25%) [9.970mb / 9.988mb]
benchWarmWithFileCache # only name......I49 - Mo0.047ms11.98%) [6.917mb / 6.958mb]
benchWarmWithFileCache # everything.....I49 - Mo0.172ms4.64%) [13.097mb / 13.114mb]
benchCold # only name...................I199 - Mo2.384ms12.80%) [2.143mb / 4.779mb]
benchCold # everything..................I199 - Mo2.506ms18.67%) [2.276mb / 4.779mb]
benchColdIncludingInitializationAndAuto.I199 - Mo74.279ms18.78%) [2.092mb / 4.779mb]
benchColdIncludingInitializationAndAuto.I199 - Mo72.901ms4.37%) [2.188mb / 4.779mb]
benchWarmWithMemoryCache # only name....I49 - Mo0.009ms4.68%) [2.149mb / 4.911mb]
benchWarmWithMemoryCache # everything...I49 - Mo0.038ms4.53%) [3.487mb / 4.911mb]
benchWarmWithFileCache # only name......I49 - Mo0.033ms2.12%) [2.229mb / 4.911mb]
benchWarmWithFileCache # everything.....I49 - Mo0.068ms3.02%) [6.352mb / 6.385mb]
benchCold # only name...................I199 - Mo2.519ms15.44%) [2.166mb / 4.911mb]
benchCold # everything..................I199 - Mo2.701ms12.87%) [2.267mb / 4.911mb]
benchColdIncludingInitializationAndAuto.I199 - Mo72.206ms3.00%) [2.132mb / 4.911mb]
benchColdIncludingInitializationAndAuto.I199 - Mo77.608ms10.01%) [2.217mb / 4.911mb]

\Tests\Benchmark\BetterReflectionBench
\Tests\Benchmark\NativeReflectionBench

benchWarmWithMemoryCache # only name....I49 - Mo0.005ms (±8.26%) [3.085mb / 4.779mb]
benchWarmWithMemoryCache # everything...I49 - Mo0.016ms (±5.70%) [3.093mb / 4.779mb]
benchCold # only name...................I199 - Mo1.693ms (±6.13%) [3.104mb / 4.779mb]
benchCold # everything..................I199 - Mo2.299ms (±14.67%) [3.116mb / 4.779mb]
benchColdIncludingInitializationAndAuto.I199 - Mo59.184ms (±5.79%) [3.084mb / 4.779mb]
benchColdIncludingInitializationAndAuto.I199 - Mo63.590ms (±18.52%) [3.092mb / 4.779mb]
benchWarm # only name...................I49 - Mo0.001ms (±13.48%) [575.552kb / 4.910mb]
benchWarm # everything..................I49 - Mo0.002ms (±9.02%) [575.616kb / 4.910mb]
benchCold # only name...................I199 - Mo0.005ms (±39.39%) [576.376kb / 4.911mb]
benchCold # everything..................I199 - Mo0.013ms (±32.23%) [576.376kb / 4.911mb]

\Tests\Benchmark\NativeReflectionBench
\Tests\Benchmark\BetterReflectionBench

benchWarm # only name...................I49 - Mo0.001ms (±9.11%) [517.504kb / 4.778mb]
benchWarm # everything..................I49 - Mo0.004ms (±6.08%) [517.568kb / 4.778mb]
benchCold # only name...................I199 - Mo0.009ms (±55.16%) [518.488kb / 4.779mb]
benchCold # everything..................I199 - Mo0.022ms (±23.29%) [518.488kb / 4.779mb]
benchWarmWithMemoryCache # only name....I49 - Mo0.003ms (±4.81%) [3.096mb / 4.911mb]
benchWarmWithMemoryCache # everything...I49 - Mo0.010ms (±3.97%) [3.137mb / 4.911mb]
benchCold # only name...................I199 - Mo1.265ms (±17.02%) [3.116mb / 4.911mb]
benchCold # everything..................I199 - Mo1.797ms (±13.45%) [3.103mb / 4.911mb]
benchColdIncludingInitializationAndAuto.I199 - Mo58.146ms (±9.89%) [3.095mb / 4.911mb]
benchColdIncludingInitializationAndAuto.I199 - Mo61.066ms (±3.91%) [3.074mb / 4.911mb]

Subjects: 9, Assertions: 0, Failures: 0, Errors: 0
~/Projects/Personal/good-php/reflection> docker run -it --rm -v $"($env.PWD)/benchmark/typhoon:/app" -v $"($env.PWD)/misc/opcache.ini:/usr/local/etc/php/conf.d/docker-php-ext-opcache.ini" -v $"($env.PWD)/tests/Stubs:/app/tests/Stubs" -w /app chialab/php-dev:8.2 composer benchmark riant everything
> Composer\Config::disableProcessTimeout
> vendor/bin/phpbench run src
PHPBench (1.4.0) running benchmarks... #standwithukraine
with configuration file: /app/phpbench.json
with PHP version 8.2.27, xdebug ✔, opcache ✔

\TyphoonReflectionBench

benchWarmWithMemoryCache # only name....I49 - Mo0.006ms (±10.36%) [1.975mb / 4.841mb]
benchWarmWithMemoryCache # everything...I49 - Mo0.018ms (±3.40%) [2.042mb / 4.841mb]
benchWarmWithFileCache # only name......I49 - Mo0.135ms (±2.14%) [2.031mb / 4.841mb]
benchWarmWithFileCache # everything.....I49 - Mo0.148ms (±2.74%) [2.033mb / 4.841mb]
benchCold # only name...................I199 - Mo6.987ms (±9.90%) [1.904mb / 4.840mb]
benchCold # everything..................I199 - Mo7.288ms (±10.82%) [1.971mb / 4.840mb]
benchColdIncludingInitializationAndAuto.I199 - Mo96.134ms (±8.41%) [1.900mb / 4.841mb]
benchColdIncludingInitializationAndAuto.I199 - Mo96.932ms (±11.23%) [1.967mb / 4.841mb]

Subjects: 4, Assertions: 0, Failures: 0, Errors: 0
```

With cache, it's the slowest of them all. But the difference is in __nanoseconds__.

### How does it work

Unfortunately, it's not as simple as just using the native reflection and parsing some
Expand Down
22 changes: 22 additions & 0 deletions benchmark/typhoon/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"scripts": {
"benchmark": [
"Composer\\Config::disableProcessTimeout",
"vendor/bin/phpbench run src"
]
},
"require-dev": {
"typhoon/reflection": "^0.4.4",
"typhoon/phpstorm-reflection-stubs": "^0.4.4",
"phpbench/phpbench": "^1.4",
"typhoon/opcache": "^0.2.1",
"symfony/cache": "^7.2",
"illuminate/support": "^12.0"
},
"autoload-dev": {
"psr-4": {
"Benchmark\\Typhoon\\": "src/",
"Tests\\Stubs\\": "tests/Stubs/"
}
}
}
6 changes: 6 additions & 0 deletions benchmark/typhoon/phpbench.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema":"./vendor/phpbench/phpbench/phpbench.schema.json",
"runner.bootstrap": "vendor/autoload.php",
"runner.time_unit": "ms",
"runner.progress_summary_variant_format": "label(\"Mo\") ~ display_as_time(mode(variant.time.avg), coalesce(subject.time_unit,\"time\"), subject.time_precision, subject.time_mode) ~ \n\" (\" ~ rstdev(variant.time.avg) ~ \")\" ~ \" [\" ~ mode(variant.mem.final) as memory ~ \" / \" ~ mode(variant.mem.peak) as memory ~ \"]\""
}
109 changes: 109 additions & 0 deletions benchmark/typhoon/src/TyphoonReflectionBench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

use Illuminate\Support\Str;
use PhpBench\Attributes\BeforeMethods;
use PhpBench\Attributes\Iterations;
use PhpBench\Attributes\ParamProviders;
use PhpBench\Attributes\Revs;
use PhpBench\Attributes\Warmup;
use Symfony\Component\Cache\Adapter\NullAdapter;
use Symfony\Component\Cache\Psr16Cache;
use Tests\Stubs\Classes\ClassStub;
use Typhoon\OPcache\TyphoonOPcache;
use Typhoon\Reflection\Cache\FreshCache;
use Typhoon\Reflection\ClassReflection;
use Typhoon\Reflection\TyphoonReflector;

class TyphoonReflectionBench
{
private TyphoonReflector $reflector;

public function setUpWithMemoryCache(): void
{
$this->reflector = TyphoonReflector::build();
}

public function setUpWithFileCache(): void
{
$freshOpcache = new FreshCache(new TyphoonOPcache(sys_get_temp_dir() . '/' . Str::random(32)));
$freshOpcache->clear();

$this->reflector = TyphoonReflector::build(cache: $freshOpcache);
}

public function setUpWithoutCache(): void
{
$this->reflector = TyphoonReflector::build(cache: new Psr16Cache(new NullAdapter()));
}

#[Iterations(50)]
#[Revs(200)]
#[Warmup(1)]
#[BeforeMethods('setUpWithMemoryCache')]
#[ParamProviders('hardnessProvider')]
public function benchWarmWithMemoryCache(array $params): void
{
$this->callMethods($params['hardness'], $this->reflector->reflectClass(ClassStub::class));
}

#[Iterations(50)]
#[Revs(200)]
#[Warmup(1)]
#[BeforeMethods('setUpWithFileCache')]
#[ParamProviders('hardnessProvider')]
public function benchWarmWithFileCache(array $params): void
{
$this->callMethods($params['hardness'], $this->reflector->reflectClass(ClassStub::class));
}

#[Iterations(200)]
#[Warmup(1)]
#[BeforeMethods('setUpWithoutCache')]
#[ParamProviders('hardnessProvider')]
public function benchCold(array $params): void
{
$this->callMethods($params['hardness'], $this->reflector->reflectClass(ClassStub::class));
}

#[Iterations(200)]
#[ParamProviders('hardnessProvider')]
public function benchColdIncludingInitializationAndAutoLoad(array $params): void
{
$this->setUpWithoutCache();

$this->callMethods($params['hardness'], $this->reflector->reflectClass(ClassStub::class));
}

public function hardnessProvider(): iterable
{
yield 'only name' => [
'hardness' => ['name' => true, 'everything' => false],
];

yield 'everything' => [
'hardness' => ['name' => true, 'everything' => true],
];
}

private function callMethods(array $hardness, ClassReflection $reflection): void
{
if ($hardness['name']) {
$reflection->file();
(string) $reflection->id;
}

if ($hardness['everything']) {
$reflection->attributes();
$reflection->templates();
$reflection->parentName();
// $reflection->implements();
// $reflection->uses();
$reflection->properties();
$reflection->methods();
// $reflection->isAnonymous();
$reflection->isAbstract();
$reflection->isFinal();
// $reflection->isBuiltIn();
}
}
}
31 changes: 17 additions & 14 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,34 @@
],
"require": {
"php": ">=8.2",
"tenantcloud/php-standard": "^2.0",
"symfony/var-exporter": "^7.0",
"phpstan/phpdoc-parser": "^1.26",
"illuminate/support": "^10.0 || ^11.0 || ^12.0",
"nikic/php-parser": "^5.0",
"phpstan/phpdoc-parser": "^2.1",
"psr/simple-cache": "^3.0",
"symfony/cache": "^7.0",
"webmozart/assert": "^1.11",
"nikic/php-parser": "^5.0"
"symfony/var-exporter": "^7.0",
"webmozart/assert": "^1.11"
},
"require-dev": {
"pestphp/pest": "^2.8",
"php-cs-fixer/shim": "~3.21.3",
"tenantcloud/php-cs-fixer-rule-sets": "~3.1.0",
"phpstan/phpstan": "~1.10.21",
"pestphp/pest": "^3.0",
"php-cs-fixer/shim": "~3.80.0",
"tenantcloud/php-cs-fixer-rule-sets": "~3.4.1",
"phpstan/phpstan": "~2.1.0",
"phake/phake": "^4.2",
"phpstan/phpstan-phpunit": "^1.3",
"phpstan/phpstan-webmozart-assert": "^1.2",
"phpstan/phpstan-mockery": "^1.1",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-webmozart-assert": "^2.0",
"phpstan/phpstan-mockery": "^2.0",
"phpbench/phpbench": "^1.2",
"roave/better-reflection": "^6.36",
"dms/phpunit-arraysubset-asserts": "^0.5.0",
"jiripudil/phpstan-sealed-classes": "^1.1"
},
"autoload": {
"psr-4": {
"GoodPhp\\Reflection\\": "src/"
}
},
"files": [
"src/Util/Lazy/functions.php"
]
},
"autoload-dev": {
"psr-4": {
Expand All @@ -55,6 +57,7 @@
"minimum-stability": "stable",
"prefer-stable": true,
"config": {
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true
}
Expand Down
9 changes: 9 additions & 0 deletions misc/opcache.ini
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
zend_extension=opcache.so
opcache.enable_cli=1
opcache.jit_buffer_size=64M
opcache.jit=1235
opcache.revalidate_freq=0
opcache.validate_timestamps=0
opcache.max_accelerated_files=10000
opcache.memory_consumption=192
opcache.max_wasted_percentage=10
opcache.interned_strings_buffer=16
xdebug.mode=off
19 changes: 16 additions & 3 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,35 @@ parameters:
paths:
- src

reportUnmatchedIgnoredErrors: false
ignoreErrors:
- '#Call-site variance of covariant object in generic type ReflectionClass<covariant object> in PHPDoc tag \@param for parameter \$reflection is redundant, template type T of object of class ReflectionClass has the same variance.#'
# PHPStan doesn't understand that Collection::values() produces a list
- '#Method .*::.* should return list<(.*)> but returns array<(int, )?\1>.#i'
- '#Parameter [\$|\#].* of .* expects list<(.*)>(|.*)?, (array|non-empty-array)<(int, )?\1> given.#i'
- '#Property .*::\$.* \(list<(.*)>\) does not accept (array|non-empty-array)<(int, )?\1>.#i'
- '#Parameter \#1 \$type of method GoodPhp\\Reflection\\NativePHPDoc\\Definition\\NativePHPDoc\\Native\\NativeTypeMapper::map\(\) expects list<ReflectionType\|string>\|ReflectionType\|string, array<ReflectionType> given.#i'
- '#Method GoodPhp\\Reflection\\NativePHPDoc\\Definition\\NativePHPDoc\\File\\ClassLikeContextParsingVisitor::methods\(\) should return list<string> but returns array<non-empty-string>.#i'
# Missing template types
- '#Method GoodPhp\\Reflection\\Type\\Template\\TypeParameterMap::toArguments\(\) should return list<GoodPhp\\Reflection\\Type\\Type> but returns array.#i'
# The return types are correct, but PHPStan complains even if types are specified explicitly.
- '#Method GoodPhp\\Reflection\\NativePHPDoc\\Reflection\\Npd.*Reflection::.* should return list<GoodPhp\\Reflection\\Reflection\\(.*)Reflection<ReflectableType of (object|UnitEnum), .*>> but returns array<int, GoodPhp\\Reflection\\Reflection\\\1Reflection<object, .*>>.#i'
# Intentionally left as nullable. If there was a solid way of checking if a property is initialized
# in PHP, then these would in fact be assigned nulls if definitions contain null types.
- '#Property GoodPhp\\Reflection\\NativePHPDoc\\Reflection\\Npd.*::.* \(GoodPhp\\Reflection\\Type\\(NamedType|Type)\|null\) is never assigned null so it can be removed from the property type.#i'
# It is in fact nullable. Probably a bug in PHPStan.
- '#Using nullsafe property access \"\?->excludedTraitMethods\" on left side of \?\? is unnecessary. Use -> instead.#i'
# It's not a native type. Bug in PHPStan.
- '#PHPDoc tag @var with type GoodPhp\\Reflection\\Reflection\\MethodReflection<ReflectableType of UnitEnum, GoodPhp\\Reflection\\Reflection\\Methods\\HasMethods<ReflectableType of UnitEnum>> is not subtype of native type GoodPhp\\Reflection\\Reflection\\MethodReflection<object, GoodPhp\\Reflection\\Reflection\\Methods\\HasMethods<object>>#i'
# Bug with inherited readonly properties
- '#Readonly property [a-z\\]+::\$[a-z0-9]+ is assigned outside of the constructor.#i'
- '#Class [a-z\\]+ has an uninitialized readonly property \$[a-z0-9]+\. Assign it in the constructor.#i'
# Wrongly inferred types
- '#Property GoodPhp\\Reflection\\Type\\Combinatorial\\(IntersectionType|UnionType)::\$types \(Illuminate\\Support\\Collection<int, GoodPhp\\Reflection\\Type\\Type>\) does not accept Illuminate\\Support\\Collection<\(int\|string\), mixed>.#i'
# We expect proper error handling, in which case file_get_contents() will never return `false`
-
message: '#Parameter \#1 \$code of method PhpParser\\Parser::parse\(\) expects string, string\|false given\.#i'
path: 'src/NativePHPDoc/Definition/NativePHPDoc/File/FileContextParser.php'
-
message: '#Parameter \#2 \$aliases of class GoodPhp\\Reflection\\NativePHPDoc\\Definition\\NativePHPDoc\\File\\FileClassLikeContext\\TraitUse constructor expects Illuminate\\Support\\Collection<int, array{string, string\|null, [\d\|]+null}>, Illuminate\\Support\\Collection<int, array{string, string\|null, int\|null}> given\.#i'
message: '#Parameter \#2 \$aliases of class GoodPhp\\Reflection\\NativePHPDoc\\Definition\\NativePHPDoc\\File\\FileClassLikeContext\\TraitUse constructor expects list<array{string, string\|null, [\d\|]+null}>, list<array{non-empty-string, non-falsy-string\|null, int<min, -1>\|int<1, max>\|null}> given\.#i'
path: 'src/NativePHPDoc/Definition/NativePHPDoc/File/ClassLikeContextParsingVisitor.php'
# This, again, I'm not sure how to approach
-
Expand Down
Loading