diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 6dc856cbf..eaced1e0b 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -47,6 +47,8 @@ CMSG_SPACE(RECEIVED_TIMESTAMP_STRUCT.size) if CMSG_SPACE_available else 0 ) +MSG_DONTROUTE = int(socket.MSG_DONTROUTE) + # Setup BCM struct def bcm_header_factory( @@ -656,7 +658,7 @@ def capture_message(sock: socket.socket, get_channel: bool = False) -> Message | error_state_indicator = bool(flags & constants.CANFD_ESI) # Section 4.7.1: MSG_DONTROUTE: set when the received frame was created on the local host. - is_rx = not bool(msg_flags & socket.MSG_DONTROUTE) + is_rx = not msg_flags & MSG_DONTROUTE if is_extended_frame_format: # log.debug("CAN: Extended") @@ -666,23 +668,22 @@ def capture_message(sock: socket.socket, get_channel: bool = False) -> Message | # log.debug("CAN: Standard") arbitration_id = can_id & 0x000007FF - msg = Message( - timestamp=timestamp, - channel=channel, - arbitration_id=arbitration_id, - is_extended_id=is_extended_frame_format, - is_remote_frame=is_remote_transmission_request, - is_error_frame=is_error_frame, - is_fd=is_fd, - is_rx=is_rx, - bitrate_switch=bitrate_switch, - error_state_indicator=error_state_indicator, - dlc=can_dlc, - data=data, + # Built positionally: binding twelve keyword arguments is slower. + return Message( + timestamp, + arbitration_id, + is_extended_frame_format, + is_remote_transmission_request, + is_error_frame, + channel, + can_dlc, + data, + is_fd, + is_rx, + bitrate_switch, + error_state_indicator, ) - return msg - class SocketcanBus(BusABC): # pylint: disable=abstract-method """A SocketCAN interface to CAN. diff --git a/doc/changelog.d/2097.changed.rst b/doc/changelog.d/2097.changed.rst new file mode 100644 index 000000000..d709605de --- /dev/null +++ b/doc/changelog.d/2097.changed.rst @@ -0,0 +1 @@ +Improved performance of the ``socketcan`` receive path: ``capture_message`` now uses roughly a third less CPU per frame. diff --git a/test/test_socketcan.py b/test/test_socketcan.py index 9d042f425..4d133e366 100644 --- a/test/test_socketcan.py +++ b/test/test_socketcan.py @@ -5,27 +5,32 @@ """ import ctypes +import socket import struct import sys import unittest import warnings -from unittest.mock import patch +from unittest.mock import Mock, patch import can from can.interfaces.socketcan.constants import ( CAN_BCM_TX_DELETE, CAN_BCM_TX_SETUP, SETTIMER, + SO_TIMESTAMPNS, STARTTIMER, TX_COUNTEVT, ) from can.interfaces.socketcan.socketcan import ( + RECEIVED_TIMESTAMP_STRUCT, BcmMsgHead, bcm_header_factory, build_bcm_header, build_bcm_transmit_header, build_bcm_tx_delete_header, build_bcm_update_header, + build_can_frame, + capture_message, ) from .config import IS_LINUX, IS_PYPY, TEST_INTERFACE_SOCKETCAN @@ -390,6 +395,44 @@ def test_pypy_socketcan_support(self): "See: https://github.com/pypy/pypy/issues/3808" ) + def test_capture_message(self): + ancillary_data = [ + ( + socket.SOL_SOCKET, + SO_TIMESTAMPNS, + RECEIVED_TIMESTAMP_STRUCT.pack(1_700_000_000, 123_456_000), + ) + ] + defaults = {"timestamp": 1_700_000_000.123456, "is_extended_id": False} + # Every pair of boolean fields differs in at least one message, so swapped + # arguments in the Message() call of capture_message() cannot go unnoticed. + for kwargs in ( + {"arbitration_id": 0x123, "channel": "vcan0", "data": [1, 2, 3]}, + {"arbitration_id": 0x1ABCDEF, "is_extended_id": True, "is_rx": False}, + {"arbitration_id": 0x7FF, "is_remote_frame": True, "dlc": 4}, + {"arbitration_id": 0x004, "is_error_frame": True, "data": bytes(8)}, + {"arbitration_id": 0x456, "is_fd": True, "data": range(12)}, + {"arbitration_id": 0x456, "is_fd": True, "bitrate_switch": True}, + {"arbitration_id": 0x456, "is_fd": True, "error_state_indicator": True}, + ): + with self.subTest(**kwargs): + expected = can.Message(**{**defaults, **kwargs}) + sock = Mock() + sock.recvmsg.return_value = ( + build_can_frame(expected), + ancillary_data, + 0 if expected.is_rx else socket.MSG_DONTROUTE, + ("vcan0",), + ) + + received = capture_message( + sock, get_channel=expected.channel is not None + ) + + self.assertTrue( + received.equals(expected), f"{received!r} != {expected!r}" + ) + if __name__ == "__main__": unittest.main()