From 8298d8c2601b1baaface61735f1873fee151c042 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 12 Jul 2012 12:34:07 +0200 Subject: [PATCH] [Form] Improved ChoiceType performance by caching ChoiceList objects --- .../Form/Extension/Core/Type/ChoiceType.php | 26 +++++-- .../Core/Type/ChoiceTypePerformanceTest.php | 36 ++++++++++ .../Form/Tests/FormIntegrationTestCase.php | 42 +++++++++++ .../Form/Tests/FormPerformanceTestCase.php | 71 +++++++++++++++++++ 4 files changed, 169 insertions(+), 6 deletions(-) create mode 100644 src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypePerformanceTest.php create mode 100644 src/Symfony/Component/Form/Tests/FormIntegrationTestCase.php create mode 100644 src/Symfony/Component/Form/Tests/FormPerformanceTestCase.php diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index 1be5c36eba..bcda8d2559 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -31,6 +31,12 @@ use Symfony\Component\OptionsResolver\OptionsResolverInterface; class ChoiceType extends AbstractType { + /** + * Caches created choice lists. + * @var array + */ + private $choiceListCache = array(); + /** * {@inheritdoc} */ @@ -123,12 +129,20 @@ class ChoiceType extends AbstractType */ 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) { diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypePerformanceTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypePerformanceTest.php new file mode 100644 index 0000000000..a9574bd2b2 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypePerformanceTest.php @@ -0,0 +1,36 @@ + + * + * 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 + */ +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, + )); + } + } +} diff --git a/src/Symfony/Component/Form/Tests/FormIntegrationTestCase.php b/src/Symfony/Component/Form/Tests/FormIntegrationTestCase.php new file mode 100644 index 0000000000..50923492ba --- /dev/null +++ b/src/Symfony/Component/Form/Tests/FormIntegrationTestCase.php @@ -0,0 +1,42 @@ + + * + * 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 + */ +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(), + ); + } +} diff --git a/src/Symfony/Component/Form/Tests/FormPerformanceTestCase.php b/src/Symfony/Component/Form/Tests/FormPerformanceTestCase.php new file mode 100644 index 0000000000..be381074f1 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/FormPerformanceTestCase.php @@ -0,0 +1,71 @@ + + * + * 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 + */ +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 + */ + 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; + } +}