Skip to content

Commit 9a7ff74

Browse files
authored
performance: remove consumed frames in place from data buffer (#1321)
Deleting the consumed prefix preserves the bytearray's amortized left-delete behavior instead of copying the entire remaining buffer after every frame. Closes #474
1 parent 6cce763 commit 9a7ff74

2 files changed

Lines changed: 26 additions & 1 deletion

File tree

src/h2/frame_buffer.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,14 @@ def __next__(self) -> Frame:
155155

156156
# At this point, as we know we'll use or discard the entire frame, we
157157
# can update the data.
158-
self._data = self._data[9+length:]
158+
# Deleting the consumed prefix mutates the bytearray in place instead
159+
# of copying the remaining bytes into a new object, as slicing would.
160+
# ``del s[i:j]`` is documented for mutable sequences in
161+
# https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types
162+
# and CPython's bytearray tracks an internal offset (``ob_start`` in
163+
# Objects/bytearrayobject.c) that makes repeated deletes from the
164+
# front amortized O(1) per byte rather than O(len) per frame.
165+
del self._data[:9+length]
159166

160167
# Pass the frame through the header buffer.
161168
new_frame = self._update_header_buffer(f)

tests/test_basic_logic.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,24 @@
2323
from . import helpers
2424

2525

26+
class TestFrameBuffer:
27+
def test_consumed_frames_are_removed_in_place(self) -> None:
28+
frame = hyperframe.frame.SettingsFrame(0).serialize()
29+
buffer = h2.frame_buffer.FrameBuffer()
30+
buffer.max_frame_size = 65535
31+
buffer.add_data(frame * 2)
32+
data = buffer._data
33+
34+
next(buffer)
35+
36+
# ``is`` checks object identity (CPython compares ``id(...)`` of both
37+
# operands): the buffer must still be the very same bytearray object,
38+
# proving the consumed frame was deleted in place rather than the
39+
# buffer being replaced by a sliced copy.
40+
assert buffer._data is data
41+
assert buffer._data == frame
42+
43+
2644
class TestBasicClient:
2745
"""
2846
Basic client-side tests.

0 commit comments

Comments
 (0)