Skip to content

Commit 3b28f91

Browse files
authored
Speed up socketcan capture_message by ~32% (#2097)
* Speed up socketcan capture_message by ~32% `socket.MSG_DONTROUTE` is an `enum.IntFlag`, so masking it built a `MsgFlag` per frame; it is now a module-level int. `Message` is built positionally to skip keyword binding, with a new test pinning the order. Live 500 kbit/s bus (aarch64, CPython 3.13.15): 107.8 -> 73.3 us CPU per frame; output identical over 3000 real and 1995 synthetic frames. Refs #1135 * Test capture_message instead of the Message signature The previous test pinned the parameter order of Message.__init__, so a swapped argument in capture_message's positional Message() call would not have been caught. Feed frames through capture_message via a mocked socket instead and compare every Message field. The cases are chosen so that every pair of boolean fields differs in at least one of them.
1 parent 2a45350 commit 3b28f91

3 files changed

Lines changed: 62 additions & 17 deletions

File tree

‎can/interfaces/socketcan/socketcan.py‎

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
CMSG_SPACE(RECEIVED_TIMESTAMP_STRUCT.size) if CMSG_SPACE_available else 0
4848
)
4949

50+
MSG_DONTROUTE = int(socket.MSG_DONTROUTE)
51+
5052

5153
# Setup BCM struct
5254
def bcm_header_factory(
@@ -656,7 +658,7 @@ def capture_message(sock: socket.socket, get_channel: bool = False) -> Message |
656658
error_state_indicator = bool(flags & constants.CANFD_ESI)
657659

658660
# Section 4.7.1: MSG_DONTROUTE: set when the received frame was created on the local host.
659-
is_rx = not bool(msg_flags & socket.MSG_DONTROUTE)
661+
is_rx = not msg_flags & MSG_DONTROUTE
660662

661663
if is_extended_frame_format:
662664
# log.debug("CAN: Extended")
@@ -666,23 +668,22 @@ def capture_message(sock: socket.socket, get_channel: bool = False) -> Message |
666668
# log.debug("CAN: Standard")
667669
arbitration_id = can_id & 0x000007FF
668670

669-
msg = Message(
670-
timestamp=timestamp,
671-
channel=channel,
672-
arbitration_id=arbitration_id,
673-
is_extended_id=is_extended_frame_format,
674-
is_remote_frame=is_remote_transmission_request,
675-
is_error_frame=is_error_frame,
676-
is_fd=is_fd,
677-
is_rx=is_rx,
678-
bitrate_switch=bitrate_switch,
679-
error_state_indicator=error_state_indicator,
680-
dlc=can_dlc,
681-
data=data,
671+
# Built positionally: binding twelve keyword arguments is slower.
672+
return Message(
673+
timestamp,
674+
arbitration_id,
675+
is_extended_frame_format,
676+
is_remote_transmission_request,
677+
is_error_frame,
678+
channel,
679+
can_dlc,
680+
data,
681+
is_fd,
682+
is_rx,
683+
bitrate_switch,
684+
error_state_indicator,
682685
)
683686

684-
return msg
685-
686687

687688
class SocketcanBus(BusABC): # pylint: disable=abstract-method
688689
"""A SocketCAN interface to CAN.

‎doc/changelog.d/2097.changed.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improved performance of the ``socketcan`` receive path: ``capture_message`` now uses roughly a third less CPU per frame.

‎test/test_socketcan.py‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,32 @@
55
"""
66

77
import ctypes
8+
import socket
89
import struct
910
import sys
1011
import unittest
1112
import warnings
12-
from unittest.mock import patch
13+
from unittest.mock import Mock, patch
1314

1415
import can
1516
from can.interfaces.socketcan.constants import (
1617
CAN_BCM_TX_DELETE,
1718
CAN_BCM_TX_SETUP,
1819
SETTIMER,
20+
SO_TIMESTAMPNS,
1921
STARTTIMER,
2022
TX_COUNTEVT,
2123
)
2224
from can.interfaces.socketcan.socketcan import (
25+
RECEIVED_TIMESTAMP_STRUCT,
2326
BcmMsgHead,
2427
bcm_header_factory,
2528
build_bcm_header,
2629
build_bcm_transmit_header,
2730
build_bcm_tx_delete_header,
2831
build_bcm_update_header,
32+
build_can_frame,
33+
capture_message,
2934
)
3035

3136
from .config import IS_LINUX, IS_PYPY, TEST_INTERFACE_SOCKETCAN
@@ -390,6 +395,44 @@ def test_pypy_socketcan_support(self):
390395
"See: https://github.com/pypy/pypy/issues/3808"
391396
)
392397

398+
def test_capture_message(self):
399+
ancillary_data = [
400+
(
401+
socket.SOL_SOCKET,
402+
SO_TIMESTAMPNS,
403+
RECEIVED_TIMESTAMP_STRUCT.pack(1_700_000_000, 123_456_000),
404+
)
405+
]
406+
defaults = {"timestamp": 1_700_000_000.123456, "is_extended_id": False}
407+
# Every pair of boolean fields differs in at least one message, so swapped
408+
# arguments in the Message() call of capture_message() cannot go unnoticed.
409+
for kwargs in (
410+
{"arbitration_id": 0x123, "channel": "vcan0", "data": [1, 2, 3]},
411+
{"arbitration_id": 0x1ABCDEF, "is_extended_id": True, "is_rx": False},
412+
{"arbitration_id": 0x7FF, "is_remote_frame": True, "dlc": 4},
413+
{"arbitration_id": 0x004, "is_error_frame": True, "data": bytes(8)},
414+
{"arbitration_id": 0x456, "is_fd": True, "data": range(12)},
415+
{"arbitration_id": 0x456, "is_fd": True, "bitrate_switch": True},
416+
{"arbitration_id": 0x456, "is_fd": True, "error_state_indicator": True},
417+
):
418+
with self.subTest(**kwargs):
419+
expected = can.Message(**{**defaults, **kwargs})
420+
sock = Mock()
421+
sock.recvmsg.return_value = (
422+
build_can_frame(expected),
423+
ancillary_data,
424+
0 if expected.is_rx else socket.MSG_DONTROUTE,
425+
("vcan0",),
426+
)
427+
428+
received = capture_message(
429+
sock, get_channel=expected.channel is not None
430+
)
431+
432+
self.assertTrue(
433+
received.equals(expected), f"{received!r} != {expected!r}"
434+
)
435+
393436

394437
if __name__ == "__main__":
395438
unittest.main()

0 commit comments

Comments
 (0)