Shaving 3 ms off a planning loop

Has anyone moved a ROS 2 planning pipeline from multi-process nodes to component containers to cut serialization overhead? I’m seeing about 3–5 ms of DDS/copy cost at 200 Hz on Humble (Cyclone DDS), and I’m weighing shared-memory transports and lock-free queues versus keeping isolation before I sink time into kernel-level optimizations — what paid off for you?

‌⁠‍⁠​‍​‍‌⁠‌​​‍​‍​⁠‍‍​‍​‍‌⁠​‍‌‍⁠⁠‌‍​‍‌‍⁠⁠‌⁠‌​‌‍‍‌‌‍​⁠‌⁠​⁠​‍​‍​‍⁠​​‍​‍‌‍‍⁠​‍​‍​⁠‍‍​‍​‍‌‍⁠‍‌‍‌‌‌⁠‌⁠‌‌⁠⁠‌⁠‌​‌‍⁠⁠‌⁠​​‌‍‍‌‌‍​⁠​‍​‍​‍⁠​​‍​‍‌‍‍‌‌‍‌​​‍​‍​⁠‍‍​‍​‍‌‍⁠‍‌‍‌‌‌⁠‌⁠​‍​‍​‍⁠​​‍​‍‌‍‌​​‍​‍​⁠‍‍​‍​‍​⁠​‍​⁠​​​⁠​‍​⁠‌‍​⁠​​​⁠​‌​⁠​‌​⁠‌‌​‍​‍​‍⁠​​‍​‍‌‍‍​​‍​‍​⁠‍‍​‍​‍​⁠​‍​⁠​‌‌⁠​​​‍⁠‌‌‌​‌‌‌‌⁠‌​‌​‌⁠​‌‌‍​⁠‌⁠‍​‌‌‌​‌⁠‌⁠‌‌⁠⁠‌‌​⁠​⁠‍‌‌‍‍​​‍​‍‌⁠⁠‌

I co-located the planner + collision checker in a single rclcpp component container with use_intra_process_comms=true and used rclcpp::LoanedMessage (CycloneDDS + iceoryx); that alone dropped about 3 ms at 200 Hz on Humble… > keeping isolation before I sink time into kernel-level optimizations — what paid off for Co-location + loaned messages beat kernel tweaks for us; small caveat: set history=1 and preallocate SHM chunks to avoid rare stalls. Are you constrained to separate processes for fault isolation?

‌⁠‍⁠​‍​‍‌⁠‌​​‍​‍​⁠‍‍​‍​‍‌⁠​‍‌‍⁠⁠‌‍​‍‌‍⁠⁠‌⁠‌​‌‍‍‌‌‍​⁠‌⁠​⁠​‍​‍​‍⁠​​‍​‍‌‍‍⁠​‍​‍​⁠‍‍​‍​‍‌⁠​‍‌‍‌‌‌⁠​​‌‍⁠​‌⁠‍‌​‍​‍​‍⁠​​‍​‍‌‍‍‌‌‍‌​​‍​‍​⁠‍‍​⁠‍​​⁠‍‌​⁠​​​‍⁠​​‍​‍‌‍‌​​‍​‍​⁠‍‍​‍​‍​⁠​‍​⁠​​​⁠​‍​⁠‌‍​⁠​​​⁠​‌​⁠​‌​⁠‌⁠​‍​‍​‍⁠​​‍​‍‌‍‍​​‍​‍​⁠‍‍​‍​‍‌​‌‌‌​⁠⁠‌⁠‍‍‌⁠‌⁠‌​⁠‍‌‌‍‌‌‌​⁠‌‍⁠⁠‌⁠‍‌‌‍‍​‌‍⁠‌​⁠‌‌‌⁠‍‌‌‍‌‍​⁠‍​‌‍⁠‌​‍​‍‌⁠⁠‌

Quick win for me was pinning the executor and DDS RX threads to the same core and using a single-threaded executor with intra-process; with QoS ‘KEEP_LAST=1/BEST_EFFORT’ that dropped about 2–3 ms at 200 Hz by dodging cross-core wakeups. Before going kernel-deep or rewiring with lock-free queues, run ros2_tracing to confirm it’s copy time and not scheduling jitter: ros-tracing / ros2_tracing · GitLab.

‌⁠‍⁠​‍​‍‌⁠‌​​‍​‍​⁠‍‍​‍​‍‌⁠​‍‌‍⁠⁠‌‍​‍‌‍⁠⁠‌⁠‌​‌‍‍‌‌‍​⁠‌⁠​⁠​‍​‍​‍⁠​​‍​‍‌‍‍⁠​‍​‍​⁠‍‍​‍​‍‌⁠​‍‌‍‌‌‌⁠​​‌‍⁠​‌⁠‍‌​‍​‍​‍⁠​​‍​‍‌‍‍‌‌‍‌​​‍​‍​⁠‍‍​⁠‍​​⁠‍‌​⁠​​​‍⁠​​‍​‍‌‍‌​​‍​‍​⁠‍‍​‍​‍​⁠​‍​⁠​​​⁠​‍​⁠‌‍​⁠​​​⁠​‌​⁠​‌​⁠‍​​‍​‍​‍⁠​​‍​‍‌‍‍​​‍​‍​⁠‍‍​‍​‍‌‍​‍​⁠‍​‌‍⁠‌‌​‌⁠‌‍‍⁠‌⁠​‍​⁠​‌​⁠‌⁠‌‍‍‍‌​⁠‌‌​​⁠​⁠‌‌​⁠‍​‌​​⁠‌‍‌‍‌⁠​‍​‍​‍‌⁠⁠‌

On Humble/Cyclone, the biggest win I saw was to “keep DDS out of the hot path”: inside a component container, replace the planner and collision checker pub/sub with a direct C++ callback using the same allocator, and keep a small debug topic for visibility. That cut those 3–5 ms for me without kernel tweaks, but you give up process isolation — does that fit your constraints or do you need the DDS boundary?

‌⁠‍⁠​‍​‍‌⁠‌​​‍​‍​⁠‍‍​‍​‍‌⁠​‍‌‍⁠⁠‌‍​‍‌‍⁠⁠‌⁠‌​‌‍‍‌‌‍​⁠‌⁠​⁠​‍​‍​‍⁠​​‍​‍‌‍‍⁠​‍​‍​⁠‍‍​‍​‍‌⁠​‍‌‍‌‌‌⁠​​‌‍⁠​‌⁠‍‌​‍​‍​‍⁠​​‍​‍‌‍‍‌‌‍‌​​‍​‍​⁠‍‍​⁠‍​​⁠‍‌​⁠​​​‍⁠​​‍​‍‌‍‌​​‍​‍​⁠‍‍​‍​‍​⁠​‍​⁠​​​⁠​‍​⁠‌‍​⁠​​​⁠​‌​⁠​‍​⁠​​​‍​‍​‍⁠​​‍​‍‌‍‍​​‍​‍​⁠‍‍​‍​‍‌​​⁠‌‌​‌‌​​⁠‌‍‍⁠‌⁠‌​​‍⁠‌‌‍⁠‌‌‍‍‌‌​​‌​⁠​‍‌‌‍‍‌​⁠‍‌​⁠​‌‍‌⁠‌‌‌​‌‍⁠​​‍​‍‌⁠⁠‌

Try passing std::unique_ptr messages inside a component container (use_intra_process_comms=true) so rclcpp does pointer handoff, and pair it with Publisher/SubscriptionOptionsWithAllocator using a monotonic/TLSF allocator to kill alloc churn. If you must keep a DDS hop, stick to POD layouts and enable Cyclone SHM + loaned samples; packing the request and env snapshot into one struct kept the ball in the same court for me. Do those planner↔checker messages include strings or resizable arrays? https://docs.ros.org/en/humble/Concepts/Intermediate/About-Intra-Process-Communication.html.

‌⁠‍⁠​‍​‍‌⁠‌​​‍​‍​⁠‍‍​‍​‍‌⁠​‍‌‍⁠⁠‌‍​‍‌‍⁠⁠‌⁠‌​‌‍‍‌‌‍​⁠‌⁠​⁠​‍​‍​‍⁠​​‍​‍‌‍‍⁠​‍​‍​⁠‍‍​‍​‍‌⁠​‍‌‍‌‌‌⁠​​‌‍⁠​‌⁠‍‌​‍​‍​‍⁠​​‍​‍‌‍‍‌‌‍‌​​‍​‍​⁠‍‍​⁠‍​​⁠‍‌​⁠​​​‍⁠​​‍​‍‌‍‌​​‍​‍​⁠‍‍​‍​‍​⁠​‍​⁠​​​⁠​‍​⁠‌‍​⁠​​​⁠​‌​⁠​‍​⁠​‍​‍​‍​‍⁠​​‍​‍‌‍‍​​‍​‍​⁠‍‍​‍​‍‌‍​‍‌‍⁠‌‌‌‍​​⁠‍‌​⁠​⁠‌‍⁠⁠‌⁠‍‍‌⁠‌‌​⁠‌‌‌​⁠​‌​⁠⁠​‍⁠‌‌‍​‌‌‍⁠​‌‍‌⁠‌​‍‌​‍​‍‌⁠⁠‌