Skip to content

Commit

Permalink
[OptionsResolver] Improved the performance of normalizers
Browse files Browse the repository at this point in the history
Normalizers are now stored in the Options instance only once. Previously,
normalizers were stored in Options upon resolving, which meant that
they were added a lot of time if the same resolver was used for many
different options arrays.

This improvement led to an improvement of 30ms on
advancedform.gpserver.dk/app_dev.php/taxclasses/1
  • Loading branch information
webmozart committed Jul 26, 2012
1 parent e08e60c commit e659f0e
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 27 deletions.
108 changes: 93 additions & 15 deletions src/Symfony/Component/OptionsResolver/Options.php
Expand Up @@ -26,6 +26,18 @@ class Options implements \ArrayAccess, \Iterator, \Countable
*/
private $options = array();

/**
* A list of normalizer closures.
* @var array
*/
private $normalizers = array();

/**
* A list storing the names of all options that need to be normalized.
* @var array
*/
private $normalization = array();

/**
* A list storing the names of all LazyOption instances as keys.
* @var array
Expand Down Expand Up @@ -87,6 +99,38 @@ public function set($option, $value)
$this->overload($option, $value);
}

/**
* Sets the normalizer for a given option.
*
* Normalizers should be closures with the following signature:
*
* <code>
* function (Options $options, $value)
* </code>
*
* This closure will be evaluated once the option is read using
* {@link get()}. The closure has access to the resolved values of
* other options through the passed {@link Options} instance.
*
* @param string $option The name of the option.
* @param \Closure $normalizer The normalizer.
*
* @throws OptionDefinitionException If options have already been read.
* Once options are read, the container
* becomes immutable.
*/
public function setNormalizer($option, \Closure $normalizer)
{
if ($this->reading) {
throw new OptionDefinitionException('Normalizers cannot be added anymore once options have been read.');
}

$this->normalizers[$option] = $normalizer;

// Each option for which a normalizer exists needs to be normalized
$this->normalization[$option] = true;
}

/**
* Replaces the contents of the container with the given options.
*
Expand Down Expand Up @@ -149,9 +193,6 @@ public function overload($option, $value)
$currentValue = isset($this->options[$option]) ? $this->options[$option] : null;
$value = new LazyOption($value, $currentValue);

// Store locks for lazy options to detect cyclic dependencies
$this->lock[$option] = false;

// Store which options are lazy for more efficient resolving
$this->lazy[$option] = true;

Expand All @@ -162,7 +203,6 @@ public function overload($option, $value)
}

// Reset lazy flag and locks by default
unset($this->lock[$option]);
unset($this->lazy[$option]);

$this->options[$option] = $value;
Expand Down Expand Up @@ -193,6 +233,10 @@ public function get($option)
$this->resolve($option);
}

if (isset($this->normalization[$option])) {
$this->normalize($option);
}

return $this->options[$option];
}

Expand Down Expand Up @@ -224,7 +268,6 @@ public function remove($option)
}

unset($this->options[$option]);
unset($this->lock[$option]);
unset($this->lazy[$option]);
}

Expand All @@ -242,7 +285,6 @@ public function clear()
}

$this->options = array();
$this->lock = array();
$this->lazy = array();
}

Expand All @@ -257,19 +299,20 @@ public function all()
{
$this->reading = true;

// Create a copy because resolve() modifies the array
$lazy = $this->lazy;

// Performance-wise this is slightly better than
// while (null !== $option = key($this->lazy))
foreach ($lazy as $option => $isLazy) {
// When resolve() is called, potentially multiple lazy options
// are evaluated, so check again if the option is still lazy.
foreach ($this->lazy as $option => $isLazy) {
if (isset($this->lazy[$option])) {
$this->resolve($option);
}
}

foreach ($this->normalization as $option => $normalize) {
if (isset($this->normalization[$option])) {
$this->normalize($option);
}
}

return $this->options;
}

Expand Down Expand Up @@ -388,7 +431,7 @@ public function count()
}

/**
* Evaluates the given option if it is a lazy option.
* Evaluates the given lazy option.
*
* The evaluated value is written into the options array. The closure for
* evaluating the option is discarded afterwards.
Expand All @@ -400,7 +443,7 @@ public function count()
*/
private function resolve($option)
{
if ($this->lock[$option]) {
if (isset($this->lock[$option])) {
$conflicts = array();

foreach ($this->lock as $option => $locked) {
Expand All @@ -414,9 +457,44 @@ private function resolve($option)

$this->lock[$option] = true;
$this->options[$option] = $this->options[$option]->evaluate($this);
$this->lock[$option] = false;
unset($this->lock[$option]);

// The option now isn't lazy anymore
unset($this->lazy[$option]);
}

/**
* Normalizes the given option.
*
* The evaluated value is written into the options array.
*
* @param string $option The option to normalizer.
*
* @throws OptionDefinitionException If the option has a cyclic dependency
* on another option.
*/
private function normalize($option)
{
if (isset($this->lock[$option])) {
$conflicts = array();

foreach ($this->lock as $option => $locked) {
if ($locked) {
$conflicts[] = $option;
}
}

throw new OptionDefinitionException('The options "' . implode('", "', $conflicts) . '" have a cyclic dependency.');
}

/** @var \Closure $normalizer */
$normalizer = $this->normalizers[$option];

$this->lock[$option] = true;
$this->options[$option] = $normalizer($this, $this->options[$option]);
unset($this->lock[$option]);

// The option is now normalized
unset($this->normalization[$option]);
}
}
15 changes: 3 additions & 12 deletions src/Symfony/Component/OptionsResolver/OptionsResolver.php
Expand Up @@ -53,12 +53,6 @@ class OptionsResolver implements OptionsResolverInterface
*/
private $allowedTypes = array();

/**
* A list of normalizers transforming each resolved options.
* @var array
*/
private $normalizers = array();

/**
* Creates a new instance.
*/
Expand Down Expand Up @@ -194,7 +188,9 @@ public function setNormalizers(array $normalizers)
{
$this->validateOptionsExistence($normalizers);

$this->normalizers = array_replace($this->normalizers, $normalizers);
foreach ($normalizers as $option => $normalizer) {
$this->defaultOptions->setNormalizer($option, $normalizer);
}

return $this;
}
Expand Down Expand Up @@ -231,11 +227,6 @@ public function resolve(array $options = array())
$combinedOptions->set($option, $value);
}

// Apply filters
foreach ($this->normalizers as $option => $filter) {
$combinedOptions->overload($option, $filter);
}

// Resolve options
$resolvedOptions = $combinedOptions->all();

Expand Down
149 changes: 149 additions & 0 deletions src/Symfony/Component/OptionsResolver/Tests/OptionsTest.php
Expand Up @@ -79,6 +79,16 @@ public function testRemoveNotSupportedAfterGet()
$this->options->remove('foo');
}

/**
* @expectedException Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
*/
public function testSetNormalizerNotSupportedAfterGet()
{
$this->options->set('foo', 'bar');
$this->options->get('foo');
$this->options->setNormalizer('foo', function () {});
}

public function testSetLazyOption()
{
$test = $this;
Expand Down Expand Up @@ -182,6 +192,66 @@ public function testLazyOptionCanAccessOtherLazyOptions()
$this->assertEquals('dynamic', $this->options->get('bam'));
}

public function testNormalizer()
{
$this->options->set('foo', 'bar');

$this->options->setNormalizer('foo', function () {
return 'normalized';
});

$this->assertEquals('normalized', $this->options->get('foo'));
}

public function testNormalizerReceivesUnnormalizedValue()
{
$this->options->set('foo', 'bar');

$this->options->setNormalizer('foo', function (Options $options, $value) {
return 'normalized[' . $value . ']';
});

$this->assertEquals('normalized[bar]', $this->options->get('foo'));
}

public function testNormalizerCanAccessOtherOptions()
{
$test = $this;

$this->options->set('foo', 'bar');
$this->options->set('bam', 'baz');

$this->options->setNormalizer('bam', function (Options $options) use ($test) {
/* @var \PHPUnit_Framework_TestCase $test */
$test->assertEquals('bar', $options->get('foo'));

return 'normalized';
});

$this->assertEquals('bar', $this->options->get('foo'));
$this->assertEquals('normalized', $this->options->get('bam'));
}

public function testNormalizerCanAccessOtherLazyOptions()
{
$test = $this;

$this->options->set('foo', function (Options $options) {
return 'bar';
});
$this->options->set('bam', 'baz');

$this->options->setNormalizer('bam', function (Options $options) use ($test) {
/* @var \PHPUnit_Framework_TestCase $test */
$test->assertEquals('bar', $options->get('foo'));

return 'normalized';
});

$this->assertEquals('bar', $this->options->get('foo'));
$this->assertEquals('normalized', $this->options->get('bam'));
}

/**
* @expectedException Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
*/
Expand All @@ -198,6 +268,85 @@ public function testFailForCyclicDependencies()
$this->options->get('foo');
}

/**
* @expectedException Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
*/
public function testFailForCyclicDependenciesBetweenNormalizers()
{
$this->options->set('foo', 'bar');
$this->options->set('bam', 'baz');

$this->options->setNormalizer('foo', function (Options $options) {
$options->get('bam');
});

$this->options->setNormalizer('bam', function (Options $options) {
$options->get('foo');
});

$this->options->get('foo');
}

/**
* @expectedException Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
*/
public function testFailForCyclicDependenciesBetweenNormalizerAndLazyOption()
{
$this->options->set('foo', function (Options $options) {
$options->get('bam');
});
$this->options->set('bam', 'baz');

$this->options->setNormalizer('bam', function (Options $options) {
$options->get('foo');
});

$this->options->get('foo');
}

public function testAllInvokesEachLazyOptionOnlyOnce()
{
$test = $this;
$i = 1;

$this->options->set('foo', function (Options $options) use ($test, &$i) {
$test->assertSame(1, $i);
++$i;

// Implicitly invoke lazy option for "bam"
$options->get('bam');
});
$this->options->set('bam', function (Options $options) use ($test, &$i) {
$test->assertSame(2, $i);
++$i;
});

$this->options->all();
}

public function testAllInvokesEachNormalizerOnlyOnce()
{
$test = $this;
$i = 1;

$this->options->set('foo', 'bar');
$this->options->set('bam', 'baz');

$this->options->setNormalizer('foo', function (Options $options) use ($test, &$i) {
$test->assertSame(1, $i);
++$i;

// Implicitly invoke normalizer for "bam"
$options->get('bam');
});
$this->options->setNormalizer('bam', function (Options $options) use ($test, &$i) {
$test->assertSame(2, $i);
++$i;
});

$this->options->all();
}

public function testReplaceClearsAndSets()
{
$this->options->set('one', '1');
Expand Down

0 comments on commit e659f0e

Please sign in to comment.