You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
asyncio.wait_for around connection.recv() allocates a TimerHandle per frame #356
Each iteration of _recv_loop wraps the raw socket recv() in asyncio.wait_for(...) with a 50 ms deadline so _pause_pending / _running get re-checked on quiet channels. CPython's wait_for registers a TimerHandle for the deadline, sets up a cancellation callback, wraps the awaitable in a task on 3.10 (and an asyncio.timeout() context manager on 3.11+ — still allocates an internal handle), then tears the timer down whenever the inner coroutine completes — even when it completes via a frame arriving in microseconds.
On a busy channel with 5–10 k frames/second the recv loop is allocating that many TimerHandles and cancellations per second. #245 explicitly traded shield/Task per-frame allocation for this poll, but the per-frame timer cost is non-zero and the comment on _RECV_POLL_S undersells it ("adds no overhead in the hot path").
Evidence
kalshi/ws/client.py:284-291:
try:
# Poll with a short timeout so ``_pause_pending`` and# ``_running`` get re-checked on quiet channels. On a# busy channel the timeout is cancelled before firing,# so polling adds no overhead in the hot path.raw=awaitasyncio.wait_for(
self._connection.recv(), timeout=_RECV_POLL_S,
)
Cheap (docs only): amend the _RECV_POLL_S comment to acknowledge the per-frame TimerHandle allocation cost and explain why the cooperative pause approach was still chosen over shield/Task pairs.
Real fix: replace per-frame wait_for with a single long-lived recv_task = asyncio.ensure_future(self._connection.recv()) paired with an asyncio.Event that _pause_recv_loop sets. Race them via asyncio.wait({recv_task, pause_event.wait()}, return_when=FIRST_COMPLETED). The recv_task is reused on the next iteration (or recreated only after a frame is consumed) so allocation amortizes across many frames.
Validate with scripts/bench_ws_recv.py on a high-rate orderbook_delta scenario and only land the real fix if the bench shows a meaningful improvement.
Activity
polish(ws): seq sync hot path + orphan correlation + reconnect exc_in…