perf(context): skip the scope hop when the container is already at scope - #417
Merged
Conversation
fetch_context_value called find_container unconditionally, paying a frame to be handed back the container it was given -- the common case, since a request value is read from the request container. An int compare in front skips it, the same shape the compiled Factory closures use for their own navigation. Deliberately not the compiler's _navigate: that prepends a resolution step, and the Factory closure prepends its own, so the caller would appear twice in the breadcrumb. The added test pins the single step and was verified to fail against that shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Structural guard for the int-compare fast path: the timing delta is ~2.5% and sits close to the harness's own drift, so the frame's absence is what the suite asserts. Verified to fail against the unconditional find_container call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the measured -2.5% (well under the -9.2% this item estimated for the narrow variant, because most of it lives in part (iii)), the confirmed _navigate double-prepend trap, and the corrected per-kwarg frame count. The ruling on ContextProvider identity is now the item's only gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…m accuracy Review found the code clean and three inaccuracies in the deferred-item edit. The material one: comparing (ii)'s -2.5% on g9_context (~707 ns) against the -9.2% recorded for the narrow variant (~312-329 ns) is a cross-baseline error, and the conclusion drawn from it -- that most of the remaining win lives in (iii) -- does not follow. In absolute terms (i)+(ii) are ~58 ns, roughly -18% of that original baseline; what (iii) is worth alone was never measured. Also pins the cross-scope branch, which nothing asserted: a cross-scope context hop must still route through find_container, the extension point 2026-08-01-scope-map-inline-declined.md protects. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Benchmark
Details
| Benchmark suite | Current: 13cff5f | Previous: 10e4197 | Ratio |
|---|---|---|---|
benchmarks/test_guard_by_type.py::test_g16_resolve_by_type |
2077711.4573966158 iter/sec (stddev: 2.1249980078700102e-7) |
2664934.4327533874 iter/sec (stddev: 2.0453924587934402e-7) |
1.28 |
benchmarks/test_guard_by_type.py::test_g17_resolve_by_type_large_registry |
1999601.445656358 iter/sec (stddev: 2.373863379780813e-7) |
3397207.3008231083 iter/sec (stddev: 3.515253036642124e-8) |
1.70 |
benchmarks/test_guard_cold.py::test_g8_cold_first_resolve |
26716.670752933253 iter/sec (stddev: 0.000010962074144274622) |
34457.92219890785 iter/sec (stddev: 0.00001250363996556651) |
1.29 |
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[1] |
432.0862155013953 iter/sec (stddev: 0.000042759928394686705) |
558.1100634972859 iter/sec (stddev: 0.00003091000939193654) |
1.29 |
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[2] |
407.7007068402056 iter/sec (stddev: 0.00004506896828983966) |
519.8167188978247 iter/sec (stddev: 0.00002291786749641334) |
1.27 |
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[4] |
376.2678820711221 iter/sec (stddev: 0.000042489248937278186) |
487.5283154134495 iter/sec (stddev: 0.000024275221844047625) |
1.30 |
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[1] |
2429.894260114933 iter/sec (stddev: 0.000025660548844740812) |
3156.219614815241 iter/sec (stddev: 0.000019244990000459958) |
1.30 |
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[2] |
1775.7477166789315 iter/sec (stddev: 0.00030815993041339625) |
2257.523026171822 iter/sec (stddev: 0.0002606590779334005) |
1.27 |
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[4] |
1238.6428961871227 iter/sec (stddev: 0.00003273610664358648) |
1606.7510322547134 iter/sec (stddev: 0.00002783438307573802) |
1.30 |
benchmarks/test_guard_lifecycle.py::test_g6_build_child_container |
646748.6350037265 iter/sec (stddev: 4.4431278682517376e-7) |
822037.5157239352 iter/sec (stddev: 3.1891795312478927e-7) |
1.27 |
benchmarks/test_guard_lifecycle.py::test_g6b_build_child_container_auto_scope |
611851.637878268 iter/sec (stddev: 3.848085251141285e-7) |
771336.8122861808 iter/sec (stddev: 3.1678881771787854e-7) |
1.26 |
benchmarks/test_guard_lifecycle.py::test_g7_request_lifecycle_batch |
2253.7586202660405 iter/sec (stddev: 0.000006744725781675331) |
2993.4020982555853 iter/sec (stddev: 0.000007253408199680305) |
1.33 |
benchmarks/test_guard_lifecycle.py::test_g7c_event_loop_floor_control |
61199.26205182997 iter/sec (stddev: 0.000001253358263982431) |
79708.79137273741 iter/sec (stddev: 0.000001039243454664696) |
1.30 |
benchmarks/test_guard_lifecycle.py::test_g13_teardown_at_scale |
44127.985010695586 iter/sec (stddev: 0.0000016942160711884525) |
56257.952885391795 iter/sec (stddev: 0.0000013465812775314434) |
1.27 |
benchmarks/test_guard_resolve.py::test_g1_transient_resolve |
1346488.0803737435 iter/sec (stddev: 3.597540529992827e-7) |
1747734.83507298 iter/sec (stddev: 2.8841851032267993e-7) |
1.30 |
benchmarks/test_guard_resolve.py::test_g2_cached_resolve |
3331353.6412953306 iter/sec (stddev: 4.074362652860069e-8) |
4357961.233786322 iter/sec (stddev: 2.6410877546784392e-8) |
1.31 |
benchmarks/test_guard_resolve.py::test_g3_deep_chain |
521972.4370298937 iter/sec (stddev: 5.095964891553569e-7) |
673594.2354822385 iter/sec (stddev: 3.4804824265902755e-7) |
1.29 |
benchmarks/test_guard_resolve.py::test_g4_wide_resolve |
317602.0034171171 iter/sec (stddev: 6.266177883949537e-7) |
417391.54305801156 iter/sec (stddev: 4.911643485201084e-7) |
1.31 |
benchmarks/test_guard_resolve.py::test_g5_cross_scope |
1173117.763560255 iter/sec (stddev: 4.5409683756275385e-7) |
1508296.1372473787 iter/sec (stddev: 4.283170787265259e-7) |
1.29 |
benchmarks/test_guard_resolve.py::test_g9_context_resolve |
665163.6238920107 iter/sec (stddev: 6.573336375409231e-7) |
820287.3021842263 iter/sec (stddev: 3.6388032293586744e-7) |
1.23 |
benchmarks/test_guard_resolve.py::test_g12_override_active_resolve |
395034.2638627969 iter/sec (stddev: 3.795450999132161e-7) |
524806.0302587249 iter/sec (stddev: 2.423278963003047e-7) |
1.33 |
benchmarks/test_guard_validate.py::test_g10_validate_deep_chain |
31006.58923091833 iter/sec (stddev: 0.0000023807313613565124) |
39734.771986512744 iter/sec (stddev: 0.0000018412134968865806) |
1.28 |
benchmarks/test_guard_validate.py::test_g11_validate_wide |
18335.41360822927 iter/sec (stddev: 0.000005820845150404938) |
23576.896559038214 iter/sec (stddev: 0.0000026237038454077014) |
1.29 |
This comment was automatically generated by workflow using github-action-benchmark.
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.
Why
ContextProvider.fetch_context_valuecalledcontainer.find_container(self.scope)unconditionally. In the common case -- a request-scoped value read from the request container -- that is a Python frame spent to be handed back the container it was given. It is on the path every framework integration takes for its per-request values.This is part (ii) of
planning/deferred/2026-08-01-context-kwarg-inline.md. It needed no ruling from you; part (iii) still does.Design
An int compare in front of the hop, matching the shape the compiled
Factoryclosures already use for their own navigation:find_containeropens withif scope == self.scope: return self, so the guard is the negation of its own first test and the!=branch reaches it in exactly the state where it would have fallen through to_scope_mapor raised. Both error paths are preserved verbatim.Deliberately not the compiler's
_navigate. The deferred item warned that the obvious_navigate-based implementation double-prepends the resolution-step breadcrumb while CI stays green. That is exactly right:_navigateprepends a step, and the callingFactoryclosure prepends its own. A test now turns that shape red.One behavioural delta, disclosed: a
Containersubclass overridingfind_containerto redirect a same-scope request is no longer consulted byContextProvider. The four compiled-resolver navigation sites already skip it on same scope, so this narrows an existing inconsistency rather than creating one -- and2026-08-01-scope-map-inline-declined.md's objection does not apply here, because that decision protects the cross-scope hit path (untouched, and now pinned by a test) and its lifecycle consequence cannot bite:ContextProvidercaches nothing and registers no finalizer.Non-goals
ContextProvider'scontext_typeis fixed at registration.Verification
just test-ci: 468 passed, 100% line coverage.just lint-ci: clean.Three tests, each verified to fail against the shape it guards:
test_scope_error_through_a_context_kwarg_carries_one_breadcrumb_step-- fails (assert 2 == 1) against the_navigateshape.test_same_scope_context_hop_does_not_call_find_container-- fails against the unconditional call.find_containeris still called when scopes differ.Timing, stated honestly: four A/B/A runs on the repo's blessed harness (
.superpowers/spike/ab_run.sh main HEAD g9_context) gave -3.05%, -1.29%, -3.98%, -1.94% against baseline drift of 0.4-1.7%. Median ~-2.5%, ~17 ns of ~707 ns. All four negative, so the direction is real, but it sits close enough to the harness's own drift that the structural claim -- the frame is gone -- is what the suite asserts, not the percentage.That -2.5% is not comparable to the -9.2% the deferred item records for its narrow variant: different benchmark, ~707 ns versus ~312-329 ns. In absolute terms (i)+(ii) come to ~58 ns. The item has been corrected accordingly -- an earlier draft of mine drew a cross-baseline conclusion that does not follow.
Before merging
architecture/performance.md"Scope navigation" updated in this PR, including why_navigateis not used._navigate-- recorded in the code comment and the architecture page, where it prevents a regression, rather than as a decision doc.just lint-ciandjust test-cipass.