Skip to content

Generate class transformers ahead of time - #218

Open
DavidBadura wants to merge 1 commit into
3.0.xfrom
generated-transformer-3
Open

DavidBadura wants to merge 1 commit into
3.0.xfrom
generated-transformer-3

Conversation

@DavidBadura

Copy link
Copy Markdown
Member

Brings the code generation from #150 / #214 to 3.0, on top of the transformer factories from #217.

GeneratedTransformerExtension registers a ClassTransformerFactory which loads a generated transformer per class instead of using reflection. Code is never generated at runtime: the factory only requires files that exist and otherwise returns null, so the class falls back to reflection. The code is written beforehand by the GeneratedTransformerWarmer, for example on deploy:

$hydrator = (new StackHydratorBuilder())
    ->useExtension(new GeneratedTransformerExtension($cachePath))
    ->build();

(new GeneratedTransformerWarmer($hydrator, $cachePath))->warmup([ProfileCreated::class]);

A new PHPat rule (NoCodeGenerationAtRuntimeTest) makes sure the runtime classes (extension, factory, TransformerFiles, ClassFingerprint) never depend on the generator, so this can't creep back in.

Compared to #214:

  • The file name contains the fingerprints of the class and its nested classes. A changed class has no matching file and is transformed with reflection until the next warmup, nothing throws.
  • There is no root hydrator anymore. Inlining nested objects is checked per call against $context[Hydrator::HYDRATOR], so a hydrator which wraps the StackHydrator still sees nested objects.
  • The generated code decides about inlining itself from the hydrator's middlewares and default lazy flag. For that ClassTransformerFactory::create() now also gets the StackHydrator, which got the getters middlewares() and defaultLazy() and implements MetadataFactory. The warmer takes it as metadata factory, so there is no extra builder method or adapter.
  • The call stack for circular references lives in the context now (one per top level call), shared by reflection and generated transformers.
  • Since HydratorAwareNormalizer is gone, the generated code treats every normalizer as possibly calling the hydrator again, except the built-in enum and date normalizers. That only decides whether circular references are tracked.

Benchmarks on this branch (PHP 8.5, opcache, xdebug off), reflection vs generated:

Subject reflection generated
hydrate 1000 objects 1.791ms 0.900ms
extract 1000 objects 1.757ms 0.733ms
hydrate 1000 objects, cryptography 2.630ms 1.656ms
extract 1000 objects, cryptography 4.785ms 3.728ms

The generated benchmarks run the warmup once per class via BeforeClassMethods instead of in every iteration.

The GeneratedTransformerExtension registers a transformer factory which loads
generated code instead of using reflection. Code is never generated at
runtime: the factory only requires files the GeneratedTransformerWarmer wrote
beforehand and falls back to reflection if there is none. An architecture test
makes sure the runtime classes do not depend on the generation.

The file name contains the fingerprints of the class and its nested classes,
so changed classes simply fall back to reflection until the next warmup.
Nested objects are inlined if no middleware runs for them, unless the
hydrator was wrapped by another one.

The StackHydrator now implements MetadataFactory and exposes its middlewares
and the default lazy flag, which is all a transformer factory needs. The call
stack for circular references moved into the context.
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Hello 👋

here is the most recent benchmark result:

GeneratedHydratorBench
======================

+----------------------------+------------------+-----------------+
|                            | time (kde mode)  | memory          |
+----------------------------+------------------+-----------------+
| subject                    | Tag: <current>   | Tag: <current>  |
+----------------------------+------------------+-----------------+
| benchHydrate1Object ()     | 2.527μs (±0.00%) | 2.937mb         |
| benchExtract1Object ()     | 3.436μs (±0.00%) | 3.066mb         |
| benchHydrate1000Objects () | 1.361ms (±0.00%) | 2.991mb         |
| benchExtract1000Objects () | 1.261ms (±0.00%) | 2.973mb         |
+----------------------------+------------------+-----------------+

StackHydratorBench
==================

+-------------------------------+-------------------------------------+------------------------------+
|                               | time (kde mode)                     | memory                       |
+-------------------------------+------------------+------------------+-----------------+------------+
| subject                       | Tag: <current>   | Tag: base        | Tag: <current>  | Tag: base  |
+-------------------------------+------------------+------------------+-----------------+------------+
| benchHydrate1Object ()        | 4.000μs (±0.00%) | 3.200μs (±0.00%) | 2.772mb         | 3.272mb    |
| benchExtract1Object ()        | 4.400μs (±0.00%) | 4.000μs (±0.00%) | 2.772mb         | 3.272mb    |
| benchHydrate1000Objects ()    | 2.647ms (±0.00%) | 2.697ms (±0.00%) | 3.035mb         | 3.516mb    |
| benchExtract1000Objects ()    | 2.906ms (±0.00%) | 2.734ms (±0.00%) | 3.045mb         | 3.512mb    |
| benchHydrate1000000Objects () | 1.408s (±0.00%)  | 1.385s (±0.00%)  | 3.035mb         | 3.516mb    |
| benchExtract1000000Objects () | 1.693s (±0.00%)  | 1.474s (±0.00%)  | 3.046mb         | 3.032mb    |
+-------------------------------+------------------+------------------+-----------------+------------+

HydratorWithLazyBench
=====================

+------------------------------------------+-----------------------------------------+------------------------------+
|                                          | time (kde mode)                         | memory                       |
+------------------------------------------+--------------------+--------------------+-----------------+------------+
| subject                                  | Tag: <current>     | Tag: base          | Tag: <current>  | Tag: base  |
+------------------------------------------+--------------------+--------------------+-----------------+------------+
| benchHydrate1Object ()                   | 1.000μs (±0.00%)   | 1.000μs (±0.00%)   | 2.772mb         | 2.772mb    |
| benchHydrate1ObjectTriggerInit ()        | 4.800μs (±0.00%)   | 4.400μs (±0.00%)   | 2.772mb         | 2.772mb    |
| benchHydrate1000Objects ()               | 524.667μs (±0.00%) | 597.333μs (±0.00%) | 2.831mb         | 2.772mb    |
| benchHydrate1000ObjectsTriggerInit ()    | 3.361ms (±0.00%)   | 3.605ms (±0.00%)   | 3.020mb         | 3.020mb    |
| benchHydrate1000000Objects ()            | 360.782ms (±0.00%) | 373.512ms (±0.00%) | 2.831mb         | 2.772mb    |
| benchHydrate1000000ObjectsTriggerInit () | 2.237s (±0.00%)    | 2.265s (±0.00%)    | 3.020mb         | 3.020mb    |
+------------------------------------------+--------------------+--------------------+-----------------+------------+

StackHydratorWithCryptographyBench
==================================

+-------------------------------+---------------------------------------+------------------------------+
|                               | time (kde mode)                       | memory                       |
+-------------------------------+-------------------+-------------------+-----------------+------------+
| subject                       | Tag: <current>    | Tag: base         | Tag: <current>  | Tag: base  |
+-------------------------------+-------------------+-------------------+-----------------+------------+
| benchHydrate1Object ()        | 8.000μs (±0.00%)  | 7.800μs (±0.00%)  | 2.821mb         | 2.822mb    |
| benchExtract1Object ()        | 18.000μs (±0.00%) | 16.400μs (±0.00%) | 2.821mb         | 2.822mb    |
| benchHydrate1000Objects ()    | 4.383ms (±0.00%)  | 4.316ms (±0.00%)  | 3.088mb         | 2.991mb    |
| benchExtract1000Objects ()    | 10.298ms (±0.00%) | 9.608ms (±0.00%)  | 3.202mb         | 3.202mb    |
| benchHydrate1000000Objects () | 2.363s (±0.00%)   | 2.278s (±0.00%)   | 3.088mb         | 2.991mb    |
| benchExtract1000000Objects () | 7.102s (±0.00%)   | 6.866s (±0.00%)   | 3.202mb         | 3.202mb    |
+-------------------------------+-------------------+-------------------+-----------------+------------+

HydratorBench
=============

+-------------------------------+-------------------------------------+------------------------------+
|                               | time (kde mode)                     | memory                       |
+-------------------------------+------------------+------------------+-----------------+------------+
| subject                       | Tag: <current>   | Tag: base        | Tag: <current>  | Tag: base  |
+-------------------------------+------------------+------------------+-----------------+------------+
| benchHydrate1Object ()        | 4.000μs (±0.00%) | 3.600μs (±0.00%) | 2.772mb         | 2.772mb    |
| benchExtract1Object ()        | 4.600μs (±0.00%) | 4.000μs (±0.00%) | 2.772mb         | 2.772mb    |
| benchHydrate1000Objects ()    | 2.615ms (±0.00%) | 2.737ms (±0.00%) | 3.035mb         | 3.035mb    |
| benchExtract1000Objects ()    | 2.976ms (±0.00%) | 2.668ms (±0.00%) | 3.045mb         | 3.032mb    |
| benchHydrate1000000Objects () | 1.410s (±0.00%)  | 1.383s (±0.00%)  | 3.035mb         | 3.035mb    |
| benchExtract1000000Objects () | 1.690s (±0.00%)  | 1.468s (±0.00%)  | 3.046mb         | 3.032mb    |
+-------------------------------+------------------+------------------+-----------------+------------+

GeneratedHydratorWithCryptographyBench
======================================

+----------------------------+-------------------+-----------------+
|                            | time (kde mode)   | memory          |
+----------------------------+-------------------+-----------------+
| subject                    | Tag: <current>    | Tag: <current>  |
+----------------------------+-------------------+-----------------+
| benchHydrate1Object ()     | 6.022μs (±0.00%)  | 3.104mb         |
| benchExtract1Object ()     | 13.439μs (±0.00%) | 3.321mb         |
| benchHydrate1000Objects () | 2.919ms (±0.00%)  | 3.251mb         |
| benchExtract1000Objects () | 7.695ms (±0.00%)  | 3.321mb         |
+----------------------------+-------------------+-----------------+

HydratorWithCryptographyBench
=============================

+-------------------------------+---------------------------------------+------------------------------+
|                               | time (kde mode)                       | memory                       |
+-------------------------------+-------------------+-------------------+-----------------+------------+
| subject                       | Tag: <current>    | Tag: base         | Tag: <current>  | Tag: base  |
+-------------------------------+-------------------+-------------------+-----------------+------------+
| benchHydrate1Object ()        | 7.400μs (±0.00%)  | 8.000μs (±0.00%)  | 2.821mb         | 2.822mb    |
| benchExtract1Object ()        | 17.800μs (±0.00%) | 16.800μs (±0.00%) | 2.821mb         | 2.822mb    |
| benchHydrate1000Objects ()    | 4.429ms (±0.00%)  | 4.307ms (±0.00%)  | 3.088mb         | 2.991mb    |
| benchExtract1000Objects ()    | 10.646ms (±0.00%) | 9.617ms (±0.00%)  | 3.202mb         | 3.202mb    |
| benchHydrate1000000Objects () | 2.317s (±0.00%)   | 2.341s (±0.00%)   | 3.088mb         | 2.991mb    |
| benchExtract1000000Objects () | 7.277s (±0.00%)   | 6.871s (±0.00%)   | 3.202mb         | 3.202mb    |
+-------------------------------+-------------------+-------------------+-----------------+------------+

This comment gets update everytime a new commit comes in!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant