Improve performances in CircualReference detection
authorJérémy Derussé <jeremy@derusse.com>
Thu, 29 Oct 2020 18:50:46 +0000 (19:50 +0100)
committerJérémy Derussé <jeremy@derusse.com>
Sat, 31 Oct 2020 16:09:50 +0000 (17:09 +0100)
src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php

index 4c0c656..fcc0ad3 100644 (file)
@@ -180,25 +180,7 @@ class PhpDumper extends Dumper
             }
         }
 
-        (new AnalyzeServiceReferencesPass(false, !$this->getProxyDumper() instanceof NullDumper))->process($this->container);
-        $checkedNodes = [];
-        $this->circularReferences = [];
-        $this->singleUsePrivateIds = [];
-        foreach ($this->container->getCompiler()->getServiceReferenceGraph()->getNodes() as $id => $node) {
-            if (!$node->getValue() instanceof Definition) {
-                continue;
-            }
-            if (!isset($checkedNodes[$id])) {
-                $this->analyzeCircularReferences($id, $node->getOutEdges(), $checkedNodes);
-            }
-            if ($this->isSingleUsePrivateNode($node)) {
-                $this->singleUsePrivateIds[$id] = $id;
-            }
-        }
-        $this->container->getCompiler()->getServiceReferenceGraph()->clear();
-        $checkedNodes = [];
-        $this->singleUsePrivateIds = array_diff_key($this->singleUsePrivateIds, $this->circularReferences);
-
+        $this->analyzeReferences();
         $this->docStar = $options['debug'] ? '*' : '';
 
         if (!empty($options['file']) && is_dir($dir = \dirname($options['file']))) {
@@ -409,58 +391,92 @@ EOF;
         return $this->proxyDumper;
     }
 
-    private function analyzeCircularReferences(string $sourceId, array $edges, array &$checkedNodes, array &$currentPath = [], bool $byConstructor = true)
+    private function analyzeReferences()
     {
-        $checkedNodes[$sourceId] = true;
-        $currentPath[$sourceId] = $byConstructor;
+        (new AnalyzeServiceReferencesPass(false, !$this->getProxyDumper() instanceof NullDumper))->process($this->container);
+        $checkedNodes = [];
+        $this->circularReferences = [];
+        $this->singleUsePrivateIds = [];
+        foreach ($this->container->getCompiler()->getServiceReferenceGraph()->getNodes() as $id => $node) {
+            if (!$node->getValue() instanceof Definition) {
+                continue;
+            }
 
-        foreach ($edges as $edge) {
-            $node = $edge->getDestNode();
-            $id = $node->getId();
+            if ($this->isSingleUsePrivateNode($node)) {
+                $this->singleUsePrivateIds[$id] = $id;
+            }
 
-            if (!$node->getValue() instanceof Definition || $sourceId === $id || $edge->isLazy() || $edge->isWeak()) {
-                // no-op
-            } elseif (isset($currentPath[$id])) {
-                $this->addCircularReferences($id, $currentPath, $edge->isReferencedByConstructor());
-            } elseif (!isset($checkedNodes[$id])) {
-                $this->analyzeCircularReferences($id, $node->getOutEdges(), $checkedNodes, $currentPath, $edge->isReferencedByConstructor());
-            } elseif (isset($this->circularReferences[$id])) {
-                $this->connectCircularReferences($id, $currentPath, $edge->isReferencedByConstructor());
+            $newNodes = [];
+            if (!$this->collectCircularReferences($id, $node->getOutEdges(), $checkedNodes, $newNodes)) {
+                foreach ($newNodes as $newNodeId => $_) {
+                    $checkedNodes[$newNodeId] = [];
+                }
+                continue;
             }
-        }
-        unset($currentPath[$sourceId]);
-    }
 
-    private function connectCircularReferences(string $sourceId, array &$currentPath, bool $byConstructor, array &$subPath = [])
-    {
-        $currentPath[$sourceId] = $subPath[$sourceId] = $byConstructor;
+            $nodesToFlatten = $newNodes;
+            do {
+                $changedNodes = [];
+                foreach ($nodesToFlatten as $newNodeId => $_) {
+                    $deps = &$checkedNodes[$newNodeId];
+                    foreach ($deps as $id => [$path, $depsByConstructor]) {
+                        foreach ($checkedNodes[$id] as $depsId => [$subPath, $subDepsByConstructor]) {
+                            if (!isset($deps[$depsId]) || ($depsByConstructor && $subDepsByConstructor && !$deps[$depsId][1])) {
+                                array_unshift($subPath, $id);
+                                $deps[$depsId] = [$subPath, $depsByConstructor && $subDepsByConstructor];
+                                $changedNodes += $newNodes[$newNodeId] ?? [];
+                            }
+                        }
+                    }
+                }
+            } while ($nodesToFlatten = $changedNodes);
 
-        foreach ($this->circularReferences[$sourceId] as $id => $byConstructor) {
-            if (isset($currentPath[$id])) {
-                $this->addCircularReferences($id, $currentPath, $byConstructor);
-            } elseif (!isset($subPath[$id]) && isset($this->circularReferences[$id])) {
-                $this->connectCircularReferences($id, $currentPath, $byConstructor, $subPath);
+            foreach ($newNodes as $newNodeId => $_) {
+                if (null !== $n = $checkedNodes[$newNodeId][$newNodeId] ?? null) {
+                    $this->addCircularReferences($newNodeId, $n[0], $n[1]);
+                }
             }
         }
-        unset($currentPath[$sourceId], $subPath[$sourceId]);
+
+        $this->container->getCompiler()->getServiceReferenceGraph()->clear();
+        $this->singleUsePrivateIds = array_diff_key($this->singleUsePrivateIds, $this->circularReferences);
     }
 
-    private function addCircularReferences(string $id, array $currentPath, bool $byConstructor)
+    private function collectCircularReferences(string $sourceId, array $edges, array &$checkedNodes, array &$newNodes, array $path = []): bool
     {
-        $currentPath[$id] = $byConstructor;
-        $circularRefs = [];
+        $path[$sourceId] = true;
+        $checkedNodes[$sourceId] = [];
+        $newNodes[$sourceId] = [];
+        $circular = false;
+        foreach ($edges as $edge) {
+            $node = $edge->getDestNode();
+            $id = $node->getId();
+            if (!$node->getValue() instanceof Definition || $sourceId === $id || $edge->isLazy() || $edge->isWeak()) {
+                continue;
+            }
 
-        foreach (array_reverse($currentPath) as $parentId => $v) {
-            $byConstructor = $byConstructor && $v;
-            $circularRefs[] = $parentId;
+            if (isset($path[$id])) {
+                $circular = true;
+            } elseif (!isset($checkedNodes[$id])) {
+                $circular = $this->collectCircularReferences($id, $node->getOutEdges(), $checkedNodes, $newNodes, $path) || $circular;
+            }
 
-            if ($parentId === $id) {
-                break;
+            $checkedNodes[$sourceId][$id] = [[], $edge->isReferencedByConstructor()];
+            if (isset($newNodes[$id])) {
+                $newNodes[$id][$sourceId] = true;
             }
         }
+        unset($path[$sourceId]);
 
-        $currentId = $id;
-        foreach ($circularRefs as $parentId) {
+        return $circular;
+    }
+
+    private function addCircularReferences(string $sourceId, array $currentPath, bool $byConstructor)
+    {
+        $currentId = $sourceId;
+        $currentPath = array_reverse($currentPath);
+        $currentPath[] = $currentId;
+        foreach ($currentPath as $parentId) {
             if (empty($this->circularReferences[$parentId][$currentId])) {
                 $this->circularReferences[$parentId][$currentId] = $byConstructor;
             }