-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Form] [DoctrineBridge] optimized LazyChoiceList and DoctrineChoiceLoader #18359
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
[Form] [DoctrineBridge] optimized LazyChoiceList and DoctrineChoiceLoader #18359
Conversation
if (null !== $this->factory) { | ||
@trigger_error(sprintf('Passing a ChoiceListFactoryInterface to %s is deprecated since version 3.1 and will no longer be supported in 4.0. You should either call "%s::loadChoiceList" or override it to return a ChoiceListInterface.', __CLASS__, __CLASS__)); | ||
$this->factory = $factory; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This deprecation means that in the future versions it will always have to be called with null as first argument. It's a pain to move the argument to the end of the list but I think that's the only solution you can do for 4.0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The proper deprecation way would be to allow moving all args to the left, not to allow passing null as same argument (i.e. allowing using the right Symfony 4 API)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok will do that, thanks guys for the review :)
d082b38
to
6e3fd0b
Compare
Comments addressed. |
Actually the more relevant of all the profile is cached app and form types loading profile 3: |
It should also be the most used (just on display) with a warmup cache |
This might be a first step to do better but can be improved even more IMHO, so let's do this for 3.2. Status: Needs Work |
* @param null|EntityLoaderInterface $objectLoader The objects loader | ||
*/ | ||
public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, IdReader $idReader = null, EntityLoaderInterface $objectLoader = null) | ||
public function __construct($manager, $class, $idReader = null, $objectLoader = null, $factory = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a BC break. We cannot reorder the arguments of the constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I just realized what you are doing below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should it be done for 3.1 ? |
@@ -146,7 +155,7 @@ public function loadChoicesForValues(array $values, $value = null) | |||
|
|||
// Optimize performance in case we have an object loader and | |||
// a single-field identifier | |||
$optimize = null === $value || is_array($value) && $value[0] === $this->idReader; | |||
$optimize = is_array($value) && $this->idReader === $value[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you remove the null === $value
check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because I read it as null === $value && is_array($value)
😞
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if I was the one "fixing" it in the first place and introducing a regression at the same type...
However it's clear to me now that choice_value
in entity type should better not be used on big collections to keep this optimisation efficient.
What I think of now is that actually $value
is always resolved by factories when stored in the cached LazyChoiceList
which passes it to the loader each time.
What about creating another ChoiceListInterface
depending on this logic while moving it in a Symfony\Bridge\Doctrine\Form\ChoiceList\Factory\SingleIdFactoryDecorator
?
Do you think it's doable to cache a map id => value
when $value
is not null somewhere ?
What about possibilities added by the new Cache component ?
ids should be immutable, right ? But their corresponding values may need to be updated but must not change during the lifecycle of a form since we rely on it to select choices.
How to generate a good hash based on the class and $value
in that case ? that should not be a problem if $value
is a string property path but if it is a closure ?
One way I see this could be implemented is by adding a Symfony\Bridge\Doctrine\Form\ChoiceList\LazyEntityList
which would be basically a LazyChoiceList
holding the idReader
for mapping and using $value
as well.
1. Current WorkFlow
- Decorated factory creates and stores the
LazyChoiceList
storing theDoctrineChoiceLoader
and$value = null
LazyChoiceList
calls the loader and passesnull
as value.DoctrineChoiceLoader
resolves$value
with itsidReader
and load the choicesDoctrineChoiceLoader
calls the decorated factory with resolved value and loaded choices- Decorated factory creates and stores the
ArrayChoiceList
will never call it back. DoctrineChoiceLoader
stores theArrayChoiceList
and will never call it back.LazyChoiceList
stores theArrayChoiceList
while storing the loader.
Also, if another EntityType
is used in the same form, another DoctrineChoiceLoader
is created by the DoctrineType
and the same process happens again.
When the same em
, class
and 'query_builderoptions are used for the
EntityTypethe same loader is return, so the factory returns the same
LazyChoiceListstoring the loaded
ArrayChoiceList`.
- Workflow a bit improved by this PR
Removes line 5 from above.
3. Ideal Workflow
- IdReader decorated factory resolves
$value
,$idReader
and$entityLoader
(instead of DoctrineType) then creates aLazyEntityList
storing theDoctrineChoiceLoader
and the resolved arguments, LazyEntityList
calls the loader and passes it the resolved arguments.DoctrineChoiceLoader
load the choices with optimisationDoctrineChoiceLoader
creates anEntityList
with an extra map ids by valuesDoctrineChoiceLoader
stores theEntityList
.
What do you think ? Thanks.
78124ff
to
e690da1
Compare
Comments addressed. Status: Needs Review |
e690da1
to
1c553eb
Compare
Should I deprecate the caching of |
@javiereguiluz I guess this one could have a "Enhancement" label :) |
1c553eb
to
5c9d90a
Compare
5c9d90a
to
98621f4
Compare
I pushed the So with both optimizations from current to 4.0 (without BC layer): Finally profiles with deprecation layer (the final look after that PR):
And that will be even better in 4.0! Don't forget to look at the real values, they are more impacted than the percentages :) Note that actually the code as it works by default has not triggered any deprecations for my tests, it should only happen for custom implementations and that's good. |
Failures are unrelated. |
@HeahDude nice comparisons 👏 And thanks for using realistic benchmarks (it's "easy" to improve performance when doing a benchmark with 1 million entities ... but that's no real) |
Thanks @javiereguiluz, still I guess it would be nice if someone could test it in a real app too to confirm it does not mess with anything... |
What's the status of this PR? is this still to be in 3.1? Allied to #18332 we can get a great performance improvement. I tested it with Anyone for a final review? |
Thank you @HeahDude. |
…octrineChoiceLoader (HeahDude) This PR was merged into the 3.1-dev branch. Discussion ---------- [Form] [DoctrineBridge] optimized LazyChoiceList and DoctrineChoiceLoader | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | no | BC breaks? | no | Deprecations? | yes | Tests pass? | yes | Fixed tickets | ~ | License | MIT | Doc PR | ~ Problem ====== Actually we got a circular dependency: | Object | Return |----------|-------------| |`DefaultChoiceListFactory::createListFromLoader()` (decorated) | `LazyChoiceList` with loader and resolved `$value` | `LazyChoiceList::get*` | `DoctrineChoiceLoader` with resolved `$value` `DoctrineChoiceLoader::loadChoiceList()` | (decorated) `DefaultChoiceListFactory` with loaded choices and resolved `$value` `DefaultChoiceListFactory::createListFromChoices()` | `ArrayChoiceList` with `resolved `$value` With this refactoring, the `DoctrineChoiceLoader` is no longer dependant to the factory, the `ChoiceLoaderInterface::loadChoiceList()` must return a `ChoiceListInterface` but should not need a decorated factory while `$value` is already resolved. It should remain lazy IMHO. Solution ====== | Object | Return | |----------|-----------| | `DefaultChoiceListFactory::createListFromLoader()` | `LazyChoiceList` with loader and resolved `$value` | `LazyChoiceList::get*()` | `DoctrineChoiceLoader` with resolved `$value` | `DoctrineChoiceLoader::loadChoiceList()` | `ArrayChoiceList` with resolved `$value`. Since `choiceListFactory` is a private property, this change should be safe regarding BC. To justify this change, I've made some blackfire profiling. You can see my [branch of SE here](https://github.com/HeahDude/symfony-standard/tree/test/optimize-doctrine_choice_loader) and the [all in file test implementation](https://github.com/HeahDude/symfony-standard/blob/test/optimize-doctrine_choice_loader/src/AppBundle/Controller/DefaultController.php). Basically it loads a form with 3 `EntityType` fields with different classes holding 50 instances each. (INIT events are profiled with an empty cache) When | What | Diff (SE => PR) --------|-------|------ INIT (1) | build form (load types) | [see](https://blackfire.io/profiles/compare/061d5d28-15c6-4e01-b8c0-3edc9cb8daf0/graph) INIT (2) | build view (load choices) | [see](https://blackfire.io/profiles/compare/04f142a8-d886-405a-be4d-636ba82d8acd/graph) CACHED | build form (load types) | [see](https://blackfire.io/profiles/compare/293b27b6-aa58-42ae-bafb-655513201505/graph) CACHED | build view (load choices) | [see](https://blackfire.io/profiles/compare/e5b37dfe-cc9e-498f-b98a-7448830ad190/graph) SUBMIT | build form (load types) | [see](https://blackfire.io/profiles/compare/7f3baea9-0d27-46b6-8c24-c577742382dc/graph) SUBMIT | handle request (load choices) | [see](https://blackfire.io/profiles/compare/8644ebfb-4397-495b-8f3d-1a6e1d7f8476/graph) SUBMIT | build view (load values) | [see](https://blackfire.io/profiles/compare/89c3cc7c-ea07-4300-91b3-99004cb58ea1/graph) (1):  (2):  It can seem like 1 and 2 balance each other but it comes clear when comparing values: | - | Build form | Build view | |-----|---------|--------------| | wall time | -88 ms | +9.71 ms | blocking I/O | -40 ms | +3.67 ms | cpu | -48 ms | +13.4 ms | memory | -4.03 MB | +236 kB | network | -203 B | +2.21 kB Commits ------- 98621f4 [Form] optimized LazyChoiceList 86b2ff1 [DoctrineBridge] optimized DoctrineChoiceLoader
…(HeahDude) This PR was merged into the 3.1-dev branch. Discussion ---------- [Form] [DoctrineBridge] updated changelogs after #18359 | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | ~ | License | MIT | Doc PR | ~ follows #18359. Commits ------- 1c8dc9d [Form] [DoctrineBridge] updated changelogs after #18359
This PR was merged into the 3.1 branch. Discussion ---------- [DoctrineBridge] Remove dead code in DoctrineType | Q | A | ------------- | --- | Branch? | 3.1 | Bug fix? | no | New feature? | no | BC breaks? | no (Only the last arguments of a method may be removed [[3]](http://symfony.com/doc/current/contributing/code/bc.html#id13) ) | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - After optimization of DoctrineChoiceLoader (86b2ff1 in #18359) this code died :) Commits ------- 9b49723 remove dead code
This PR was merged into the 3.1 branch. Discussion ---------- [DoctrineBridge] Remove dead code in DoctrineType | Q | A | ------------- | --- | Branch? | 3.1 | Bug fix? | no | New feature? | no | BC breaks? | no (Only the last arguments of a method may be removed [[3]](http://symfony.com/doc/current/contributing/code/bc.html#id13) ) | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - After optimization of DoctrineChoiceLoader (symfony/symfony@86b2ff1 in symfony/symfony#18359) this code died :) Commits ------- 9b49723 remove dead code
…n (yceruto) This PR was submitted for the master branch but it was merged into the 3.2 branch instead (closes #21493). Discussion ---------- [DoctrineBridge] Remove dead code in DoctrineOrmExtension | Q | A | ------------- | --- | Branch? | 3.2 | Bug fix? | no | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | License | MIT After optimization in #20312 and #18359 Commits ------- 0cd8bf8 Remove dead code
… (HeahDude) This PR was squashed before being merged into the 4.4 branch. Discussion ---------- [Form] Fix same choice loader with different choice values | Q | A | ------------- | --- | Branch? | 4.4 | Bug fix? | yes | New feature? | no <!-- please update src/**/CHANGELOG.md files --> | Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Tickets | #44655 | License | MIT | Doc PR | ~ It appears that deprecating the caching in the `LazyChoiceList` (cf #18359) was a mistake. The bug went under the radar because in practice every choice field has its own loader instance. However, optimizations made in #30994 then revealed the flaw (cf #42206) as the loaders were actually shared across many fields. While working on a fix I ended up implementing something similar to what's proposed in #44655. I'll send a PR for 5.4 as well. Commits ------- 65cbf18 [Form] Fix same choice loader with different choice values
Problem
Actually we got a circular dependency:
DefaultChoiceListFactory::createListFromLoader()
(decorated)LazyChoiceList
with loader and resolved$value
LazyChoiceList::get*
DoctrineChoiceLoader
with resolved$value
DoctrineChoiceLoader::loadChoiceList()
DefaultChoiceListFactory
with loaded choices and resolved$value
DefaultChoiceListFactory::createListFromChoices()
ArrayChoiceList
withresolved
$value`With this refactoring, the
DoctrineChoiceLoader
is no longer dependant to the factory, theChoiceLoaderInterface::loadChoiceList()
must return aChoiceListInterface
but should not need a decorated factory while$value
is already resolved. It should remain lazy IMHO.Solution
DefaultChoiceListFactory::createListFromLoader()
LazyChoiceList
with loader and resolved$value
LazyChoiceList::get*()
DoctrineChoiceLoader
with resolved$value
DoctrineChoiceLoader::loadChoiceList()
ArrayChoiceList
with resolved$value
.Since
choiceListFactory
is a private property, this change should be safe regarding BC.To justify this change, I've made some blackfire profiling.
You can see my branch of SE here and the all in file test implementation.
Basically it loads a form with 3
EntityType
fields with different classes holding 50 instances each.(INIT events are profiled with an empty cache)
(1):


(2):
It can seem like 1 and 2 balance each other but it comes clear when comparing values: