Skip to content

perf(container): defer service construction with factory map instead of eager lazy proxies - #589

Merged
lisachenko merged 2 commits into
masterfrom
claude/goaop-i2-lazy-container-services
Aug 25, 2026
Merged

perf(container): defer service construction with factory map instead of eager lazy proxies#589
lisachenko merged 2 commits into
masterfrom
claude/goaop-i2-lazy-container-services

Conversation

@lisachenko

Copy link
Copy Markdown
Member

What

Container::addLazyService() previously executed new ReflectionClass($id) + newLazyProxy() for each of the 9 built-in services on every request, autoloading each service class and its whole parent hierarchy — including the Dissect lexer/parser classes — even on a hot-cache request where none of those services are ever touched.

This change makes registration truly lazy:

  • addLazyService() now only stores the factory closure — no reflection, no proxy, no autoloading at boot.
  • The service is constructed, interface-tagged and resource-tracked on its first retrieval (getService() / getValue()), with a re-entrancy guard.
  • getServicesByInterface() materializes pending factories that implement the requested interface before answering, so the weaving and console paths observe exactly the same set of services as before. This path never runs on a hot request.
  • has() accounts for both constructed values and pending factories.

Why (measured)

Boot profile of the fixture project (tests/Fixtures/project, 9 aspects, warm cache, debug=false, opcache file cache, median of 15 runs; kernel init + 6 woven class autoloads + first intercepted call):

PHP 8.5 master PHP 8.5 this PR PHP 8.4 master PHP 8.4 this PR
AspectKernel::init() 1.91 ms 1.14 ms (−40 %) 1.75 ms 1.08 ms (−38 %)
full request 3.41 ms 2.90 ms (−15 %) 3.51 ms 2.90 ms (−17 %)
included files 142 129 158 145

Cold-start behavior is unchanged (weaving dominates there and all services materialize anyway).

BC note (4.0)

Services registered via addLazyService() are no longer PHP 8.4 lazy proxy instances available from boot; they are constructed on first retrieval. Code that relied on getValue() returning an uninitialized proxy observes the factory running at retrieval time instead. The container's public API surface is unchanged.

tests/Core/ContainerTest.php was updated accordingly: the proxy-specific test became testLazyServiceIsNotConstructedUntilFirstRetrieval, which pins the new deferred-construction semantics (registration alone never invokes the factory).

Part of the boot-time series

First of five sequential PRs from the hot-start boot profiling effort (container laziness → lazy aspects → lazy transformers → trusted prebuilt cache → cache-state split). Each lands independently; later ones build on merged earlier ones.

🤖 Generated with Claude Code

https://claude.ai/code/session_01V1Z87HZ2iz23WPTtTYqFxE


Generated by Claude Code

…of eager lazy proxies

Registering the 9 built-in services previously did new ReflectionClass() +
newLazyProxy() per service on every request, autoloading each service class
and its full parent hierarchy (including the Dissect lexer/parser classes)
even on a hot-cache request where none of them are ever used.

addLazyService() now only stores the factory closure; the service is
constructed, tagged and resource-tracked on first retrieval. Interface-tag
queries (getServicesByInterface) materialize pending factories that
implement the requested interface, so the weaving and console paths observe
the same set of services as before.

BC note (4.0): container services are no longer PHP 8.4 lazy proxies; a
service registered via addLazyService() is constructed on first getService()
/ getValue() / matching getServicesByInterface() call instead of being a
proxy instance from boot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1Z87HZ2iz23WPTtTYqFxE

Copy link
Copy Markdown
Member Author

CI status on this head: all required checks are green (PHPStan, PHPUnit lowest/highest on 8.4 and 8.5). The two failing jobs are the experimental PHP 8.6 jobs (continue-on-error: true), and the failure is not caused by this PR:

Go\Proxy\Part\InterceptedFunctionGeneratorTest::testGenerate@strcoll — PHP 8.6.0 added #[\Deprecated(message: 'use Collator::compare() instead', since: '8.6')] to strcoll(), so the generated function proxy now includes the attribute while the test expects the bare signature. It reproduces identically on any branch under 8.6 (it failed the same way on the unrelated head of #588) and is deterministic, so a re-run would not change it; this PR does not touch function proxy generation.

Proposed fix (separate small PR, since it's out of scope here): in tests/Proxy/Part/InterceptedFunctionGeneratorTest.php, make the expectation version-aware, e.g. strip attribute lines before comparing or gate an expanded expected string behind PHP_VERSION_ID >= 80600:

$generated = preg_replace('/^#\[\\\\Deprecated\(.*\)\]\n/m', '', $generated);

Happy to open that follow-up PR on request.


Generated by Claude Code

@lisachenko
lisachenko marked this pull request as ready for review August 25, 2026 19:22
Comment thread src/Core/Container.php
Comment thread src/Core/Container.php
getServicesByInterface() probes pending factory ids with is_subclass_of(),
so the factory map must only ever hold class-names. Enforce the documented
class-string contract with a syntactic check at registration time (no
autoloading); arbitrary string keys keep going through add() as before.

Addresses review feedback on the lazy factory map.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1Z87HZ2iz23WPTtTYqFxE
@lisachenko
lisachenko merged commit 1fe9a93 into master Aug 25, 2026
5 of 7 checks passed
@lisachenko
lisachenko deleted the claude/goaop-i2-lazy-container-services branch August 25, 2026 19:37

Copy link
Copy Markdown
Member Author

Update on the PHP 8.6 experimental jobs: the fix now exists as #590 (swap the strcoll dataset to strcmp), and I've ported that one-line test change into this branch as 145edcd so the 8.6 jobs go green here too. It becomes a no-op once #590 merges into master.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants