Skip to content

Commit 48bb561

Browse files
committed
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
1 parent b4f82ab commit 48bb561

3 files changed

Lines changed: 59 additions & 16 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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"""
66

77
import ctypes
8+
import inspect
9+
import socket
810
import struct
911
import sys
1012
import unittest
@@ -20,6 +22,7 @@
2022
TX_COUNTEVT,
2123
)
2224
from can.interfaces.socketcan.socketcan import (
25+
MSG_DONTROUTE,
2326
BcmMsgHead,
2427
bcm_header_factory,
2528
build_bcm_header,
@@ -391,5 +394,43 @@ def test_pypy_socketcan_support(self):
391394
)
392395

393396

397+
class SocketCANHotPathTest(unittest.TestCase):
398+
"""Guard the per-frame optimisations in :func:`capture_message`."""
399+
400+
def test_msg_dontroute_is_plain_int(self):
401+
"""``socket.MSG_DONTROUTE`` is an ``enum.IntFlag`` member on CPython, and
402+
evaluating ``msg_flags & <IntFlag>`` once per received frame constructs a new
403+
flag instance each time. ``capture_message`` therefore tests against a plain
404+
int, which must still carry the same value.
405+
"""
406+
self.assertEqual(MSG_DONTROUTE, socket.MSG_DONTROUTE)
407+
self.assertIs(type(MSG_DONTROUTE), int)
408+
409+
def test_capture_message_message_args(self):
410+
"""``capture_message`` builds its :class:`~can.Message` positionally to avoid
411+
per-frame keyword binding, so a reordering of ``Message.__init__`` would
412+
silently corrupt every received frame. Pin the expected order.
413+
"""
414+
params = list(inspect.signature(can.Message.__init__).parameters)
415+
self.assertEqual(
416+
params[:13],
417+
[
418+
"self",
419+
"timestamp",
420+
"arbitration_id",
421+
"is_extended_id",
422+
"is_remote_frame",
423+
"is_error_frame",
424+
"channel",
425+
"dlc",
426+
"data",
427+
"is_fd",
428+
"is_rx",
429+
"bitrate_switch",
430+
"error_state_indicator",
431+
],
432+
)
433+
434+
394435
if __name__ == "__main__":
395436
unittest.main()

0 commit comments

Comments
 (0)