From 5b839e597d65e587a599f02f8b1cf85b813bd6c9 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 27 Jul 2026 19:35:47 +0200 Subject: [PATCH] Reuse the scope factory of the other flavour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit create() memoizes the services it resolves out of the container, but toFiberFactory()/toMutatingFactory() returned a freshly constructed factory every time, so those memos never survived a scope switching flavour — and scopes switch constantly. On a self-analysis of src/Type that was 192.8K factory constructions, each re-reading two services in the constructor and then resolving nine more on its first create(): 1.6M MemoizingContainer::getByType calls where a few thousand suffice. Pairing the two factories brings the constructions down to 791 and getByType to 94.7K. The pair is held one way strongly and the other way weakly. ScopeFactory makes one of these factories per analysed file, closing over that file's node callback, so a strong cycle between the two would keep every file's callback — and everything it captures — alive for the whole run: PHPStan disables the cycle collector, and nothing else would ever free them. Peak RSS of a src self-analysis measured 678MB before, 1061MB with a strong cycle, and 678MB with the weak back-reference. Interleaved A/B on a quiet machine, user CPU, result cache cleared, turbo extension enabled: -1.40% on src/Type (21.58s -> 21.28s, 6/8 pairs, paired t=3.0); peak RSS over the same pairs is unchanged (-0.35%, t=0.3). Analysis output is unchanged. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016XhKccs7xeB1wRTAQEemc2 --- src/Analyser/LazyInternalScopeFactory.php | 41 +++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index 7b583633875..5e2a5919bfb 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -17,6 +17,7 @@ use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Type\ClosureType; use PHPStan\Type\ExpressionTypeResolverExtension; +use WeakReference; #[GenerateFactory(interface: InternalScopeFactoryFactory::class, resultType: LazyInternalScopeFactory::class)] final class LazyInternalScopeFactory implements InternalScopeFactory @@ -46,6 +47,11 @@ final class LazyInternalScopeFactory implements InternalScopeFactory private ?AttributeReflectionFactory $attributeReflectionFactory = null; + private ?self $twin = null; + + /** @var WeakReference|null */ + private ?WeakReference $origin = null; + /** * @param callable(Node $node, Scope $scope): void|null $nodeCallback */ @@ -131,12 +137,43 @@ public function create( public function toFiberFactory(): InternalScopeFactory { - return new self($this->container, $this->nodeCallback, true); + return $this->fiber ? $this : $this->twin(); } public function toMutatingFactory(): InternalScopeFactory { - return new self($this->container, $this->nodeCallback, false); + return $this->fiber ? $this->twin() : $this; + } + + /** + * The factory for the other scope flavour, created once. Scopes switch + * flavour constantly, and a fresh factory would start with empty memos — + * resolving every service above out of the container again on its first + * create(). + * + * The pair is held one way strongly and the other way weakly: a factory + * belongs to a single analysed file (ScopeFactory::create() makes one per + * file, closing over that file's node callback), and a strong cycle here + * would keep every file's callback alive for the whole run — PHPStan runs + * with the cycle collector disabled, so nothing would ever free it. + */ + private function twin(): self + { + if ($this->twin !== null) { + return $this->twin; + } + + if ($this->origin !== null) { + $origin = $this->origin->get(); + if ($origin !== null) { + return $origin; + } + } + + $this->twin = new self($this->container, $this->nodeCallback, !$this->fiber); + $this->twin->origin = WeakReference::create($this); + + return $this->twin; } }