[DependencyInjection] performance improvement, better analysis tools

This commit is contained in:
Johannes Schmitt 2011-01-09 10:53:46 +01:00 committed by Fabien Potencier
parent e85546ef7d
commit d1a2a65d19
14 changed files with 550 additions and 109 deletions

View File

@ -0,0 +1,89 @@
<?php
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Run this pass before passes that need to know more about the relation of
* your services.
*
* This class will populate the ServiceReferenceGraph with information. You can
* retrieve the graph in other passes from the compiler.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class AnalyzeServiceReferencesPass implements RepeatablePassInterface, CompilerAwareInterface
{
protected $graph;
protected $container;
protected $currentId;
protected $currentDefinition;
protected $repeatedPass;
public function setRepeatedPass(RepeatedPass $repeatedPass) {
$this->repeatedPass = $repeatedPass;
}
public function setCompiler(Compiler $compiler)
{
$this->graph = $compiler->getServiceReferenceGraph();
}
public function process(ContainerBuilder $container)
{
$this->container = $container;
if (null === $this->graph) {
$this->graph = $this->repeatedPass->getCompiler()->getServiceReferenceGraph();
}
$this->graph->clear();
foreach ($container->getDefinitions() as $id => $definition) {
$this->currentId = $id;
$this->currentDefinition = $definition;
$this->processArguments($definition->getArguments());
$this->processArguments($definition->getMethodCalls());
}
foreach ($container->getAliases() as $id => $alias) {
$this->graph->connect($id, $alias, (string) $alias, $this->getDefinition((string) $alias), null);
}
}
protected function processArguments(array $arguments)
{
foreach ($arguments as $k => $argument) {
if (is_array($argument)) {
$this->processArguments($argument);
} else if ($argument instanceof Reference) {
$this->graph->connect(
$this->currentId,
$this->currentDefinition,
(string) $argument,
$this->getDefinition((string) $argument),
$argument
);
} else if ($argument instanceof Definition) {
$this->processArguments($argument->getArguments());
$this->processArguments($argument->getMethodCalls());
}
}
}
protected function getDefinition($id)
{
while ($this->container->hasAlias($id)) {
$id = (string) $this->container->getAlias($id);
}
if (!$this->container->hasDefinition($id)) {
return null;
}
return $this->container->getDefinition($id);
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* This class is used to remove circular dependencies between individual passes.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class Compiler
{
protected $passConfig;
protected $currentPass;
protected $currentStartTime;
protected $log;
protected $serviceReferenceGraph;
public function __construct()
{
$this->passConfig = new PassConfig();
$this->serviceReferenceGraph = new ServiceReferenceGraph();
$this->log = array();
}
public function getPassConfig()
{
return $this->passConfig;
}
public function getServiceReferenceGraph()
{
return $this->serviceReferenceGraph;
}
public function addPass(CompilerPassInterface $pass)
{
$this->passConfig->addPass($pass);
}
public function addLogMessage($string)
{
$this->log[] = $string;
}
public function getLog()
{
return $this->log;
}
public function compile(ContainerBuilder $container)
{
foreach ($this->passConfig->getPasses() as $pass) {
$this->startPass($pass);
$pass->process($container);
$this->endPass($pass);
}
}
protected function startPass(CompilerPassInterface $pass)
{
if ($pass instanceof CompilerAwareInterface) {
$pass->setCompiler($this);
}
$this->currentPass = $pass;
$this->currentStartTime = microtime(true);
}
protected function endPass(CompilerPassInterface $pass)
{
$this->currentPass = null;
$this->addLogMessage(sprintf('%s finished in %.3fs', get_class($pass), microtime(true) - $this->currentStartTime));
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Symfony\Component\DependencyInjection\Compiler;
/**
* This interface can be implemented by passes that need to access the
* compiler.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
interface CompilerAwareInterface
{
function setCompiler(Compiler $compiler);
}

View File

@ -23,8 +23,8 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
*/
class InlineServiceDefinitionsPass implements RepeatablePassInterface
{
protected $aliasMap;
protected $repeatedPass;
protected $graph;
public function setRepeatedPass(RepeatedPass $repeatedPass)
{
@ -33,14 +33,7 @@ class InlineServiceDefinitionsPass implements RepeatablePassInterface
public function process(ContainerBuilder $container)
{
$this->aliasMap = array();
foreach ($container->getAliases() as $id => $alias) {
if (!$alias->isPublic()) {
continue;
}
$this->aliasMap[$id] = (string) $alias;
}
$this->graph = $this->repeatedPass->getCompiler()->getServiceReferenceGraph();
foreach ($container->getDefinitions() as $id => $definition) {
$definition->setArguments(
@ -82,43 +75,15 @@ class InlineServiceDefinitionsPass implements RepeatablePassInterface
return false;
}
$references = count(array_keys($this->aliasMap, $id, true));
foreach ($container->getDefinitions() as $cDefinition)
{
if ($references > 1) {
break;
}
if ($this->isReferencedByArgument($id, $cDefinition->getArguments())) {
$references += 1;
continue;
}
foreach ($cDefinition->getMethodCalls() as $call) {
if ($this->isReferencedByArgument($id, $call[1])) {
$references += 1;
continue 2;
}
}
if (!$this->graph->hasNode($id)) {
return true;
}
return $references <= 1;
}
protected function isReferencedByArgument($id, $argument)
{
if (is_array($argument)) {
foreach ($argument as $arg) {
if ($this->isReferencedByArgument($id, $arg)) {
return true;
}
}
} else if ($argument instanceof Reference) {
if ($id === (string) $argument) {
return true;
}
$ids = array();
foreach ($this->graph->getNode($id)->getInEdges() as $edge) {
$ids[] = $edge->getSourceNode()->getId();
}
return false;
return count(array_unique($ids)) <= 1;
}
}

View File

@ -14,14 +14,22 @@ namespace Symfony\Component\DependencyInjection\Compiler;
/**
* Compiler Pass Configuration
*
* This class has a default configuration embedded.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class PassConfig
{
const TYPE_AFTER_REMOVING = 'afterRemoving';
const TYPE_BEFORE_OPTIMIZATION = 'beforeOptimization';
const TYPE_BEFORE_REMOVING = 'beforeRemoving';
const TYPE_OPTIMIZE = 'optimization';
const TYPE_REMOVE = 'removing';
protected $mergePass;
protected $afterRemovingPasses;
protected $beforeOptimizationPasses;
protected $beforeRemovingPasses;
protected $optimizationPasses;
protected $removingPasses;
@ -29,6 +37,10 @@ class PassConfig
{
$this->mergePass = new MergeExtensionConfigurationPass();
$this->afterRemovingPasses = array();
$this->beforeOptimizationPasses = array();
$this->beforeRemovingPasses = array();
$this->optimizationPasses = array(
new ResolveParameterPlaceHoldersPass(),
new ResolveReferencesToAliasesPass(),
@ -40,7 +52,9 @@ class PassConfig
new RemovePrivateAliasesPass(),
new ReplaceAliasByActualDefinitionPass(),
new RepeatedPass(array(
new AnalyzeServiceReferencesPass(),
new InlineServiceDefinitionsPass(),
new AnalyzeServiceReferencesPass(),
new RemoveUnusedDefinitionsPass(),
)),
);
@ -50,12 +64,15 @@ class PassConfig
{
return array_merge(
array($this->mergePass),
$this->beforeOptimizationPasses,
$this->optimizationPasses,
$this->removingPasses
$this->beforeRemovingPasses,
$this->removingPasses,
$this->afterRemovingPasses
);
}
public function addPass(CompilerPassInterface $pass, $type = self::TYPE_OPTIMIZE)
public function addPass(CompilerPassInterface $pass, $type = self::TYPE_BEFORE_OPTIMIZATION)
{
$property = $type.'Passes';
if (!isset($this->$property)) {
@ -66,6 +83,16 @@ class PassConfig
$passes[] = $pass;
}
public function getBeforeOptimizationPasses()
{
return $this->beforeOptimizationPasses;
}
public function getBeforeRemovingPasses()
{
return $this->beforeRemovingPasses;
}
public function getOptimizationPasses()
{
return $this->optimizationPasses;
@ -86,6 +113,16 @@ class PassConfig
$this->mergePass = $pass;
}
public function setBeforeOptimizationPasses(array $passes)
{
$this->beforeOptimizationPasses = $passes;
}
public function setBeforeRemovingPasses(array $passes)
{
$this->beforeRemovingPasses = $passes;
}
public function setOptimizationPasses(array $passes)
{
$this->optimizationPasses = $passes;

View File

@ -2,6 +2,8 @@
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@ -23,6 +25,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
class RemoveUnusedDefinitionsPass implements RepeatablePassInterface
{
protected $repeatedPass;
protected $graph;
public function setRepeatedPass(RepeatedPass $repeatedPass)
{
@ -31,18 +34,31 @@ class RemoveUnusedDefinitionsPass implements RepeatablePassInterface
public function process(ContainerBuilder $container)
{
$this->graph = $this->repeatedPass->getCompiler()->getServiceReferenceGraph();
$hasChanged = false;
$aliases = $container->getAliases();
foreach ($container->getDefinitions() as $id => $definition) {
if ($definition->isPublic()) {
continue;
}
$referencingAliases = array_keys($aliases, $id, true);
$isReferenced = $this->isReferenced($container, $id);
if ($this->graph->hasNode($id)) {
$edges = $this->graph->getNode($id)->getInEdges();
$referencingAliases = array();
foreach ($edges as $edge) {
$node = $edge->getSourceNode();
if ($node->isAlias()) {
$referencingAlias[] = $node->getValue();
}
}
$isReferenced = (count($edges) - count($referencingAliases)) > 0;
} else {
$referencingAliases = array();
$isReferenced = false;
}
if (1 === count($referencingAliases) && false === $isReferenced) {
$container->setDefinition(reset($referencingAliases), $definition);
$container->setDefinition((string) reset($referencingAliases), $definition);
$definition->setPublic(true);
$container->remove($id);
} else if (0 === count($referencingAliases) && false === $isReferenced) {
@ -55,44 +71,4 @@ class RemoveUnusedDefinitionsPass implements RepeatablePassInterface
$this->repeatedPass->setRepeat();
}
}
protected function isReferenced(ContainerBuilder $container, $id)
{
foreach ($container->getDefinitions() as $definition) {
if ($this->isReferencedByArgument($id, $definition->getArguments())) {
return true;
}
if ($this->isReferencedByArgument($id, $definition->getMethodCalls())) {
return true;
}
}
return false;
}
protected function isReferencedByArgument($id, $argument)
{
if (is_array($argument)) {
foreach ($argument as $arg) {
if ($this->isReferencedByArgument($id, $arg)) {
return true;
}
}
} else if ($argument instanceof Reference) {
if ($id === (string) $argument) {
return true;
}
} else if ($argument instanceof Definition) {
if ($this->isReferencedByArgument($id, $argument->getArguments())) {
return true;
}
if ($this->isReferencedByArgument($id, $argument->getMethodCalls())) {
return true;
}
}
return false;
}
}

View File

@ -9,9 +9,10 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class RepeatedPass implements CompilerPassInterface
class RepeatedPass implements CompilerPassInterface, CompilerAwareInterface
{
protected $repeat;
protected $compiler;
protected $passes;
public function __construct(array $passes)
@ -27,11 +28,25 @@ class RepeatedPass implements CompilerPassInterface
$this->passes = $passes;
}
public function setCompiler(Compiler $compiler)
{
$this->compiler = $compiler;
}
public function getCompiler()
{
return $this->compiler;
}
public function process(ContainerBuilder $container)
{
$this->repeat = false;
foreach ($this->passes as $pass) {
$time = microtime(true);
$pass->process($container);
$this->compiler->addLogMessage(sprintf(
'%s finished in %.3fs', get_class($pass), microtime(true) - $time
));
}
if ($this->repeat) {

View File

@ -0,0 +1,64 @@
<?php
namespace Symfony\Component\DependencyInjection\Compiler;
/**
* This is a directed graph of your services.
*
* This information can be used by your compiler passes instead of collecting
* it themselves which improves performance quite a lot.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class ServiceReferenceGraph
{
protected $nodes;
public function __construct()
{
$this->nodes = array();
}
public function hasNode($id)
{
return isset($this->nodes[$id]);
}
public function getNode($id)
{
if (!isset($this->nodes[$id])) {
throw new \InvalidArgumentException(sprintf('There is no node with id "%s".', $id));
}
return $this->nodes[$id];
}
public function getNodes()
{
return $this->nodes;
}
public function clear()
{
$this->nodes = array();
}
public function connect($sourceId, $sourceValue, $destId, $destValue = null, $reference = null)
{
$sourceNode = $this->createNode($sourceId, $sourceValue);
$destNode = $this->createNode($destId, $destValue);
$edge = new ServiceReferenceGraphEdge($sourceNode, $destNode, $reference);
$sourceNode->addOutEdge($edge);
$destNode->addInEdge($edge);
}
protected function createNode($id, $value)
{
if (isset($this->nodes[$id]) && $this->nodes[$id]->getValue() === $value) {
return $this->nodes[$id];
}
return $this->nodes[$id] = new ServiceReferenceGraphNode($id, $value);
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Symfony\Component\DependencyInjection\Compiler;
/**
* Represents an edge in your service graph.
*
* Value is typically a reference.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class ServiceReferenceGraphEdge
{
protected $sourceNode;
protected $destNode;
protected $value;
public function __construct(ServiceReferenceGraphNode $sourceNode, ServiceReferenceGraphNode $destNode, $value = null)
{
$this->sourceNode = $sourceNode;
$this->destNode = $destNode;
$this->value = $value;
}
public function getValue()
{
return $this->value;
}
public function getSourceNode()
{
return $this->sourceNode;
}
public function getDestNode()
{
return $this->destNode;
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Alias;
/**
* Represents a node in your service graph.
*
* Value is typically a definition, or an alias.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class ServiceReferenceGraphNode
{
protected $id;
protected $inEdges;
protected $outEdges;
protected $value;
public function __construct($id, $value)
{
$this->id = $id;
$this->value = $value;
$this->inEdges = array();
$this->outEdges = array();
}
public function addInEdge(ServiceReferenceGraphEdge $edge)
{
$this->inEdges[] = $edge;
}
public function addOutEdge(ServiceReferenceGraphEdge $edge)
{
$this->outEdges[] = $edge;
}
public function isAlias()
{
return $this->value instanceof Alias;
}
public function isDefinition()
{
return $this->value instanceof Definition;
}
public function getId()
{
return $this->id;
}
public function getInEdges()
{
return $this->inEdges;
}
public function getOutEdges()
{
return $this->outEdges;
}
public function getValue()
{
return $this->value;
}
}

View File

@ -2,12 +2,7 @@
namespace Symfony\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\Compiler\RemoveUnusedDefinitionsPass;
use Symfony\Component\DependencyInjection\Compiler\ResolveInterfaceInjectorsPass;
use Symfony\Component\DependencyInjection\Compiler\MergeExtensionConfigurationPass;
use Symfony\Component\DependencyInjection\Compiler\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\DependencyInjection\InterfaceInjector;
@ -39,7 +34,7 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
protected $resources = array();
protected $extensionConfigs = array();
protected $injectors = array();
protected $compilerPassConfig;
protected $compiler;
/**
* Constructor
@ -49,7 +44,7 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
{
parent::__construct($parameterBag);
$this->compilerPassConfig = new PassConfig();
$this->compiler = new Compiler();
}
/**
@ -154,7 +149,7 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
*/
public function addCompilerPass(CompilerPassInterface $pass)
{
$this->compilerPassConfig->addPass($pass);
$this->compiler->addPass($pass);
}
/**
@ -164,7 +159,17 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
*/
public function getCompilerPassConfig()
{
return $this->compilerPassConfig;
return $this->compiler->getPassConfig();
}
/**
* Returns the compiler instance
*
* @return Compiler
*/
public function getCompiler()
{
return $this->compiler;
}
/**
@ -335,9 +340,7 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
*/
public function freeze()
{
foreach ($this->compilerPassConfig->getPasses() as $pass) {
$pass->process($this);
}
$this->compiler->compile($this);
parent::freeze();
}

View File

@ -0,0 +1,86 @@
<?php
namespace Symfony\Tests\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Compiler\Compiler;
use Symfony\Component\DependencyInjection\Compiler\AnalyzeServiceReferencesPass;
use Symfony\Component\DependencyInjection\Compiler\RepeatedPass;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class AnalyzeServiceReferencesPassTest extends \PHPUnit_Framework_TestCase
{
public function testProcess()
{
$container = new ContainerBuilder();
$a = $container
->register('a')
->addArgument($ref1 = new Reference('b'))
;
$b = $container
->register('b')
->addMethodCall('setA', array($ref2 = new Reference('a')))
;
$c = $container
->register('c')
->addArgument($ref3 = new Reference('a'))
->addArgument($ref4 = new Reference('b'))
;
$graph = $this->process($container);
$this->assertEquals(2, count($edges = $graph->getNode('b')->getInEdges()));
$this->assertSame($ref1, $edges[0]->getValue());
$this->assertSame($ref4, $edges[1]->getValue());
}
public function testProcessDetectsReferencesFromInlinedDefinitions()
{
$container = new ContainerBuilder();
$container
->register('a')
;
$container
->register('b')
->addArgument(new Definition(null, array($ref = new Reference('a'))))
;
$graph = $this->process($container);
$this->assertEquals(1, count($refs = $graph->getNode('a')->getInEdges()));
$this->assertSame($ref, $refs[0]->getValue());
}
public function testProcessDoesNotSaveDuplicateReferences()
{
$container = new ContainerBuilder();
$container
->register('a')
;
$container
->register('b')
->addArgument(new Definition(null, array($ref1 = new Reference('a'))))
->addArgument(new Definition(null, array($ref2 = new Reference('a'))))
;
$graph = $this->process($container);
$this->assertEquals(2, count($graph->getNode('a')->getInEdges()));
}
protected function process(ContainerBuilder $container)
{
$pass = new RepeatedPass(array(new AnalyzeServiceReferencesPass()));
$pass->setCompiler($compiler = new Compiler());
$pass->process($container);
return $compiler->getServiceReferenceGraph();
}
}

View File

@ -2,6 +2,12 @@
namespace Symfony\Tests\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\AnalyzeServiceReferencesPass;
use Symfony\Component\DependencyInjection\Compiler\Compiler;
use Symfony\Component\DependencyInjection\Compiler\RepeatedPass;
use Symfony\Component\DependencyInjection\Compiler\InlineServiceDefinitionsPass;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@ -77,7 +83,8 @@ class InlineServiceDefinitionsPassTest extends \PHPUnit_Framework_TestCase
protected function process(ContainerBuilder $container)
{
$pass = new InlineServiceDefinitionsPass();
$pass->process($container);
$repeatedPass = new RepeatedPass(array(new AnalyzeServiceReferencesPass(), new InlineServiceDefinitionsPass()));
$repeatedPass->setCompiler(new Compiler());
$repeatedPass->process($container);
}
}

View File

@ -2,8 +2,9 @@
namespace Symfony\Tests\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\AnalyzeServiceReferencesPass;
use Symfony\Component\DependencyInjection\Compiler\Compiler;
use Symfony\Component\DependencyInjection\Compiler\RepeatedPass;
use Symfony\Component\DependencyInjection\Compiler\RemoveUnusedDefinitionsPass;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
@ -73,8 +74,8 @@ class RemoveUnusedDefinitionsPassTest extends \PHPUnit_Framework_TestCase
protected function process(ContainerBuilder $container)
{
$pass = new RemoveUnusedDefinitionsPass();
$repeatedPass = new RepeatedPass(array($pass));
$pass->process($container);
$repeatedPass = new RepeatedPass(array(new AnalyzeServiceReferencesPass(), new RemoveUnusedDefinitionsPass()));
$repeatedPass->setCompiler(new Compiler());
$repeatedPass->process($container);
}
}