Skip to content

perf(context): skip the scope hop when the container is already at scope - #417

Merged
lesnik512 merged 4 commits into
mainfrom
perf/context-hop-fast-path
Aug 3, 2026
Merged

perf(context): skip the scope hop when the container is already at scope#417
lesnik512 merged 4 commits into
mainfrom
perf/context-hop-fast-path

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Why

ContextProvider.fetch_context_value called container.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 Factory closures already use for their own navigation:

if container.scope != self.scope:
    container = container.find_container(self.scope)

find_container opens with if 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_map or 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: _navigate prepends a step, and the calling Factory closure prepends its own. A test now turns that shape red.

One behavioural delta, disclosed: a Container subclass overriding find_container to redirect a same-scope request is no longer consulted by ContextProvider. The four compiled-resolver navigation sites already skip it on same scope, so this narrows an existing inconsistency rather than creating one -- and 2026-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: ContextProvider caches nothing and registers no finalizer.

Non-goals

  • Not part (iii), the compile-time fold, which is where the remaining work is and still needs your ruling on whether a ContextProvider's context_type is fixed at registration.
  • No change to the cross-scope path.

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 _navigate shape.
  • test_same_scope_context_hop_does_not_call_find_container -- fails against the unconditional call.
  • the same test's cross-scope half -- asserts find_container is 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

  • Behaviour changed? architecture/performance.md "Scope navigation" updated in this PR, including why _navigate is not used.
  • Rejected an alternative? _navigate -- recorded in the code comment and the architecture page, where it prevents a regression, rather than as a decision doc.
  • Found real work you are not doing now? Part (iii), already filed and now narrowed to its single gate.
  • just lint-ci and just test-ci pass.

lesnik512 and others added 4 commits August 3, 2026 14:59
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>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@lesnik512
lesnik512 merged commit 8f8611e into main Aug 3, 2026
9 checks passed
@lesnik512
lesnik512 deleted the perf/context-hop-fast-path branch August 3, 2026 12:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant