Skip to content

[Form] Improved performance of ChoiceType and EntityType #4881

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 3 commits into from
Jul 12, 2012
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
33 changes: 28 additions & 5 deletions src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ abstract class DoctrineType extends AbstractType
*/
protected $registry;

/**
* @var array
*/
private $choiceListCache = array();

public function __construct(ManagerRegistry $registry)
{
$this->registry = $registry;
Expand All @@ -46,6 +51,7 @@ public function buildForm(FormBuilderInterface $builder, array $options)

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$choiceListCache =& $this->choiceListCache;
$registry = $this->registry;
$type = $this;

Expand All @@ -59,17 +65,34 @@ public function setDefaultOptions(OptionsResolverInterface $resolver)
return null;
};

$choiceList = function (Options $options) use ($registry) {
$choiceList = function (Options $options) use ($registry, &$choiceListCache, &$time) {
Copy link
Member

Choose a reason for hiding this comment

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

what is $time here ? it seems undefined and unused

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed already. I was faster than you hehehe ;)

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's in the next PR.

$manager = $registry->getManager($options['em']);

return new EntityChoiceList(
$manager,
$choiceHashes = is_array($options['choices'])
? array_map('spl_object_hash', $options['choices'])
Copy link
Contributor

Choose a reason for hiding this comment

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

Getting Warning: spl_object_hash() expects parameter 1 to be object, array given here.

: $options['choices'];

$hash = md5(json_encode(array(
spl_object_hash($manager),
$options['class'],
$options['property'],
$options['loader'],
$options['choices'],
$choiceHashes,
$options['group_by']
);
)));

if (!isset($choiceListCache[$hash])) {
$choiceListCache[$hash] = new EntityChoiceList(
$manager,
$options['class'],
$options['property'],
$options['loader'],
$options['choices'],
$options['group_by']
);
}

return $choiceListCache[$hash];
};

$resolver->setDefaults(array(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bridge\Doctrine\Tests\Form\Type;

use Symfony\Component\Form\Tests\FormPerformanceTestCase;
use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIdentEntity;
use Doctrine\ORM\Tools\SchemaTool;
use Symfony\Bridge\Doctrine\Tests\DoctrineOrmTestCase;
use Symfony\Component\Form\Extension\Core\CoreExtension;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension;

/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class EntityTypePerformanceTest extends FormPerformanceTestCase
{
const ENTITY_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIdentEntity';

/**
* @var \Doctrine\ORM\EntityManager
*/
private $em;

protected function getExtensions()
{
$manager = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry');
$manager->expects($this->any())
->method('getManager')
->will($this->returnValue($this->em));

return array(
new CoreExtension(),
new DoctrineOrmExtension($manager)
);
}

protected function setUp()
{
if (!class_exists('Symfony\Component\Form\Form')) {
$this->markTestSkipped('The "Form" component is not available');
}

if (!class_exists('Doctrine\DBAL\Platforms\MySqlPlatform')) {
$this->markTestSkipped('Doctrine DBAL is not available.');
}

if (!class_exists('Doctrine\Common\Version')) {
$this->markTestSkipped('Doctrine Common is not available.');
}

if (!class_exists('Doctrine\ORM\EntityManager')) {
$this->markTestSkipped('Doctrine ORM is not available.');
}

$this->em = DoctrineOrmTestCase::createTestEntityManager();

parent::setUp();

$schemaTool = new SchemaTool($this->em);
$classes = array(
$this->em->getClassMetadata(self::ENTITY_CLASS),
);

try {
$schemaTool->dropSchema($classes);
} catch (\Exception $e) {
}

try {
$schemaTool->createSchema($classes);
} catch (\Exception $e) {
}

$ids = range(1, 300);

foreach ($ids as $id) {
$name = 65 + chr($id % 57);
$this->em->persist(new SingleIdentEntity($id, $name));
}

$this->em->flush();
}

/**
* This test case is realistic in collection forms where each
* row contains the same entity field.
*/
public function testCollapsedEntityField()
{
$this->setMaxRunningTime(1);

for ($i = 0; $i < 20; ++$i) {
$form = $this->factory->create('entity', null, array(
'class' => self::ENTITY_CLASS,
));

// force loading of the choice list
$form->createView();
}
}
}
26 changes: 20 additions & 6 deletions src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@

class ChoiceType extends AbstractType
{
/**
* Caches created choice lists.
* @var array
*/
private $choiceListCache = array();

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -123,12 +129,20 @@ public function finishView(FormViewInterface $view, FormInterface $form, array $
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$choiceList = function (Options $options) {
return new SimpleChoiceList(
// Harden against NULL values (like in EntityType and ModelType)
null !== $options['choices'] ? $options['choices'] : array(),
$options['preferred_choices']
);
$choiceListCache =& $this->choiceListCache;

$choiceList = function (Options $options) use (&$choiceListCache) {
// Harden against NULL values (like in EntityType and ModelType)
$choices = null !== $options['choices'] ? $options['choices'] : array();

// Reuse existing choice lists in order to increase performance
$hash = md5(json_encode(array($choices, $options['preferred_choices'])));

if (!isset($choiceListCache[$hash])) {
$choiceListCache[$hash] = new SimpleChoiceList($choices, $options['preferred_choices']);
}

return $choiceListCache[$hash];
};

$emptyData = function (Options $options) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Form\Tests\Extension\Core\Type;

use Symfony\Component\Form\Tests\FormPerformanceTestCase;

/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoiceTypePerformanceTest extends FormPerformanceTestCase
{
/**
* This test case is realistic in collection forms where each
* row contains the same choice field.
*/
public function testSameChoiceFieldCreatedMultipleTimes()
{
$this->setMaxRunningTime(1);
$choices = range(1, 300);

for ($i = 0; $i < 100; ++$i) {
$this->factory->create('choice', rand(1, 400), array(
'choices' => $choices,
));
}
}
}
42 changes: 42 additions & 0 deletions src/Symfony/Component/Form/Tests/FormIntegrationTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Form\Tests;

use Symfony\Component\Form\FormFactory;
use Symfony\Component\Form\Extension\Core\CoreExtension;

/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormIntegrationTestCase extends \PHPUnit_Framework_TestCase
{
/**
* @var \Symfony\Component\Form\FormFactoryInterface
*/
protected $factory;

protected function setUp()
{
if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
$this->markTestSkipped('The "EventDispatcher" component is not available');
}

$this->factory = new FormFactory($this->getExtensions());
}

protected function getExtensions()
{
return array(
new CoreExtension(),
);
}
}
71 changes: 71 additions & 0 deletions src/Symfony/Component/Form/Tests/FormPerformanceTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Form\Tests;

/**
* Base class for performance tests.
*
* Copied from Doctrine 2's OrmPerformanceTestCase.
*
* @author robo
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormPerformanceTestCase extends FormIntegrationTestCase
{
/**
* @var integer
*/
protected $maxRunningTime = 0;

/**
*/
protected function runTest()
{
$s = microtime(true);
parent::runTest();
$time = microtime(true) - $s;

if ($this->maxRunningTime != 0 && $time > $this->maxRunningTime) {
$this->fail(
sprintf(
'expected running time: <= %s but was: %s',

$this->maxRunningTime,
$time
)
);
}
}

/**
* @param integer $maxRunningTime
* @throws InvalidArgumentException
* @since Method available since Release 2.3.0
Copy link
Member

Choose a reason for hiding this comment

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

this is wrong :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oops, will be fixed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

well I said I copied the code :)

Copy link
Member

Choose a reason for hiding this comment

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

yeah, but then it should mention it is a Doctrine release :)

*/
public function setMaxRunningTime($maxRunningTime)
{
if (is_integer($maxRunningTime) && $maxRunningTime >= 0) {
$this->maxRunningTime = $maxRunningTime;
} else {
throw new \InvalidArgumentException;
}
}

/**
* @return integer
* @since Method available since Release 2.3.0
*/
public function getMaxRunningTime()
{
return $this->maxRunningTime;
}
}
10 changes: 9 additions & 1 deletion src/Symfony/Component/OptionsResolver/OptionsResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ public function __construct()
$this->defaultOptions = new Options();
}

/**
* Clones the resolver.
*/
public function __clone()
{
$this->defaultOptions = clone $this->defaultOptions;
}

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -210,7 +218,7 @@ public function isRequired($option)
/**
* {@inheritdoc}
*/
public function resolve(array $options)
public function resolve(array $options = array())
{
$this->validateOptionsExistence($options);
$this->validateOptionsCompleteness($options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,5 +206,5 @@ public function isRequired($option);
* @throws Exception\OptionDefinitionException If a cyclic dependency is detected
* between two lazy options.
*/
public function resolve(array $options);
public function resolve(array $options = array());
}
Loading