From 7262262977af361ee08fb31e0d2e24eec0e4f302 Mon Sep 17 00:00:00 2001 From: Ali Aliyev Date: Tue, 24 Mar 2026 23:08:30 +0400 Subject: [PATCH 1/5] fix: use in-place del instead of slice-copy for audio buffer bytearray slice assignment (self._buffer = self._buffer[n:]) creates a new bytearray every 20ms. At 48kHz mono s16, that's 50 allocations per second, each copying the remaining buffer. pymalloc retains these in arena pools and never returns the memory to the OS. del self._buffer[:n] removes data in-place without allocating a new object. Memray profiling showed this single line accounted for 900MB of allocations per call. --- getstream/video/rtc/audio_track.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/getstream/video/rtc/audio_track.py b/getstream/video/rtc/audio_track.py index c191ad18..975e07c5 100644 --- a/getstream/video/rtc/audio_track.py +++ b/getstream/video/rtc/audio_track.py @@ -130,7 +130,7 @@ async def write(self, pcm: PcmData): ) # Drop from the beginning of the buffer to keep latest data - self._buffer = self._buffer[bytes_to_drop:] + del self._buffer[:bytes_to_drop] buffer_duration_ms = ( len(self._buffer) @@ -192,7 +192,7 @@ async def recv(self) -> Frame: if len(self._buffer) >= self._bytes_per_frame: # We have enough data audio_bytes = bytes(self._buffer[: self._bytes_per_frame]) - self._buffer = self._buffer[self._bytes_per_frame :] + del self._buffer[: self._bytes_per_frame] elif len(self._buffer) > 0: # We have some data but not enough - pad with silence audio_bytes = bytes(self._buffer) From 4c2969dea5375b4e30bc4dceaa00d17139ea3a9d Mon Sep 17 00:00:00 2001 From: Ali Aliyev Date: Thu, 26 Mar 2026 02:23:34 +0400 Subject: [PATCH 2/5] test: verify buffer operations are in-place (no reallocation) Assert that recv() and overflow trim modify the same bytearray object rather than creating a new one via slice-copy. --- tests/test_audio_stream_track.py | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/test_audio_stream_track.py b/tests/test_audio_stream_track.py index ec4384bc..ad3469c7 100644 --- a/tests/test_audio_stream_track.py +++ b/tests/test_audio_stream_track.py @@ -302,6 +302,60 @@ async def _continuous_reader(self, track): frames_received += 1 assert frame.samples == 960 + @pytest.mark.asyncio + async def test_recv_does_not_reallocate_buffer(self): + """Test that recv consumes data in-place without creating a new buffer object.""" + track = AudioStreamTrack(sample_rate=48000, channels=1, format="s16") + + # Write 40ms of data (enough for 2 frames) + samples = np.zeros(1920, dtype=np.int16) + pcm = PcmData( + samples=samples, + sample_rate=48000, + format=AudioFormat.S16, + channels=1, + ) + await track.write(pcm) + + # Store reference to the buffer object + buffer_id = id(track._buffer) + + # Receive a frame (consumes 20ms from buffer) + await track.recv() + + assert id(track._buffer) == buffer_id, "recv should modify buffer in-place, not create a new one" + assert len(track._buffer) == 960 * 2, "should have 20ms of data remaining (960 samples * 2 bytes)" + + @pytest.mark.asyncio + async def test_buffer_overflow_does_not_reallocate(self): + """Test that buffer overflow trims in-place without creating a new buffer object.""" + track = AudioStreamTrack( + sample_rate=48000, channels=1, format="s16", audio_buffer_size_ms=100 + ) + + # Write 50ms of data first to get a buffer reference + samples_50ms = np.zeros(2400, dtype=np.int16) + pcm = PcmData( + samples=samples_50ms, + sample_rate=48000, + format=AudioFormat.S16, + channels=1, + ) + await track.write(pcm) + buffer_id = id(track._buffer) + + # Write 200ms of data (exceeds 100ms limit, triggers overflow trim) + samples_200ms = np.zeros(9600, dtype=np.int16) + pcm_large = PcmData( + samples=samples_200ms, + sample_rate=48000, + format=AudioFormat.S16, + channels=1, + ) + await track.write(pcm_large) + + assert id(track._buffer) == buffer_id, "overflow trim should modify buffer in-place, not create a new one" + @pytest.mark.asyncio async def test_media_stream_error(self): """Test that MediaStreamError is raised when track is not live.""" From 1ba0b9db4eee4f55880fc0be2c43eb1e61bf6215 Mon Sep 17 00:00:00 2001 From: Ali Aliyev Date: Thu, 26 Mar 2026 13:47:40 +0400 Subject: [PATCH 3/5] style: format test assertions with ruff --- tests/test_audio_stream_track.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test_audio_stream_track.py b/tests/test_audio_stream_track.py index ad3469c7..056d4851 100644 --- a/tests/test_audio_stream_track.py +++ b/tests/test_audio_stream_track.py @@ -323,8 +323,12 @@ async def test_recv_does_not_reallocate_buffer(self): # Receive a frame (consumes 20ms from buffer) await track.recv() - assert id(track._buffer) == buffer_id, "recv should modify buffer in-place, not create a new one" - assert len(track._buffer) == 960 * 2, "should have 20ms of data remaining (960 samples * 2 bytes)" + assert id(track._buffer) == buffer_id, ( + "recv should modify buffer in-place, not create a new one" + ) + assert len(track._buffer) == 960 * 2, ( + "should have 20ms of data remaining (960 samples * 2 bytes)" + ) @pytest.mark.asyncio async def test_buffer_overflow_does_not_reallocate(self): @@ -354,7 +358,9 @@ async def test_buffer_overflow_does_not_reallocate(self): ) await track.write(pcm_large) - assert id(track._buffer) == buffer_id, "overflow trim should modify buffer in-place, not create a new one" + assert id(track._buffer) == buffer_id, ( + "overflow trim should modify buffer in-place, not create a new one" + ) @pytest.mark.asyncio async def test_media_stream_error(self): From b33a7a3b8857d70f8d25a90bd94be4e0d03f9a56 Mon Sep 17 00:00:00 2001 From: Ali Aliyev Date: Thu, 26 Mar 2026 16:16:21 +0400 Subject: [PATCH 4/5] fix: use object identity check instead of id() in buffer tests id() can be reused after deallocation, and `obj is int` is always False. Save a reference before the operation and compare with `is` after to correctly verify the buffer is the same object. --- tests/test_audio_stream_track.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_audio_stream_track.py b/tests/test_audio_stream_track.py index 056d4851..f08b8f7e 100644 --- a/tests/test_audio_stream_track.py +++ b/tests/test_audio_stream_track.py @@ -317,13 +317,12 @@ async def test_recv_does_not_reallocate_buffer(self): ) await track.write(pcm) - # Store reference to the buffer object - buffer_id = id(track._buffer) + buffer_ref = track._buffer # save reference before recv # Receive a frame (consumes 20ms from buffer) await track.recv() - assert id(track._buffer) == buffer_id, ( + assert track._buffer is buffer_ref, ( "recv should modify buffer in-place, not create a new one" ) assert len(track._buffer) == 960 * 2, ( @@ -346,7 +345,7 @@ async def test_buffer_overflow_does_not_reallocate(self): channels=1, ) await track.write(pcm) - buffer_id = id(track._buffer) + buffer_ref = track._buffer # save reference before overflow # Write 200ms of data (exceeds 100ms limit, triggers overflow trim) samples_200ms = np.zeros(9600, dtype=np.int16) @@ -358,7 +357,7 @@ async def test_buffer_overflow_does_not_reallocate(self): ) await track.write(pcm_large) - assert id(track._buffer) == buffer_id, ( + assert track._buffer is buffer_ref, ( "overflow trim should modify buffer in-place, not create a new one" ) From 823c41922a5ad1b5b8a1a625302591026d5023fa Mon Sep 17 00:00:00 2001 From: Ali Aliyev Date: Thu, 26 Mar 2026 18:04:48 +0400 Subject: [PATCH 5/5] test: assert buffer length after overflow trim Identity check alone doesn't prove overflow path executed. Add length assertion to verify buffer is trimmed to max size. --- tests/test_audio_stream_track.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_audio_stream_track.py b/tests/test_audio_stream_track.py index f08b8f7e..a418b3be 100644 --- a/tests/test_audio_stream_track.py +++ b/tests/test_audio_stream_track.py @@ -360,6 +360,12 @@ async def test_buffer_overflow_does_not_reallocate(self): assert track._buffer is buffer_ref, ( "overflow trim should modify buffer in-place, not create a new one" ) + max_buffer_seconds = 100 / 1000 + bytes_per_sample = 2 + expected_max_bytes = int(max_buffer_seconds * 48000) * bytes_per_sample + assert len(track._buffer) == expected_max_bytes, ( + "buffer should be trimmed to configured max size" + ) @pytest.mark.asyncio async def test_media_stream_error(self):