Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[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

Merged
merged 2 commits into from
Apr 28, 2016

Conversation

HeahDude
Copy link
Contributor

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 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)

When What Diff (SE => PR)
INIT (1) build form (load types) see
INIT (2) build view (load choices) see
CACHED build form (load types) see
CACHED build view (load choices) see
SUBMIT build form (load types) see
SUBMIT handle request (load choices) see
SUBMIT build view (load values) see

(1):
build_form-no_cache
(2):
build_view-no_cache

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

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;
}
Copy link
Contributor

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

Copy link
Member

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)

Copy link
Contributor Author

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 :)

@HeahDude
Copy link
Contributor Author

Comments addressed.

@HeahDude
Copy link
Contributor Author

Actually the more relevant of all the profile is cached app and form types loading profile 3:

load_types-cached

@HeahDude
Copy link
Contributor Author

It should also be the most used (just on display) with a warmup cache

@HeahDude
Copy link
Contributor Author

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)
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I'm just feel bad about removing the type hints but @iltar and @stof were suggesting that change for good reasons I guess.

@HeahDude
Copy link
Contributor Author

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];
Copy link
Contributor

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?

Copy link
Contributor Author

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) 😞

Copy link
Contributor Author

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

  1. Decorated factory creates and stores the LazyChoiceList storing the DoctrineChoiceLoader and $value = null
  2. LazyChoiceList calls the loader and passes null as value.
  3. DoctrineChoiceLoader resolves $value with its idReader and load the choices
  4. DoctrineChoiceLoader calls the decorated factory with resolved value and loaded choices
  5. Decorated factory creates and stores the ArrayChoiceList will never call it back.
  6. DoctrineChoiceLoader stores the ArrayChoiceList and will never call it back.
  7. LazyChoiceList stores the ArrayChoiceList 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 theEntityTypethe same loader is return, so the factory returns the sameLazyChoiceListstoring the loadedArrayChoiceList`.

  1. Workflow a bit improved by this PR

Removes line 5 from above.

3. Ideal Workflow

  1. IdReader decorated factory resolves $value, $idReader and $entityLoader (instead of DoctrineType) then creates a LazyEntityList storing the DoctrineChoiceLoader and the resolved arguments,
  2. LazyEntityList calls the loader and passes it the resolved arguments.
  3. DoctrineChoiceLoader load the choices with optimisation
  4. DoctrineChoiceLoader creates an EntityList with an extra map ids by values
  5. DoctrineChoiceLoader stores the EntityList.

What do you think ? Thanks.

@HeahDude HeahDude force-pushed the minor-optimize-doctrine_choice_loader branch 2 times, most recently from 78124ff to e690da1 Compare March 31, 2016 07:18
@HeahDude
Copy link
Contributor Author

Comments addressed.

Status: Needs Review

@HeahDude HeahDude force-pushed the minor-optimize-doctrine_choice_loader branch from e690da1 to 1c553eb Compare March 31, 2016 08:55
@HeahDude
Copy link
Contributor Author

Should I deprecate the caching of LazyChoiceList in that PR too ? (ref #18332 (comment))

@HeahDude
Copy link
Contributor Author

@javiereguiluz I guess this one could have a "Enhancement" label :)

@HeahDude HeahDude changed the title [RFC] [DoctrineBridge] Optimize DoctrineChoiceLoader [Form] [DoctrineBridge] Optimize LazyChoiceList and DoctrineChoiceLoader Apr 2, 2016
@HeahDude HeahDude changed the title [Form] [DoctrineBridge] Optimize LazyChoiceList and DoctrineChoiceLoader [Form] [DoctrineBridge] optimized LazyChoiceList and DoctrineChoiceLoader Apr 2, 2016
@HeahDude HeahDude force-pushed the minor-optimize-doctrine_choice_loader branch from 1c553eb to 5c9d90a Compare April 3, 2016 17:05
@HeahDude HeahDude force-pushed the minor-optimize-doctrine_choice_loader branch from 5c9d90a to 98621f4 Compare April 3, 2016 17:07
@HeahDude
Copy link
Contributor Author

HeahDude commented Apr 3, 2016

I pushed the LazyChoiceList optimization, with both it's even better, here some profiles from previous optimization of the loader to both loader and LazyChoiceList with this new [commit](:

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.

@HeahDude
Copy link
Contributor Author

HeahDude commented Apr 3, 2016

Failures are unrelated.

@javiereguiluz
Copy link
Member

@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)

@HeahDude
Copy link
Contributor Author

HeahDude commented Apr 3, 2016

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...

@HeahDude
Copy link
Contributor Author

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 symfony-demo by quickly adding some EntityType fields in some forms, and it worked like a charm :)

Anyone for a final review?

@fabpot
Copy link
Member

fabpot commented Apr 28, 2016

Thank you @HeahDude.

@fabpot fabpot merged commit 98621f4 into symfony:master Apr 28, 2016
fabpot added a commit that referenced this pull request Apr 28, 2016
…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):
![build_form-no_cache](https://cloud.githubusercontent.com/assets/10107633/14136166/b5a85eb8-f661-11e5-8556-3e0dcbfaf404.jpg)
(2):
![build_view-no_cache](https://cloud.githubusercontent.com/assets/10107633/14136240/1162f3ee-f662-11e5-834a-1ed1e519dc83.jpg)

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 added a commit to HeahDude/symfony that referenced this pull request Apr 28, 2016
HeahDude added a commit to HeahDude/symfony that referenced this pull request Apr 28, 2016
HeahDude added a commit to HeahDude/symfony that referenced this pull request Apr 28, 2016
@HeahDude HeahDude deleted the minor-optimize-doctrine_choice_loader branch April 28, 2016 17:16
HeahDude added a commit to HeahDude/symfony that referenced this pull request Apr 29, 2016
fabpot added a commit that referenced this pull request Apr 29, 2016
…(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
@fabpot fabpot mentioned this pull request May 13, 2016
fabpot added a commit that referenced this pull request Oct 28, 2016
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
symfony-splitter pushed a commit to symfony/doctrine-bridge that referenced this pull request Oct 28, 2016
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
fabpot added a commit that referenced this pull request Feb 1, 2017
…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
xabbuh added a commit that referenced this pull request May 20, 2022
… (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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants