@@ -90,6 +90,8 @@ class WebSocketResponse(StreamResponse, Generic[_DecodeText]):
9090 _heartbeat_cb : asyncio .TimerHandle | None = None
9191 _pong_response_cb : asyncio .TimerHandle | None = None
9292 _ping_task : asyncio .Task [None ] | None = None
93+ _need_heartbeat_reset : bool = False
94+ _heartbeat_reset_handle : asyncio .Handle | None = None
9395
9496 def __init__ (
9597 self ,
@@ -118,9 +120,15 @@ def __init__(
118120 self ._max_msg_size = max_msg_size
119121 self ._writer_limit = writer_limit
120122 self ._decode_text = decode_text
123+ self ._need_heartbeat_reset = False
124+ self ._heartbeat_reset_handle = None
121125
122126 def _cancel_heartbeat (self ) -> None :
123127 self ._cancel_pong_response_cb ()
128+ if self ._heartbeat_reset_handle is not None :
129+ self ._heartbeat_reset_handle .cancel ()
130+ self ._heartbeat_reset_handle = None
131+ self ._need_heartbeat_reset = False
124132 if self ._heartbeat_cb is not None :
125133 self ._heartbeat_cb .cancel ()
126134 self ._heartbeat_cb = None
@@ -133,6 +141,23 @@ def _cancel_pong_response_cb(self) -> None:
133141 self ._pong_response_cb .cancel ()
134142 self ._pong_response_cb = None
135143
144+ def _on_data_received (self ) -> None :
145+ if self ._heartbeat is None or self ._need_heartbeat_reset :
146+ return
147+ loop = self ._loop
148+ assert loop is not None
149+ # Coalesce multiple chunks received in the same loop tick into a single
150+ # heartbeat reset. Resetting immediately per chunk increases timer churn.
151+ self ._need_heartbeat_reset = True
152+ self ._heartbeat_reset_handle = loop .call_soon (self ._flush_heartbeat_reset )
153+
154+ def _flush_heartbeat_reset (self ) -> None :
155+ self ._heartbeat_reset_handle = None
156+ if not self ._need_heartbeat_reset :
157+ return
158+ self ._reset_heartbeat ()
159+ self ._need_heartbeat_reset = False
160+
136161 def _reset_heartbeat (self ) -> None :
137162 if self ._heartbeat is None :
138163 return
@@ -156,6 +181,12 @@ def _reset_heartbeat(self) -> None:
156181
157182 def _send_heartbeat (self ) -> None :
158183 self ._heartbeat_cb = None
184+
185+ # If heartbeat reset is pending (data is being received), skip sending
186+ # the ping and let the reset callback handle rescheduling the heartbeat.
187+ if self ._need_heartbeat_reset :
188+ return
189+
159190 loop = self ._loop
160191 assert loop is not None and self ._writer is not None
161192 now = loop .time ()
@@ -349,14 +380,14 @@ def _post_start(
349380 loop = self ._loop
350381 assert loop is not None
351382 self ._reader = WebSocketDataQueue (request ._protocol , 2 ** 16 , loop = loop )
352- request .protocol .set_parser (
353- WebSocketReader (
354- self ._reader ,
355- self ._max_msg_size ,
356- compress = bool (self ._compress ),
357- decode_text = self ._decode_text ,
358- )
383+ parser = WebSocketReader (
384+ self ._reader ,
385+ self ._max_msg_size ,
386+ compress = bool (self ._compress ),
387+ decode_text = self ._decode_text ,
359388 )
389+ cb = None if self ._heartbeat is None else self ._on_data_received
390+ request .protocol .set_parser (parser , data_received_cb = cb )
360391 # disable HTTP keepalive for WebSocket
361392 request .protocol .keep_alive (False )
362393
@@ -576,7 +607,6 @@ async def receive(
576607 msg = await self ._reader .read ()
577608 else :
578609 msg = await self ._reader .read ()
579- self ._reset_heartbeat ()
580610 finally :
581611 self ._waiting = False
582612 if self ._close_wait :
0 commit comments