Skip to content

Commit

Permalink
Read serial line in chunks instead of character by character
Browse files Browse the repository at this point in the history
This significantly reduces CPU load caused by this driver.

Without this change, CPU load on a powerful x86 machine with a
USB-connected GPS is 36%. The output of the strace command looks
like this:

    ...
    pselect6(35, [33 34], [], [], {tv_sec=1, tv_nsec=999999000}, NULL) = 1 (in [33], left {tv_sec=1, tv_nsec=999997160})
    read(33, "$", 1)                        = 1
    pselect6(35, [33 34], [], [], {tv_sec=1, tv_nsec=999999000}, NULL) = 1 (in [33], left {tv_sec=1, tv_nsec=999997160})
    read(33, "G", 1)                        = 1
    pselect6(35, [33 34], [], [], {tv_sec=1, tv_nsec=999999000}, NULL) = 1 (in [33], left {tv_sec=1, tv_nsec=999997160})
    read(33, "P", 1)                        = 1
    pselect6(35, [33 34], [], [], {tv_sec=1, tv_nsec=999999000}, NULL) = 1 (in [33], left {tv_sec=1, tv_nsec=999997180})
    read(33, "R", 1)                        = 1
    ...

It can be seen that characters are read from the serial line one by
one. This is due to how IOBase.readline() interacts with pySerial.

In this commit, we implement the same functionality as
IOBase.readline(), but in a way allowing to read the serial line
characters in bigger chunks. With it, CPU load decreases to 7.5% and
strace reports this:

    ...
    pselect6(35, [33 34], [], [], {tv_sec=1, tv_nsec=926286000}, NULL) = 1 (in [33], left {tv_sec=1, tv_nsec=906510472})
    read(33, "$GPRMC,,V,,,,,,,,,,N*53\r\n$GPGGA,"..., 422) = 143
    pselect6(35, [33 34], [], [], {tv_sec=1, tv_nsec=906287000}, NULL) = 1 (in [33], left {tv_sec=1, tv_nsec=886945652})
    read(33, "$GPRMC,,V,,,,,,,,,,N*53\r\n$GPGGA,"..., 279) = 143
    pselect6(35, [33 34], [], [], {tv_sec=1, tv_nsec=886721000}, NULL) = 1 (in [33], left {tv_sec=1, tv_nsec=866882570})
    read(33, "$GPRMC,,V,,,,,,,,,,N*53\r\n$GPGGA,"..., 136) = 136
    pselect6(35, [33 34], [], [], {tv_sec=1, tv_nsec=999996000}, NULL) = 1 (in [33], left {tv_sec=1, tv_nsec=999991549})
    read(33, ",,*57\r\n", 1024)             = 7
    pselect6(35, [33 34], [], [], {tv_sec=1, tv_nsec=999827000}, NULL) = 1 (in [33], left {tv_sec=1, tv_nsec=985949688})
    read(33, "$GPRMC,,V,,,,,,,,,,N*53\r\n$GPGGA,"..., 1017) = 143
    pselect6(35, [33 34], [], [], {tv_sec=1, tv_nsec=985746000}, NULL) = 1 (in [33], left {tv_sec=1, tv_nsec=966235438})
    read(33, "$GPRMC,,V,,,,,,,,,,N*53\r\n$GPGGA,"..., 874) = 143
    ...

Reading from the kernel happens most of the time in chunks of 143
bytes (for our GPS) and therefore, the system call overhead is 143×
lower.

Tested on Septentrio mosaic-H GPS.
  • Loading branch information
wentasah committed Apr 7, 2023
1 parent 06d71c7 commit c463711
Showing 1 changed file with 22 additions and 11 deletions.
33 changes: 22 additions & 11 deletions src/libnmea_navsat_driver/nodes/nmea_serial_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
# POSSIBILITY OF SUCH DAMAGE.

import serial
import io

import rclpy

Expand All @@ -50,18 +51,28 @@ def main(args=None):
GPS = serial.Serial(port=serial_port, baudrate=serial_baud, timeout=2)
driver.get_logger().info("Successfully connected to {0} at {1}.".format(serial_port, serial_baud))
try:
data = bytearray()
while rclpy.ok():
data = GPS.readline().strip()
try:
if isinstance(data, bytes):
data = data.decode("utf-8")
driver.add_sentence(data, frame_id)
except ValueError as e:
driver.get_logger().warn(
"Value error, likely due to missing fields in the NMEA message. Error was: %s. "
"Please report this issue at github.com/ros-drivers/nmea_navsat_driver, including a bag file "
"with the NMEA sentences that caused it." % e)

data.extend(GPS.read(1024)) # read at most 1024 bytes
lines = data.splitlines(keepends=True)
data.clear()
def process_line(line):
line = line.decode("utf-8").rstrip()
try:
driver.add_sentence(line, frame_id)
except ValueError as e:
driver.get_logger().warn(
"Value error, likely due to missing fields in the NMEA message. Error was: %s. "
"Please report this issue at github.com/ros-drivers/nmea_navsat_driver, including a bag file "
"with the NMEA sentences that caused it." % e)
# process complete lines
for line in lines[0:-1]:
process_line(line)
if lines[-1].endswith(b'\r\n'):
process_line(lines[-1])
else:
# continue with the last, incomplete line
data = lines[-1]
except Exception as e:
driver.get_logger().error("Ros error: {0}".format(e))
GPS.close() # Close GPS serial port
Expand Down

0 comments on commit c463711

Please sign in to comment.