diff --git a/src/h2/frame_buffer.py b/src/h2/frame_buffer.py index c2f8a541..cab6f905 100644 --- a/src/h2/frame_buffer.py +++ b/src/h2/frame_buffer.py @@ -155,7 +155,14 @@ 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:] + # 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. new_frame = self._update_header_buffer(f) diff --git a/tests/test_basic_logic.py b/tests/test_basic_logic.py index 1df989e0..d1bc0f1e 100644 --- a/tests/test_basic_logic.py +++ b/tests/test_basic_logic.py @@ -23,6 +23,24 @@ 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) + + # ``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 + + class TestBasicClient: """ Basic client-side tests.