-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[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 ? |
| // 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
LazyChoiceListstoring theDoctrineChoiceLoaderand$value = null LazyChoiceListcalls the loader and passesnullas value.DoctrineChoiceLoaderresolves$valuewith itsidReaderand load the choicesDoctrineChoiceLoadercalls the decorated factory with resolved value and loaded choices- Decorated factory creates and stores the
ArrayChoiceListwill never call it back. DoctrineChoiceLoaderstores theArrayChoiceListand will never call it back.LazyChoiceListstores theArrayChoiceListwhile 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 theEntityTypethe same loader is return, so the factory returns the sameLazyChoiceListstoring the loadedArrayChoiceList`.
- Workflow a bit improved by this PR
Removes line 5 from above.
3. Ideal Workflow
- IdReader decorated factory resolves
$value,$idReaderand$entityLoader(instead of DoctrineType) then creates aLazyEntityListstoring theDoctrineChoiceLoaderand the resolved arguments, LazyEntityListcalls the loader and passes it the resolved arguments.DoctrineChoiceLoaderload the choices with optimisationDoctrineChoiceLoadercreates anEntityListwith an extra map ids by valuesDoctrineChoiceLoaderstores 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)LazyChoiceListwith loader and resolved$valueLazyChoiceList::get*DoctrineChoiceLoaderwith resolved$valueDoctrineChoiceLoader::loadChoiceList()DefaultChoiceListFactorywith loaded choices and resolved$valueDefaultChoiceListFactory::createListFromChoices()ArrayChoiceListwithresolved$value`With this refactoring, the
DoctrineChoiceLoaderis no longer dependant to the factory, theChoiceLoaderInterface::loadChoiceList()must return aChoiceListInterfacebut should not need a decorated factory while$valueis already resolved. It should remain lazy IMHO.Solution
DefaultChoiceListFactory::createListFromLoader()LazyChoiceListwith loader and resolved$valueLazyChoiceList::get*()DoctrineChoiceLoaderwith resolved$valueDoctrineChoiceLoader::loadChoiceList()ArrayChoiceListwith resolved$value.Since
choiceListFactoryis 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
EntityTypefields 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: