# Intermediate adaptive-batch checkpoint

This round starts from release extension
`09140253d30af272785eadc1a1d2613a6e2066653a63dbe09a552dcc9d762705`,
archived with its source in `round4-before-source.tar.gz`. The qualified intermediate
wheel contains extension
`18449600b0e0984867e6afa9ebca1818c06daf8094d6e3425b3ab12af2334e83`.

The implementation remains an ASGI-only HTTP/1 Linux prototype. This study is
performance and regression evidence, not complete protocol or production
qualification. The intermediate same-session comparisons are
[the saturation/control report](../results/latency-round4-report.txt) and
[the separate fixed-rate report](../results/latency-round4-rate20k-report.txt).

## Intermediate implementation

The fixed sixteen-event reactor limit is replaced with a bounded adaptive limit.
It starts at sixteen events, then estimates a subsequent batch from the preceding
callback's elapsed time, targeting about 500 microseconds. Its cap is one quarter
of the current connection count, clamped between sixteen and 128; the adaptive
limit itself is clamped to at least four events. Small connection sets therefore
retain short batches, while larger sets can amortize native poller callbacks.
This is an advisory feedback policy, not a wall-time guarantee: arbitrary
application work, accepting clients and servicing a connection can exceed the
target. Every event returned by epoll is processed.

Eager application starts remain bounded at 128 per native callback. Buffered
pipelining pumps sixteen iterations before scheduling a continuation. Each
request still has its own Task, context and 128-ready-operation fairness quota.
Custom task factories continue to be checked on each dispatch. There are no
application-specific dispatch rules.

Request targets use ranges into the already-owned request-head block, removing
a separate native allocation. Absolute-form authority normalization still
replaces conflicting Host, and an absent path still becomes `/`. The strict CRLF
scan checks line endings and embedded CR without removing any validation. The
three status digits serialize directly after the existing status/version checks.
Fixed HTTP scope values and task-constructor keyword names are interned once.
Mutable request mappings and header lists remain separate.

The retained changes add 35 native lines (2,137 total), keep three direct Rust
dependencies and add no unsafe operations. Python runtime code is unchanged.
Writes, streaming flushes, disconnect errors and output backpressure retain the
previous behavior. There is no response cache, application GC tuning, validation
bypass or alternative request/channel ownership system.

## Fresh profiles

`round4-retained-native` and `round4-peregrine-native` contain ordinary delayed
user-cycle profiles, without Python perf trampolines. Each validates the exact
200/17-byte FastAPI JSON response before load and records source/binary identity.
The reports record zero lost samples. The interpreter evaluator accounts for
48.39% of retained Skirr user-cycle samples and 49.19% of Peregrine samples.
Allocation, frame cleanup and Python object/API work remain visible. These
separately normalized user-space percentages do not include kernel execution
and cannot be interpreted as throughput headroom or entirely application work.

`round4-retained-python` collected 5,656 samples; `round4-peregrine-python`
collected 5,951, both with zero reported sampling errors. Their summaries filter
for the actual FastAPI request stack, excluding imports and supervisor frames.
The most sampled paths include dependency handling, routing, JSON encoding,
response headers, telemetry checks and the response-send wrapper. Of filtered
FastAPI samples, Skirr's last Python frame is Starlette's `_send` in 656/5,137
samples; Peregrine's is `_send` in 141/4,528. This includes native I/O and other
work below that frame; it is not a measurement of the wrapper's statements alone.
Peregrine's deferred response writes also move work outside that application
frame, so this difference does not predict an equivalent removable CPU cost.

The comparison harness now records process-tree user and system CPU seconds
separately. Normalize these by completed requests to compare CPU demand; neither
the latency histogram nor total CPU percentage alone separates kernel/socket
work from interpreter work. Per-run clock samples and raw counters are retained.

## Rejected experiments

All trials remain in `bench/results` with `round4-*` names; the final report
uses only `latency-round4-*` and the exact qualified wheel.

- A callback deadline that stopped partway through returned epoll events caused
  socket timeouts and extreme tails. The trial was stopped and rejected. The
  replacement requests a smaller next batch and always services returned events.
  A proposed ordinary socket unit case did not reproduce those tail stalls; it
  was removed instead of claiming it proved the performance regression fixed.
- Uncapped adaptive batches improved some high-concurrency results but increased
  small-set tiny-response tails. The connection-aware cap is retained instead.
- Exposing `send` as a bound native method and moving Task creation through a
  Python helper passed the protocol tests but reduced tiny-response throughput
  without closing the FastAPI gap. Both are removed; patches are retained.
- A bounded immutable header-pair cache did not provide a worthwhile gain and
  was removed. No additional request values remain cached.
- A direct Sender vectorcall entry retained PyO3 ownership/thread checks, but
  its repeated comparison did not justify 80 extra integration lines. The whole
  module and its unsafe type-slot changes were removed. This differs from the
  earlier registry-based channel experiment; neither is retained.
- Consuming Task.result() before checking readiness saves a successful-path
  query but turns ordinary waiting requests into exception-driven control flow.
  It did not show a clear gain and was removed; pending requests keep the prior
  readiness/result behavior.

## Profiling limitations and excluded artifacts

The optional CPython perf-JIT investigation requires `perf record -k 1` before
`perf inject --jit`. `round4-invalid-jit-clock` omitted that clock and failed
injection; it is excluded. Some later JIT postprocessing used an overwritten
intermediate native binary, invalidating native unwinding. Those outputs are
explicitly named `invalid-after-binary-rebuild-*` and are excluded.

Even freshly processed JIT profiles have very different named-Python-frame
coverage for the eager and ordinary Task paths. They are not used for comparative
function-percentage claims. The retained native profiles and Python-only worker
profiles above are the evidence used here. Zero lost perf samples does not imply
complete call chains. Perf instrumentation throughput is never benchmark data.

## Qualification

The installed wheel passes 61 socket/ASGI cases under standard asyncio and
uvloop with Python's debug allocator. Strict Clippy and three native framing
tests pass. A separate Rust AddressSanitizer build passes the same 61 cases on
both loops, with leak detection disabled for the uninstrumented embedding host.
See `asan-round4-check.txt`; this is not leak or complete fuzz qualification.

The expanded socket tables cover absolute-form missing and escaped paths, query
separation, mixed line endings and repeated CR. The saved starting binary also
passes all 61 cases. These additions protect preserved protocol behavior during
refactoring; they are not claimed as pre-existing correctness bugs. Existing
coverage exercises ready-receive and pipeline fairness, Task failure/cancellation,
custom factories/context isolation, streaming, reset/error delivery, partial
writes and concurrent waiting applications. No test-only runtime hooks were added.

Final performance findings are appended after all scheduled runs finish; individual
favorable trial runs are not selected as final evidence.