From a741c6db62a80b949ba44965dd3a65ff63fe8143 Mon Sep 17 00:00:00 2001 From: Utkarsh Tiwari Date: Mon, 27 Jul 2026 01:50:38 +0530 Subject: [PATCH 1/2] Remove consumed frames in place Deleting the consumed prefix preserves the bytearray's amortized left-delete behavior instead of copying the entire remaining buffer after every frame. Closes #474 --- src/h2/frame_buffer.py | 2 +- tests/test_basic_logic.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/h2/frame_buffer.py b/src/h2/frame_buffer.py index c2f8a5411..e7eaaa577 100644 --- a/src/h2/frame_buffer.py +++ b/src/h2/frame_buffer.py @@ -155,7 +155,7 @@ def __next__(self) -> Frame: # At this point, as we know we'll use or discard the entire frame, we # can update the data. - self._data = self._data[9+length:] + del self._data[:9+length] # Pass the frame through the header buffer. new_frame = self._update_header_buffer(f) diff --git a/tests/test_basic_logic.py b/tests/test_basic_logic.py index 1df989e06..2bef59c45 100644 --- a/tests/test_basic_logic.py +++ b/tests/test_basic_logic.py @@ -23,6 +23,20 @@ from . import helpers +class TestFrameBuffer: + def test_consumed_frames_are_removed_in_place(self) -> None: + frame = hyperframe.frame.SettingsFrame(0).serialize() + buffer = h2.frame_buffer.FrameBuffer() + buffer.max_frame_size = 65535 + buffer.add_data(frame * 2) + data = buffer._data + + next(buffer) + + assert buffer._data is data + assert buffer._data == frame + + class TestBasicClient: """ Basic client-side tests. From 3a59b4acaf8345bcf7b38c4fa6fde3d405dc6be1 Mon Sep 17 00:00:00 2001 From: Utkarsh Tiwari Date: Tue, 28 Jul 2026 15:04:09 +0530 Subject: [PATCH 2/2] Document in-place prefix deletion and the identity assertion --- src/h2/frame_buffer.py | 7 +++++++ tests/test_basic_logic.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/src/h2/frame_buffer.py b/src/h2/frame_buffer.py index e7eaaa577..cab6f9056 100644 --- a/src/h2/frame_buffer.py +++ b/src/h2/frame_buffer.py @@ -155,6 +155,13 @@ def __next__(self) -> Frame: # At this point, as we know we'll use or discard the entire frame, we # can update the data. + # Deleting the consumed prefix mutates the bytearray in place instead + # of copying the remaining bytes into a new object, as slicing would. + # ``del s[i:j]`` is documented for mutable sequences in + # https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types + # and CPython's bytearray tracks an internal offset (``ob_start`` in + # Objects/bytearrayobject.c) that makes repeated deletes from the + # front amortized O(1) per byte rather than O(len) per frame. del self._data[:9+length] # Pass the frame through the header buffer. diff --git a/tests/test_basic_logic.py b/tests/test_basic_logic.py index 2bef59c45..d1bc0f1eb 100644 --- a/tests/test_basic_logic.py +++ b/tests/test_basic_logic.py @@ -33,6 +33,10 @@ def test_consumed_frames_are_removed_in_place(self) -> None: next(buffer) + # ``is`` checks object identity (CPython compares ``id(...)`` of both + # operands): the buffer must still be the very same bytearray object, + # proving the consumed frame was deleted in place rather than the + # buffer being replaced by a sliced copy. assert buffer._data is data assert buffer._data == frame