Skip to content

perf: drop wasted {} defaults in kwargs.get("headers", ...) across enhanced_handler#159

Merged
justrach merged 1 commit intorelease/1.0.30from
perf/158-enhanced-handler-headers-default
May 10, 2026
Merged

perf: drop wasted {} defaults in kwargs.get("headers", ...) across enhanced_handler#159
justrach merged 1 commit intorelease/1.0.30from
perf/158-enhanced-handler-headers-default

Conversation

@justrach
Copy link
Copy Markdown
Owner

@justrach justrach commented May 10, 2026

Closes #158. Sub-task of #42. Follow-up to #155 (which fixed the same pattern in the fast handlers).

Summary

create_enhanced_handler (the slow / Depends path) had six sites with the same wasted-PyDict_New pattern as #155kwargs.get("headers", {}) where the empty-dict default is either never observed (caller checks truthiness immediately) or paid twice (once for the default arg, again when or {} re-coerces).

line site shape new shape
890 async, parse headers headers_dict = kwargs.get("headers", {}) kwargs.get("headers")
964 async, build _Request headers=kwargs.get("headers", {}) or {}, kwargs.get("headers") or {},
982 async, dep context "headers": kwargs.get("headers", {}), kwargs.get("headers") or {},
1073 sync, parse headers headers_dict = kwargs.get("headers", {}) kwargs.get("headers")
1148 sync, build _Request headers=kwargs.get("headers", {}) or {}, kwargs.get("headers") or {},
1164 sync, dep context (only w/ Depends) "headers": kwargs.get("headers", {}), kwargs.get("headers") or {},

All six are identity-preserving for every observed call shape:

  • The 2 if d: sites (890, 1073) treat None and {} the same — both falsy.
  • The 2 or {} sites (964, 1148) — or {} already coerces None to {}; the inner default was double-allocating.
  • The 2 dep-context sites (982, 1164) — DependencyResolver._resolve_single reads back via headers.items(), so the value must be a dict; switching to kwargs.get(...) or {} keeps that contract while skipping the wasted default-arg {} literal on the headers-present (common) path.

Files / subsystems touched

  • python/turboapi/request_handler.py — 6 sites in create_enhanced_handler (sync + async). Also rolls in a pre-existing isort fix that ruff flagged on the same import block (residue from perf: hoist parse_qs / HTTPException imports out of fast handler closures #148).
  • benchmarks/bench_enhanced_handler_headers_default.py — new isolated microbench targeting the four shapes used in the patch.

No public API change. No dependency change. No uv.lock change.

Red-to-green

Microbench (load-bearing artifact)

.venv/bin/python benchmarks/bench_enhanced_handler_headers_default.py, n=1M × 5 runs, free-threaded 3.14t, M-series macOS:

case                     median_ns     p99_ns
default_no_hdr                36.5      125.0   <- old, no headers
no_default_no_hdr             27.2      125.0   <- new, no headers (-25%)
default_or_w_hdr              38.4      125.0   <- old, with headers + or {}
no_default_or_w_hdr           31.3       84.0   <- new, with headers + or {} (-18%, p99 -33%)

The patched paths drop 7-9 ns / call (-18 to -25 % median). The or {} pair also drops p99 by 33% — the wasted {} literal alloc was the source of variance under GC pressure.

Aggregate per-request savings on the slow / Depends path: roughly 6 × 7-9 ns ≈ 40-50 ns per request through enhanced_handler. Modest in absolute terms but every request through this path benefits, and it's the path that runs whenever a route uses Depends, Form, File, or Request injection.

Targeted tests

PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 .venv/bin/python -m pytest -q \
  tests/test_annotated_depends.py \
  tests/test_request_parsing.py \
  tests/test_post_body_parsing.py \
  tests/test_async_handlers.py

Result: 24 passed in 34.54s. Pre-existing PytestReturnNotNoneWarnings (test_mixed_sync_async, test_async_error_handling) are unchanged by this PR.

Risk

Low. The two dep-context sites (982, 1164) are the only ones that needed care — DependencyResolver._resolve_single does for k, v in headers.items() (and .lower() checks) on context["headers"], which would crash on None. Audited and confirmed: switching to kwargs.get(...) or {} keeps the dict contract, so the consumer sees identical inputs.

The 4 truthiness-checked sites (890, 964, 1073, 1148) are direct counterparts to the merged #155 / #155 pattern — same identity-preserving swap.

Checklist

  • Linked issue: perf: extend #151 — kwargs.get("headers", {}) in 6 more sites in create_enhanced_handler #158 (sub-task of perf: roadmap from current ~140k baseline toward 200k+ req/s #42).
  • Rebased onto current main: yes (branched from main at 13a3709).
  • Generated files / lockfiles changed: no. uv.lock, .zig-cache/, zig-out/ untouched. The zig/zig-pkg/dhi-... build artifact was explicitly NOT staged.
  • Dependency surface changed: no.
  • Bench layer declared: driver-only Python microbench targeting the four kwargs.get("headers", ...) shapes in isolation. Caches: stdlib defaults. Cold/warm: warmed steady-state (20k-iter warmup before each measurement). Number of runs: 5 medians. Machine: local M-series macOS Apple Silicon, free-threaded CPython 3.14.0.
  • Submission matches CONTRIBUTING.md: confirmed.

🤖 Generated with Claude Code


Open in Devin Review

@github-actions
Copy link
Copy Markdown
Contributor

Performance Regression Report

Runner: ci-pr | Duration: 5s per endpoint | Threads: 4 | Connections: 100 | Workers: 24

Endpoint req/s avg latency p99 latency threshold status
GET /health 87,590 0.22ms 0.99ms 80,000 OK
GET / 88,090 0.23ms 1.08ms 80,000 OK
GET /json 83,514 0.22ms 0.67ms 75,000 OK
GET /users/123 85,262 0.21ms 0.73ms 75,000 OK
POST /items 47,244 0.69ms 1.32ms 45,000 OK
GET /status201 77,876 0.24ms 0.90ms 65,000 OK
AVERAGE 78,262 50,000 OK

✅ All endpoints pass regression thresholds

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional findings.

Open in Devin Review

@justrach justrach force-pushed the perf/158-enhanced-handler-headers-default branch from ee42770 to c2fb928 Compare May 10, 2026 12:51
@justrach justrach changed the base branch from main to release/1.0.30 May 10, 2026 12:51
@github-actions
Copy link
Copy Markdown
Contributor

Performance Regression Report

Runner: ci-pr | Duration: 5s per endpoint | Threads: 4 | Connections: 100 | Workers: 24

Endpoint req/s avg latency p99 latency threshold status
GET /health 103,206 0.20ms 0.70ms 80,000 OK
GET / 99,249 0.18ms 0.52ms 80,000 OK
GET /json 93,021 0.20ms 1.02ms 75,000 OK
GET /users/123 93,786 0.18ms 0.84ms 75,000 OK
POST /items 59,578 0.46ms 0.97ms 45,000 OK
GET /status201 92,266 0.20ms 0.78ms 65,000 OK
AVERAGE 90,184 50,000 OK

✅ All endpoints pass regression thresholds

…hanced_handler

Closes #158. Sub-task of #42. Follow-up to #155 (PR for #151).

create_enhanced_handler (the slower / Depends path) had six sites with
the same wasted-PyDict_New pattern that #155 fixed in the fast path:

  4 sites with `if d:` truthiness check immediately after — drop default
  to None (None is also falsy, so identity-preserving):
    * L890 in async enhanced_handler (parse headers)
    * L1073 in sync enhanced_handler (parse headers)
  2 sites already coercing with `or {}` — drop the inner default
  (`or {}` keeps the contract; the inner `{}` was double-allocating):
    * L964 in async enhanced_handler (build _Request)
    * L1148 in sync enhanced_handler (build _Request)
  2 dep-context sites that consumer reads back via `headers.items()` —
  switch to `kwargs.get(...) or {}` to keep the dict contract while
  saving the wasted default-arg `{}` literal on the headers-present
  (common) path:
    * L982 in async enhanced_handler (dep context)
    * L1164 in sync enhanced_handler (dep context — only when handler
      uses Depends/Security)

All six are identity-preserving for every observed call shape. The
DependencyResolver still receives a dict; HeaderParser still gets None
or a dict (both falsy when empty); the _Request constructor still gets
a dict.

Also rolls in a pre-existing isort fix that ruff flagged on the same
import block (residue from #148 — first-party turboapi.exceptions
import was sorted alongside stdlib).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@justrach justrach force-pushed the perf/158-enhanced-handler-headers-default branch from c2fb928 to 0048106 Compare May 10, 2026 12:53
@chatgpt-codex-connector
Copy link
Copy Markdown

💡 Codex Review

if _returns_md or (_returns_md is None and hasattr(result, "model_dump")):
result = result.model_dump()

P1 Badge Preserve tuple returns for model-annotated handlers

When a parameterized handler is annotated as returning a model but uses the supported (body, status) return form, this cached True branch runs before tuple handling and calls model_dump() on the tuple, producing a 500 instead of the intended status response. The zero-arg fast path still checks tuples first, and the old per-response hasattr(result, "model_dump") skipped tuples, so this regresses only these model-annotated non-zero-arg routes (and the async path has the same ordering).

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@justrach justrach merged commit 15bf12d into release/1.0.30 May 10, 2026
1 check passed
@justrach justrach deleted the perf/158-enhanced-handler-headers-default branch May 10, 2026 12:57
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.

perf: extend #151 — kwargs.get("headers", {}) in 6 more sites in create_enhanced_handler

1 participant