Skip to content

Faster cross-thread wakes, asyncio turn shape and native getaddrinfo behind threaded servers - #122

Merged
egorsmkv merged 7 commits into
RustedBytes:masterfrom
CBoYXD:perf/native-resolver-and-loop-fast-paths
Oct 19, 2026
Merged

egorsmkv merged 7 commits into
RustedBytes:masterfrom
CBoYXD:perf/native-resolver-and-loop-fast-paths

Conversation

@CBoYXD

@CBoYXD CBoYXD commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Behind Granian, a Litestar web service with an async SQLAlchemy/asyncpg pool
ran up to 44% slower on rsloop 0.1.64 than on uvloop at c16. The cause was not raw loop speed: the
way the loop woke up for call_soon_threadsafe and how much work it ran per
turn made request starts and database completions arrive in bursts, and a
small SQLAlchemy pool then kept discarding and reopening connections (33–55 new
Postgres sessions per 1k requests vs 3–4 on uvloop, each one a SCRAM login).
v0.1.65 still does this: in the run below it is 17–35% behind uvloop at c16,
with 27–53 new sessions per 1k. This PR fixes that and one lost-wake race.
Each commit stands alone and can be reviewed or dropped separately:

  1. fix: fence the block_on park against a wake that races it. The waker
    stores the ready flag and then loads waiting, while the park stores
    waiting and then loads the ready flag. With only release/acquire both loads
    can see the old value, so the coordination thread (which parks with no
    deadline) can sleep through a wake for good. Callbacks queued before a
    run_forever then never reach the run. It shows as a hang in about 1 in
    300–450 runs of the Task wakeup lost (or SIGSEGV) when a future is resolved inside a greenlet after the loop is re-entered from a deeper C stack — SQLAlchemy asyncio hangs under Granian #115 repro under Docker. Adds a SeqCst fence on both
    sides, plus two stress tests (100k wakes each). On master without the fences
    the block_on test stalls in 2 of 3 runs.
  2. perf: enqueue off-thread call_soon_threadsafe directly into the active
    run.
    One wake per handle instead of a hop through the coordination thread.
    A counter of handles still on the command channel keeps each producer's FIFO
    order across run start and end.
  3. perf: park instead of spinning for cross-thread wakes by default.
    RSLOOP_WAKE_SPIN_US now defaults to 0 instead of 50. Most cross-thread
    wakes come from Python threads that still hold the GIL when they flip the
    flag, so a spinning loop thread queues for the GIL behind them, paying up to
    the 5 ms switch interval. This is the root cause of the burst/churn pattern.
    The spin stays available as an opt-in.
  4. perf: run one callback generation per turn and poll I/O without the
    GIL.
    This is asyncio's _run_once shape. A turn runs what was ready when
    it started, then does a non-blocking reactor poll with the GIL released, as
    uvloop does. It no longer runs to quiescence (refill every 64, up to 1024).
    call_soon and call_soon_threadsafe callbacks are merged in scheduling
    order: each off-thread item carries the loop thread's enqueue count. A
    queued stop() still takes effect in the turn that requested it.
  5. perf: resolve host names natively, without the GIL. On Unix, for an
    exact loop with the standard socket.getaddrinfo and plain arguments,
    getaddrinfo(3) runs on the blocking pool without the GIL. The loop thread
    raises the audit event and builds the same rows and errors CPython would.
    At most min(32, CPUs + 4) lookups run at once, like asyncio's default
    executor. Anything unusual (a subclass, a custom default executor, a
    replaced or functools.wraps-wrapped socket.getaddrinfo, IDNA, odd port
    types) keeps the executor path.
  6. perf: benchmark tools and analysis. perf/ANALYSIS.md and
    perf/bench/. This commit is optional for you to take (see below).

Measurements

All figures below were measured before this branch was rebased onto master
f045b7b (on the v0.1.65 base, 37a3d3f); they were not re-run after the rebase.

All arms are release wheels (maturin build --release --locked), each
unpacked into its own directory and put first on PYTHONPATH, so every arm
runs on the same interpreter. "v0.1.65" is 37a3d3f.

Pool repro (perf/bench/pool_churn.py). A SQLAlchemy async engine
(pool_size=4, max_overflow=2, asyncpg, Postgres) on one loop, with 8 driver
threads posting requests through call_soon_threadsafe as Granian does,
2 queries per request. Docker epoll, --cpus 1, 3 runs per arm. Other builds
were running on the machine (load1 13–25); the connection count does not
depend on that load.

arm req/s median (min–max) CPU µs/req new connections per 1k post → task start µs
uvloop 0.22.1 1,747 (1,627–2,260) 552 0.0 (0.0–0.1) 254
rsloop v0.1.65 439 (272–479) 2,152 65.9 (59.9–74.2) 2,359
this branch 1,427 (1,412–2,135) 652 1.4 (0.8–1.8) 395

So v0.1.65 does not match uvloop on this shape yet. Every new connection is a
Postgres SCRAM login, and that login is where the service's extra CPU went.

Microbenchmarks (perf/bench/micro.py). Docker epoll, --cpus 2, 3
rounds with the arm order rotated, median CPU µs per op. Measured under load
(load1 11–21), so read a few percent as noise.

bench uvloop v0.1.65 branch
cross-thread call_soon_threadsafe ping-pong 10.55 15.32 9.89
thread → task → thread-safe queue ping-pong 11.26 13.09 9.27
Granian-shaped request (thread post, 2 DB round trips, reply) 64.05 62.39 51.82
self-rescheduling call_soon chain 0.79 0.20 0.51
asyncio.timeout around a resolved await 2.61 1.52 2.21
TCP ping-pong, 1 connection 11.70 17.68 10.84
TCP ping-pong, 8 connections 8.48 7.53 8.00
getaddrinfo("localhost") 47.76 99.47 33.67
16 concurrent getaddrinfo 36.53 108.60 39.73
run_in_executor(None, int) 22.84 37.51 23.34
open_connection("localhost", ...) + round trip + close 269.9 385.7 284.6

Your benches/compare_event_loops.py, pinned to one CPU, 1,000,000
callbacks / 200,000 tasks / 20,000 TCP round trips. Median seconds of 5 runs,
two interleaved passes per build:

workload v0.1.65 branch
callbacks 0.171 / 0.168 0.177 / 0.163
tasks 0.267 / 0.289 0.275 / 0.262
tcp_streams 0.202 / 0.206 0.208 / 0.209

These are level.

getaddrinfo burst. 1,000 concurrent lookups of localhost on a
20-CPU host. v0.1.65 grows smol's pool to its 500-thread limit and takes
114–123 ms. The branch uses 24 threads (cap min(32, 20 + 4)) and takes
29–30 ms.

Full service against v0.1.65 (Granian, 2 workers, --cpus 2, oha
c16/c64, 15 s warm-up and 60 s per row, 3 rounds with the arm order
rotated). The endpoints are
authenticated JSON reads, named by workload: two session reads (light
authenticated GETs on different routes), an aggregate read (a GET with a DB
aggregate), a count read (a GET with a small DB count) and a list read (a GET
with a small DB list). Each cell is the median of the 3 rounds, with the
min–max over the rounds in brackets. "ahead" means the branch's range lies
entirely above the other arm's, and "overlap" means the two ranges overlap.
CPU-ms is the API container's cgroup CPU per 1k requests, and new PG sessions
are counted from pg_stat_database.

endpoint c uvloop rps v0.1.65 rps branch rps branch vs v0.1.65 branch vs uvloop CPU-ms/1k (uvloop / v0.1.65 / branch) new PG sessions/1k (uvloop / v0.1.65 / branch)
session read 1 16 1,499 (1,420–1,703) 1,200 (997–1,374) 1,424 (1,316–1,650) +18.7% (overlap) −5.0% (overlap) 1,331 / 1,659 / 1,401 3.4 / 27.7 / 3.3
session read 1 64 1,443 (1,194–1,476) 1,101 (1,033–1,120) 1,628 (1,469–1,654) +47.8% (ahead) +12.8% (overlap) 1,385 / 1,803 / 1,229 3.0 / 27.9 / 4.8
aggregate read 16 949 (821–1,019) 792 (789–822) 930 (917–968) +17.5% (ahead) −1.9% (overlap) 2,094 / 2,501 / 2,142 5.4 / 28.0 / 7.5
aggregate read 64 814 (786–971) 913 (796–923) 939 (909–966) +2.8% (overlap) +15.4% (overlap) 2,438 / 2,175 / 2,118 4.6 / 11.0 / 5.7
session read 2 16 1,650 (1,595–1,723) 1,078 (1,025–1,083) 1,779 (1,700–1,970) +64.9% (ahead) +7.8% (overlap) 1,209 / 1,854 / 1,124 5.1 / 53.4 / 2.8
session read 2 64 1,765 (1,717–1,823) 1,818 (1,787–1,952) 1,925 (1,726–1,927) +5.9% (overlap) +9.1% (overlap) 1,134 / 1,101 / 1,039 2.5 / 2.3 / 2.3
count read 16 1,335 (1,319–1,605) 997 (977–1,044) 1,401 (1,400–1,507) +40.4% (ahead) +5.0% (overlap) 1,497 / 2,004 / 1,427 10.7 / 33.4 / 3.7
count read 64 1,443 (1,224–1,598) 1,503 (1,464–1,525) 1,450 (1,291–1,509) −3.5% (overlap) +0.5% (overlap) 1,387 / 1,331 / 1,380 3.0 / 2.4 / 2.5
list read 16 1,263 (1,162–1,280) 922 (915–1,058) 1,194 (1,091–1,228) +29.5% (ahead) −5.5% (overlap) 1,582 / 2,165 / 1,664 8.7 / 26.9 / 4.0
list read 64 1,287 (1,152–1,304) 1,210 (1,079–1,246) 1,175 (1,137–1,257) −2.9% (overlap) −8.7% (overlap) 1,555 / 1,654 / 1,703 3.4 / 3.6 / 3.8

This was measured while other builds were running on the 20-CPU host
(load1 3.1–9.7, 1.5–5.9 cores of CPU outside the benchmark, P-cores at
2.1–4.5 GHz), so the API container was pinned to two physical P-cores
(CPUs 0–3), oha to CPUs 4–7 and Postgres/Redis/NATS to CPUs 8–11, and
rows above 6 foreign cores were measured again; even so, 77 of the 90 rows,
spread over all three rounds and all arms, were above the 2.5-core
quiet-machine threshold, so every round counts as contaminated and only the
"ahead" cells should be read as differences.

At c16, v0.1.65 still opens 27–53 new Postgres sessions per 1k requests,
against 3–11 on uvloop and 3–8 on the branch. There the branch is ahead of
v0.1.65 on four of the five rows (+17.5% to +64.9%), and session read 1
(+18.7%) overlaps. The branch also uses 14–39% less CPU per request than
v0.1.65, and it is within noise of uvloop on all five rows. At c64, the branch
is ahead of v0.1.65 on session read 1 (+47.8%, where v0.1.65 still
churns 28 sessions per 1k). The other four c64 rows are within noise of
v0.1.65 (−3.5% to +5.9%). No row has the branch outside noise of uvloop
(−8.7% to +15.4%).

On session read 1, contention also widens v0.1.65's deficit. One
quiet-gated, unpinned round of session read 1 and the aggregate read on the
same three wheels, taken the
same morning before the builds started (every row at or below 2.5 foreign
cores), gave these rps for uvloop / v0.1.65 / branch:

endpoint c=16 c=64
session read 1 1,700 / 1,644 / 1,634 1,402 / 1,608 / 1,883
aggregate read 999 / 831 / 1,037 894 / 943 / 1,150

New PG sessions per 1k in that round were 12.7 for v0.1.65 against 1.7 for
the branch on session read 1 at c16, and 33.8 against 10.1 on the aggregate
read at c16. So on a quieter machine, v0.1.65 is level on session read 1 at
c16 and the gap there is smaller than the table shows. The c64 rows and the
aggregate read at c16 point the same way in both runs.

Full service on the v0.1.64 base (same harness, earlier quiet run of the
same changes):

endpoint c uvloop rsloop 0.1.64 these changes new PG sessions/1k (uvloop / 0.1.64 / changes)
session read 1 16 1,787 1,636 1,799 2.9 / 17.9 / 2.9
session read 1 64 1,668 1,525 1,827 2.7 / 17.0 / 5.1
aggregate read 16 1,069 940 1,070 6.5 / 33.5 / 8.1
session read 2 16 2,208 1,172 2,222 2.9 / 55.7 / 1.4
count read 16 1,771 1,138 1,689 3.2 / 46.3 / 2.8
list read 16 1,349 1,057 1,445 9.2 / 40.1 / 4.0
session read 2 64 2,190 2,348 2,172 1.4 / 2.7 / 2.0

The measured stack: Litestar 2.23.0 on Granian 2.7.5 (2 workers),
SQLAlchemy 2.0.50 (async engine, QueuePool) over asyncpg 0.31.0 to Postgres,
redis-py 5.3.1 and nats-py 2.15.0, with uvloop 0.22.1 as the reference loop and
oha as the load generator. What each dependency showed:

  • Granian hands every request to the loop with call_soon_threadsafe from
    its own threads, which still hold the GIL when the wake flag flips. That is
    the wake pattern behind the spin problem (commit 3).
  • SQLAlchemy's QueuePool + asyncpg turned the resulting bursts into
    connection churn. Every new asyncpg connection is a SCRAM login
    (_generate_salted_password, libcrypto), and that is where the extra CPU
    went (commits 3 and 4).
  • nats-py and asyncpg connect by host name, so every connect goes through
    loop.getaddrinfo (a health probe that opens a NATS connection per request
    spent ~250 µs of CPU on it on v0.1.64; commit 5).
  • Litestar and redis-py: no effect of their own was isolated; every arm ran
    the same application code.

perf/ANALYSIS.md has the method, the profiles and the rest of the rows.

Things you may want to change or drop

  • Chained call_soon / timer cost (commit 4). A self-rescheduling
    call_soon chain goes from 0.20 to 0.51 µs per step, and a cancelled timer
    from 1.52 to 2.21 µs. Both are still below uvloop (0.79 and 2.61), and your
    compare_event_loops.py callbacks workload is level. The turn shape is the
    point of the change: a self-rescheduling chain now yields to I/O every step,
    as on asyncio and uvloop. Without it, a call_soon chain held back a readable
    socket for 513 callbacks in the new test. If you prefer the old drain, commits
    1–3 and 5 still apply without it, but commit 4 is what fixed the session read 1
    and aggregate read c64 rows.
  • Spin default (commit 3). Workloads whose wakes come from GIL-free threads
    (rsloop's own reader workers) can still opt in with RSLOOP_WAKE_SPIN_US=50.
    On the v0.1.64 base the spin let rsloop beat uvloop by 4–7% on the
    saturated c64 rows of session read 2, count read and list read, while costing 22–47% at c16. Against v0.1.65, which
    still spins by default, those c64 rows in the run above are within noise
    of the branch (−3.5% to +5.9%).
  • perf/ (commit 6, benchmark tooling) is optional and can be dropped.
    Commits 1–5 do not depend on it. The harness needs a private application
    stack, so it documents the method rather than being runnable as-is.
    micro.py and pool_churn.py run standalone. Drop the commit if you would
    rather keep only benches/.
  • Fork note (not in this PR). smol's blocking pool keeps its idle count in
    a static, so blocking::run in a child forked within 500 ms of the parent's
    last blocking job waits for a thread that does not exist (a small Rust test
    reproduces it). In Python, however, run_in_executor and rsloop.run already
    hang in a child forked after the parent ran any rsloop loop, on v0.1.64,
    v0.1.65 and this branch alike. So this PR does not try to fix fork support.
  • Blocking pool. An earlier version of this work replaced the
    thread-per-call blocking::run with its own cached pool. v0.1.65 already
    moved it to smol::unblock, so that part was dropped. Only the bounded
    lookup slots remain, on top of smol::unblock.

Tests added

  • Rust: vibeio::executor::tests::{block_on,spawned_task}_never_misses_a_wake_racing_with_the_park,
    engine::loop_core::generation_order_tests (3), blocking::tests (bounded
    slots: cap, blocked bounded jobs don't delay others, a panicking job passes
    its slot on), resolver::tests (glibc IPv6 text, IDNA fast path, cap formula
    vs asyncio's, EAI_SYSTEM vs PyErr_SetFromErrno).
  • Python: tests/test_fast_callbacks.py (producer order across run restarts,
    call_soon / call_soon_threadsafe ordering, 3 tests against asyncio and
    rsloop), tests/test_local_sockets.py::test_tcp_data_is_delivered_between_call_soon_generations,
    tests/test_native_addrinfo.py (25 argument shapes vs socket.getaddrinfo,
    concurrency, cancellation, audit event, a functools.wraps wrapper, the
    thread cap).

Verification

A cross-thread wake stores the root future's ready flag (or pushes the
task onto the remote queue) and then loads the runtime's `waiting` flag;
the park stores `waiting` and then loads ready. With release/acquire
only, both stores can still sit in their store buffers when the loads
run, so the waker skips the interrupt and the park sleeps through the
wake. The loop thread recovers at its next park deadline, but the
coordination thread parks with no timer and never wakes again:
callbacks queued before a run_forever (a run_until_complete task's
first step) are then never forwarded while loop-local work keeps
running. The greenlet repro of RustedBytes#115 hung this way in about 1 of 300-450
runs under Docker (epoll), on v0.1.64 as well.

A SeqCst fence now separates store and load on the park side and on
both notifiers (root waker and remote task queue). Same-thread wakes
skip it. Two stress tests ping-pong 100k wakes against the park on the
mio driver and fail when progress stops for 5 s; without the fences the
root-waker test stalls in 2 of 3 runs.
…e run

Every off-thread handle used to travel through the command channel to
the coordination thread, which then pushed it into the run's queue and
woke the loop thread: two thread wakeups per handle where one is enough.
Behind a server that resumes requests with call_soon_threadsafe from its
own I/O threads (Granian), that is two wakeups per request start.

While a run is active and no earlier handle is still on the channel, the
handle now joins the run's pending queue directly (one wake). A counter
of handles still on the channel keeps each producer's order: once one is
queued there (before EnterRun, between runs), later handles follow it.
finish_run now puts the run's queue (older work) ahead of commands
received since the last dispatch.

Microbenchmark on the v0.1.64 base (Docker epoll, --cpus 2): cross-thread
ping-pong CPU 11.4 -> 7.8 us/op, task ping-pong 10.4 -> 6.9 us/op (uvloop
5.9 / 6.9).
The loop thread spun for up to 50 us, with the GIL released, waiting for
a cross-thread wake before parking. Behind Granian those wakes come from
Python threads (call_soon_threadsafe in its future watcher) that still
hold the GIL when the flag flips, so the spinning loop thread queued for
the GIL behind them, paying up to the 5 ms switch interval per reattach.
Request starts and completions then reached the loop in bursts. With a
small SQLAlchemy pool (4 + 2 per worker at 8 concurrent requests) the
pool kept discarding returned connections into a full idle queue and
opening new ones: 33-55 new Postgres sessions per 1k requests vs 3-4 on
uvloop, the whole CPU-per-request gap at c16.

RSLOOP_WAKE_SPIN_US now defaults to 0 (park at once) and remains an
opt-in for workloads whose wakes come from GIL-free threads.

Microbenchmarks on the v0.1.64 base (Docker epoll, --cpus 2, medians of
3): Granian-shaped
cross-thread request CPU 54.6 -> 41.1 us/req (uvloop 50.0), cross-thread
ping-pong 14.1 -> 6.5 us/op (uvloop 7.6), and a 4 + 2 pool churn repro
598 -> 2,593 req/s with 61.3 -> 0.2 connects per 1k (uvloop 2,166, 0.1).
v0.1.65 keeps the spin: the same repro opens 60-74 connections per 1k
there.
run_forever drained each turn to quiescence: callbacks scheduled during
the turn joined it (every 64 items, up to 1024), and the reactor was
polled only once nothing was left. Under load, database responses piled
up between polls and completed in one burst, so more connections came
back to a SQLAlchemy pool in one turn than its idle queue holds before
any woken waiter ran; the overflow was closed and reopened (a Litestar
API at c64: ~20 new Postgres sessions per 1k requests vs 0.1-3.7 on
uvloop, ~690 full-queue check-ins in 20 s).

A turn now runs the callbacks queued when it started, as asyncio's
_run_once does; newer ones wait for the next turn, after another
non-blocking poll. That poll runs with the GIL released, as uvloop's
does: back-to-back turns would otherwise hold the GIL for the switch
interval while call_soon_threadsafe producers wait. A stop() whose Stop
item is still queued keeps the turn going until it runs, so stop keeps
taking effect in the turn that requested it.

The loop thread schedules into its own lock-free queue and other threads
into the run's pending queue, so a generation has to be merged from
both. Off-thread items now carry the loop thread's enqueue count, read
when they are queued, and the drain merges them behind exactly the local
items scheduled before them: call_soon and call_soon_threadsafe
callbacks run in the order they were scheduled, as on asyncio (before,
50 alternating call_soon / thread-posted call_soon_threadsafe calls from
one callback did not interleave in call order). Marks are stored once
per change, the loop thread keeps a single-writer counter, and work
queued before a run goes ahead of the run's own.

On the v0.1.64 base, the same API at c64: 1,618 -> 1,936 rps (1.3-3.0
new sessions per 1k; uvloop 1,746). Microbenchmark cost against v0.1.65:
a self-rescheduling call_soon chain 0.20 -> 0.51 us/op and a cancelled
timer 1.52 -> 2.21 us/op (uvloop 0.79 and 2.61), now one turn per step
as in asyncio and uvloop; benches/compare_event_loops.py is level.
New tests: a call_soon chain no longer delays a readable protocol by
hundreds of callbacks (513 before, 1 after), and three ordering tests
compare call_soon / call_soon_threadsafe order with asyncio.
loop.getaddrinfo() of a host name goes through run_in_executor: a
blocking worker takes the GIL to call socket.getaddrinfo and takes it
again after the lookup. On a busy loop each acquisition waits for the
loop thread to drop the GIL. uvloop resolves in libuv's pool without
it. Services that connect by name (a NATS or Postgres client per new
connection) pay this on every connect.

- resolver.rs: on Unix, an exact loop without a custom default executor,
  with the standard library's socket.getaddrinfo installed and plain
  host and port arguments (an ASCII name the IDNA codec passes unchanged
  or bytes; an int port in range, str or bytes), calls getaddrinfo(3) on
  the blocking pool without the GIL. The loop thread raises the
  socket.getaddrinfo audit event through PySys_Audit, as _socket does (a
  replaced sys.audit skips no hook), and builds the same rows (enum members,
  glibc's inet_ntop text) or the same gaierror / OSError (EAI_SYSTEM as
  PyErr_SetFromErrno raises it, errno 0 included). Every other shape
  keeps the executor path.
- A replaced or wrapped socket.getaddrinfo still receives every lookup,
  as on asyncio: the standard function is recognised by what a
  functools.wraps wrapper cannot copy (its globals are the socket
  module's namespace, its code is named getaddrinfo, it has no
  __wrapped__). A replaced _socket.getaddrinfo, the delegate it calls,
  keeps the executor path too. Both are checked again on every lookup.
- blocking.rs: at most min(32, CPUs + 4) lookups run at once, the size
  of asyncio's default executor (CPUs from the affinity mask, as
  os.process_cpu_count counts them); further lookups wait in order, so a
  burst of 1,000 lookups no longer starts hundreds of threads.
  run_in_executor jobs never wait for a lookup slot.
- engine: a LoopWork ready item runs the loop-thread half of such work,
  so the worker never takes the GIL.

Tests: tests/test_native_addrinfo.py compares 25 argument shapes with
socket.getaddrinfo (rows, enum types and errors, including the shapes
left to CPython), 64 concurrent lookups, cancelled lookups, a lookup
created before run_until_complete, the audit event (also with sys.audit
replaced), a functools.wraps wrapper installed before the first lookup,
a replaced _socket.getaddrinfo before and after native lookups, and the
thread cap under a burst. Rust tests cover the bounded slots, glibc's IPv6 text, the IDNA
fast path, the cap formula and the EAI_SYSTEM mapping.
perf/ANALYSIS.md records how the gap behind Granian was found (per-thread
profiles, perf and strace, a standalone SQLAlchemy pool repro, a pool
trace inside the service), what each commit of this series changes and
its tests, and microbenchmark and pool-repro measurements against v0.1.65
and uvloop, the full-service comparison against v0.1.65 (not a quiet
run, so only the "ahead" cells are differences) and the quiet-gated run
on the v0.1.64 base.

perf/bench/ holds the tools those measurements used:
- harness.py: the full-service harness (quiet-gated, interleaved and
  rotated arms, CPU and new Postgres sessions per 1k requests); it needs
  an application stack, described by RSPERF_* variables, which also name
  the endpoints and test-account login of the API under test.
- quiet.py: CPU used outside the benchmark's containers over a window.
- compare.py: per-row medians, round ranges and deltas for any arms; a
  measurement with non-2xx responses or transport errors is counted as
  failed and left out; oha's end-of-run "aborted due to deadline" entries
  are not failures (tests/tooling/test_perf_compare.py).
- micro.py, pool_churn.py: standalone microbenchmarks and the pool repro.
- sitecustomize.py: an opt-in SQLAlchemy pool trace for the service
  workers.
Preserve callback FIFO across loop restarts, serialize subprocess lifecycle
notifications, and use a portable native driver in wake-race tests.

Skip canceled native DNS work before it reaches libc and recheck immutable
stdlib code plus wrapper metadata after in-place instrumentation or reload.
Add deterministic regressions for queue activation, subprocess EOF ordering,
DNS cancellation and Future cleanup, and instrumentation changes.
@egorsmkv
egorsmkv merged commit 67c9d2d into RustedBytes:master Oct 19, 2026
46 checks passed
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.

2 participants