Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement zerocopy writes #990

Merged
merged 12 commits into from
Nov 1, 2024
Merged

Implement zerocopy writes #990

merged 12 commits into from
Nov 1, 2024

Conversation

bdraco
Copy link
Member

@bdraco bdraco commented Nov 1, 2024

With Python 3.12+ and later transport.writelines is implemented as sendmsg(..., IOV_MAX) which allows us to avoid joining the bytes and sending them in one go.

Older Python will effectively do the same thing we do now b"".join(...)

Copy link

codspeed-hq bot commented Nov 1, 2024

CodSpeed Performance Report

Merging #990 will not alter performance

Comparing zerocopy_writes (9a6f50b) with main (4bea46b)

Summary

✅ 2 untouched benchmarks

Copy link

codecov bot commented Nov 1, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 100.00%. Comparing base (4bea46b) to head (9a6f50b).
Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #990   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           17        17           
  Lines         2671      2673    +2     
=========================================
+ Hits          2671      2673    +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@bdraco
Copy link
Member Author

bdraco commented Nov 1, 2024

sendmsg(155, {msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\0", iov_len=1}, {iov_base="\24", iov_len=1}, {iov_base="K", iov_len=1}, {iov_base="\10\252\304\354\373\316\202/\20\36\30\1\"\6\253\252\3\344\347U", iov_len=20}], msg_iovlen=4, msg_controllen=0, msg_flags=0}, 0) = 23

@bdraco bdraco marked this pull request as ready for review November 1, 2024 12:41
With Python 3.12+ and later transport.writelines is implemented as `sendmsg(..., IOV_MAX)`
which allows us to avoid joining the bytes and sending them in one go
With Python 3.12+ and later transport.writelines is implemented as `sendmsg(..., IOV_MAX)`
which allows us to avoid joining the bytes and sending them in one go
Copy link
Contributor

coderabbitai bot commented Nov 1, 2024

Walkthrough

The changes in this pull request primarily involve renaming and modifying the APIFrameHelper class and its related components in the aioesphomeapi package. The public member variable _writer has been renamed to _writelines, and its type has been changed to accept an iterable of byte-like objects. This affects various methods across multiple files, enhancing the handling of data writing operations. Additionally, modifications to test files ensure that the new writing mechanism is correctly validated.

Changes

File Path Change Summary
aioesphomeapi/_frame_helper/base.pxd Renamed public member variable _writer to _writelines in APIFrameHelper.
aioesphomeapi/_frame_helper/base.py Updated _writer to _writelines to accept an Iterable of byte-like objects. Modified _write_bytes method signature. Adjusted connection_made and close methods for new attribute.
aioesphomeapi/_frame_helper/noise.py Altered _send_hello_handshake and write_packets methods to use lists instead of concatenated byte strings. Enhanced error handling in _handle_error method for better context.
aioesphomeapi/_frame_helper/plain_text.py Modified write_packets method to append data conditionally and call _write_bytes with a list instead of a joined byte string.
tests/common.py Replaced generate_plaintext_packet with two functions: generate_split_plaintext_packet returning a list of bytes and the new generate_plaintext_packet returning a single concatenated byte string.
tests/test__frame_helper.py Updated MockAPINoiseFrameHelper to use transport.writelines instead of transport.write. Adjusted methods and assertions to reflect the new writing mechanism.
tests/test_client.py Added generate_split_plaintext_packet function. Updated instances of transport.write to transport.writelines in tests.
tests/test_connection.py Changed instances of transport.write to transport.writelines across multiple test functions, ensuring assertions reflect the new method of sending messages.

Possibly related PRs

  • Avoid a dict lookup and int conversion to process every packet #946: The changes in this PR involve modifications to the APINoiseFrameHelper class and its methods, which are related to the handling of frames and packet processing, similar to the changes made to the APIFrameHelper class in the main PR. Both PRs involve updates to how data is processed and written, indicating a connection in their functionality.

📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 35cf0bd and 9a6f50b.

📒 Files selected for processing (1)
  • aioesphomeapi/_frame_helper/noise.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • aioesphomeapi/_frame_helper/noise.py

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Outside diff range and nitpick comments (1)
aioesphomeapi/_frame_helper/base.py (1)

186-196: Excellent implementation of zerocopy writes.

The changes effectively implement zerocopy writes by:

  1. Accepting an iterable of byte-like objects
  2. Only concatenating bytes for debug logging when enabled
  3. Directly passing the iterable to transport.writelines, avoiding unnecessary concatenation in the data path

This implementation aligns perfectly with the PR objective to leverage Python 3.12's transport.writelines for improved performance.

Note: The performance benefit will be most noticeable when sending multiple large buffers, as it avoids the memory allocation and copying overhead of concatenating them.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 4bea46b and 35cf0bd.

📒 Files selected for processing (8)
  • aioesphomeapi/_frame_helper/base.pxd (1 hunks)
  • aioesphomeapi/_frame_helper/base.py (5 hunks)
  • aioesphomeapi/_frame_helper/noise.py (2 hunks)
  • aioesphomeapi/_frame_helper/plain_text.py (1 hunks)
  • tests/common.py (1 hunks)
  • tests/test__frame_helper.py (9 hunks)
  • tests/test_client.py (6 hunks)
  • tests/test_connection.py (12 hunks)
🧰 Additional context used
📓 Path-based instructions (8)
aioesphomeapi/_frame_helper/base.pxd (1)

Pattern **: - Do not generate or add any sequence diagrams

aioesphomeapi/_frame_helper/base.py (1)

Pattern **: - Do not generate or add any sequence diagrams

aioesphomeapi/_frame_helper/noise.py (1)

Pattern **: - Do not generate or add any sequence diagrams

aioesphomeapi/_frame_helper/plain_text.py (1)

Pattern **: - Do not generate or add any sequence diagrams

tests/common.py (1)

Pattern **: - Do not generate or add any sequence diagrams

tests/test__frame_helper.py (1)

Pattern **: - Do not generate or add any sequence diagrams

tests/test_client.py (1)

Pattern **: - Do not generate or add any sequence diagrams

tests/test_connection.py (1)

Pattern **: - Do not generate or add any sequence diagrams

🔇 Additional comments (28)
aioesphomeapi/_frame_helper/plain_text.py (2)

60-61: LGTM! Good optimization to skip empty data.

The conditional check prevents adding empty data to the output buffer, which is more efficient.


63-63: Verify transport.writelines implementation.

Since this PR aims to leverage Python 3.12's transport.writelines with IOV_MAX, let's verify the base implementation.

✅ Verification successful

Let's gather more information about the implementation details.


Let's check one more thing to understand how the _writelines is set and used across the codebase.


Transport writelines implementation is correctly used

The verification shows that the base implementation properly handles transport.writelines:

  • The base class correctly initializes _writelines in connection_made by assigning it from transport
  • Type checking ensures _writelines is available before use
  • The implementation properly accepts an Iterable[bytes] as required for transport.writelines
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if base class implements writelines correctly
# Expected: Find implementation of _write_bytes in base class that uses transport.writelines

# Look for base class implementation
ast-grep --pattern 'def _write_bytes(self, $_, $_)'

# Look for transport.writelines usage
rg -l 'transport.*writelines'

Length of output: 195


Script:

#!/bin/bash
# Check the implementation of _write_bytes in base.py and its usage of transport.writelines
rg -A 10 "_write_bytes" aioesphomeapi/_frame_helper/base.py

# Check test cases to understand the expected behavior
rg -A 5 "transport.*writelines" tests/test__frame_helper.py

Length of output: 761


Script:

#!/bin/bash
# Check how _writelines is initialized and used
rg -A 5 "_writelines" aioesphomeapi/_frame_helper/base.py

# Check the connection_made implementation that sets up transport
rg -A 10 "connection_made" aioesphomeapi/_frame_helper/base.py

Length of output: 1367

tests/common.py (2)

79-80: LGTM! Good backward compatibility implementation.

The function maintains compatibility with code expecting single bytes while leveraging the new split packet generation internally.


68-76: LGTM! The packet structure is correctly split for zerocopy writes.

The function efficiently splits the packet into separate byte segments that align with the sendmsg IOV structure shown in the PR comments. Using _cached_varuint_to_bytes for length and type encoding maintains good performance.

Let's verify the packet structure matches the protocol:

✅ Verification successful

Based on the comprehensive verification results, I can now provide a final response:

The packet structure implementation is correct and consistent with the protocol.

The verification confirms that:

  1. The packet structure in generate_split_plaintext_packet matches the protocol implementation in APIPlaintextFrameHelper.write_packets and data_received
  2. The packet components are correctly ordered:
    • Null byte marker (b"\0")
    • Length as varuint
    • Message type as varuint
    • Serialized bytes
  3. The implementation is validated by extensive test cases in test__frame_helper.py that verify both packet generation and parsing
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the packet structure matches the protocol implementation
# Test: Check if the packet structure in other files matches our implementation
rg -A 5 "data_received.*\x00.*varuint.*bytes_"

Length of output: 48


Script:

#!/bin/bash
# Let's search for protocol implementation files and packet handling
rg -l "class APIConnection|class APIServer" --type py

Length of output: 105


Script:

#!/bin/bash
# Search for packet handling and message type mapping
rg "PROTO_TO_MESSAGE_TYPE|MESSAGE_TYPE_TO_PROTO" --type py -A 3

Length of output: 2466


Script:

#!/bin/bash
# Look for packet parsing/receiving logic
rg "async def data_received|def data_received" --type py -A 10

Length of output: 1902


Script:

#!/bin/bash
# Check test files that verify packet structure
fd "test.*packet.*\.py$"

Length of output: 26


Script:

#!/bin/bash
# Let's examine the packet structure in the frame helper implementation
cat aioesphomeapi/_frame_helper/plain_text.py

Length of output: 3579


Script:

#!/bin/bash
# Check the actual packet generation in connection.py
rg "def _message_to_json|def _format_message_for_log" aioesphomeapi/connection.py -A 10

Length of output: 89


Script:

#!/bin/bash
# Look for any test cases that verify packet structure
rg "test.*packet" tests/ -l

Length of output: 57


Script:

#!/bin/bash
# Let's check the frame helper test cases
cat tests/test__frame_helper.py

Length of output: 24959


Script:

#!/bin/bash
# Check the actual packet writing implementation in connection
rg "write_packet|write_packets" aioesphomeapi/ -A 5

Length of output: 2568

aioesphomeapi/_frame_helper/base.py (5)

5-5: LGTM: Import addition is appropriate.

The addition of Iterable import from collections.abc is necessary for type hinting the new writelines parameter.


35-35: LGTM: Slot name change reflects new functionality.

The renaming from _writer to _writelines in __slots__ maintains consistency with the new zerocopy writes implementation.


55-57: LGTM: Type annotation accurately reflects transport.writelines signature.

The type annotation properly defines the expected callable that accepts an iterable of byte-like objects, aligning with Python's transport.writelines specification.


152-152: LGTM: Connection lifecycle properly manages writelines.

The changes consistently handle the writelines assignment during connection establishment and cleanup during connection closure.

Also applies to: 178-178


152-152: Verify Python version requirement.

Since this implementation relies on Python 3.12's transport.writelines feature, ensure that the package's Python version requirement is properly updated in setup.py/pyproject.toml.

aioesphomeapi/_frame_helper/noise.py (2)

221-221: Optimization: Efficient handshake frame transmission

The change from byte concatenation to passing a list of byte sequences leverages Python 3.12's zerocopy writes capability, eliminating unnecessary memory allocation and copy operations during the handshake phase.


349-349: Optimization: Efficient packet transmission

The change leverages Python 3.12's zerocopy writes capability by passing the list of packets directly to _write_bytes, avoiding unnecessary concatenation of potentially large encrypted packets. This is particularly beneficial when sending multiple packets in sequence.

tests/test__frame_helper.py (3)

5-5: LGTM: Import of Iterable for type hints

The addition of Iterable from collections.abc aligns with the changes to support multiple buffers in writelines.


136-136: LGTM: Updated MockAPINoiseFrameHelper to use writelines

The changes correctly mock the transport to use writelines instead of write, which aligns with the PR's objective to leverage Python 3.12's zerocopy writes capability.

Also applies to: 151-151


Line range hint 441-563: Verify the test coverage for writelines error handling

The tests should verify that writelines properly handles transport errors (e.g., ConnectionResetError, OSError) and propagates them correctly.

tests/test_connection.py (9)

118-118: LGTM: Assertion correctly updated for writelines.

The assertion correctly verifies that the disconnect request is sent as multiple buffers using writelines.


155-155: LGTM: Consistent assertion pattern.

The assertion follows the same pattern as other disconnect tests, correctly verifying the writelines call.


509-509: LGTM: Protocol transport assignment.

The assignment of _writelines to the transport's writelines method is correct for the mock protocol setup.


552-554: LGTM: Test structure update.

The test case structure is properly updated with correct indentation and context management.


651-651: LGTM: Error simulation.

The mock correctly simulates an OSError during writelines to test error handling during disconnect.


898-898: LGTM: Ping test assertions.

The ping test correctly:

  1. Defines the expected ping request bytes
  2. Verifies the call count
  3. Validates the exact calls made
  4. Checks the final state

Also applies to: 909-909, 911-911, 920-920


937-937: LGTM: Ping response handling.

The test properly verifies that only one ping request is sent when responses are received.

Also applies to: 950-951


981-983: LGTM: Ping request response.

The test correctly verifies the response to a ping request using the new writelines interface.


Line range hint 898-983: Consider adding buffer size verification.

While the tests verify the correct sequence of bytes being sent, they could be enhanced to verify that each buffer in writelines is under the system's IOV_MAX limit.

tests/test_client.py (5)

129-129: LGTM: Import added for new packet generation utility

The addition of generate_split_plaintext_packet import aligns with the PR's objective to implement zerocopy writes by supporting split packet generation.


1443-1448: LGTM: Test assertion verifies correct packet structure

The test assertion correctly verifies that the transport writelines is called with the expected packet structure for bluetooth GATT write operations. The packet is split into multiple buffers:

  1. Control byte
  2. Length byte
  3. Message type
  4. Payload

2056-2057: LGTM: Test assertion verifies zerocopy write behavior

The assertion correctly verifies that the transport writelines is called with the split packet generated by generate_split_plaintext_packet for bluetooth device requests.


2147-2147: LGTM: Test verifies transport writelines call count

The test properly verifies the sequence of transport writelines calls:

  1. Initial connect request
  2. Subsequent disconnect request after timeout

Also applies to: 2153-2153


2191-2191: LGTM: Test verifies initial connect request

The assertion correctly verifies that the initial connect request is written via transport writelines.

aioesphomeapi/_frame_helper/base.pxd Show resolved Hide resolved
aioesphomeapi/_frame_helper/plain_text.py Show resolved Hide resolved
tests/test__frame_helper.py Show resolved Hide resolved
@bdraco bdraco merged commit ba05d38 into main Nov 1, 2024
18 checks passed
@bdraco bdraco deleted the zerocopy_writes branch November 1, 2024 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant