perf: memoize wiring plans on the registry, shared tree-wide - #326
Merged
Merged
Conversation
The WiringPlan memo lived on CacheItem, which is per-container, so a REQUEST-scoped Factory rebuilt an identical plan on every child container (measured: 101 WiringPlan.build calls across 100 request containers where 1 is correct), and validate()'s get_dependencies/iter_validation_issues rebuilt it unmemoized on every graph walk. Move the memo onto ProvidersRegistry behind a single plan_for(provider, build) accessor, keyed by provider_id and stamped with the registry version. The registry is shared by a container and every child, so the plan builds once per registry version for the whole tree; distinct roots keep separate memos, so the same Factory registered into two registries with different provider sets wires independently. Invalidation is the version-stamped read-side check ported from CacheItem (snapshot before build), preserving the resolve-vs-add_providers concurrency guarantee; no new lock, GIL-assuming per deferred.md A-1. CacheItem loses wiring_plan/wiring_plan_version, and uncached factories skip fetch_cache_item entirely. Behaviour and public API are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lesnik512
added a commit
that referenced
this pull request
Jul 17, 2026
Cover the 2.28.0->2.29.0 backlog: the single-path compiled resolver (#334) and its measured perf, the wiring-plan memoization (#326), the three correctness fixes (#340 ContextProvider-via-kwargs default, #321 NoneType default/nullability, #320 validate traverses kwargs=), and the closed provider set under an explicit breaking-change heading (per planning/decisions/2026-07-17-custom-providers-retracted.md, the release notes carry that warning, not the docs). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The
WiringPlanmemo lived onCacheItem, which is per-container, so aREQUEST-scopedFactoryrebuilt an identical plan on every child container (measured: 101WiringPlan.buildcalls across 100 request containers where 1 is correct), andvalidate()'sget_dependencies/iter_validation_issuesrebuilt it unmemoized on every graph walk.This moves the memo onto
ProvidersRegistry— the object the plan is actually keyed to — behind a singleplan_for(provider, build)accessor, keyed byprovider_idand stamped with the registry version. Design rationale and the alternatives weighed live in the change bundle:planning/changes/2026-07-15.01-wiring-plan-registry-memo.md.What changes
ProvidersRegistry.plan_for— version-stamped, snapshot-before-build memo shared tree-wide (a container and every child share one registry), so a deeper-scope factory plans once per registry version, not once per child container. Distinct roots keep separate memos, so the sameFactoryin two registries with different provider sets wires independently.Factory._planreplaces_ensure_plan; the resolve path plusget_dependencies/iter_validation_issuesall route through it, sovalidate()warms the memo instead of rebuilding on each walk.CacheItemloseswiring_plan/wiring_plan_version(5 fields → 3); uncached factories skipfetch_cache_itementirely.CacheItem, preserving the resolve-vs-add_providersconcurrency guarantee. No new lock — GIL-assuming, consistent withdeferred.mdA-1 (repointed to the registry location in this PR).Behaviour and public API are unchanged.
Tests
REQUEST-scopedFactoryacross 50 child containers asserts one build per provider (fails at 55 on old code, passes at 2). Replaces the weaker single-container count test.Factory, two registries, different provider sets → wires differently. Verified to fail under a naive version-only shared memo.add_providers-rebuilds-stale-plan tests stay green as invalidation guards.just test-ci(400 passed, 100% coverage),just lint-ci, andjust bench(22 passed) all green.🤖 Generated with Claude Code