Perception › ROS 2 implementation › Acronym glossary

Middleware and real-time engineering

The architecture names the nodes and their deadlines. This tutorial explains what happens between two callbacks, why QoS is a safety choice, and exactly when an image or point cloud moves without a copy. Numbers below are design budgets, not measured guarantees; validate them on the target hardware and ROS distribution.

1 · Follow a message through ROS 2

application node → rclcpp (publishers, callback groups, intra-process manager) → rcl (graph, time, parameters) → rmw (middleware adapter) → DDS/Zenoh (discovery, QoS) → SHM or UDP transport

A node is a collection of entities, not a thread or process. A context initializes the middleware; a container holds multiple nodes in one process. Performance diagnosis starts at the correct layer: CDR fragmentation/transport for a multi-megabyte cloud, executor dispatch for callback jitter, CUDA event dependencies for inference stalls.

rmwReason to selectQualification question
Cyclone DDSConfigurable peers, PSMX/iceoryx shared-memory pathIs this build's PSMX actually enabled and sized for the samples?
Fast DDSWidely deployed; SHM transport and async publisher optionDoes its selected type/transport loan memory, or merely serialize into SHM?
Connext DDSQoS and observability toolingLicensing and target availability?
Zenoh rmwRouter-based discovery, useful across networksAre its release-specific QoS and SHM guarantees tested?

Constrain discovery on the vehicle network; pin the rmw version and test large-message transport under packet loss. One participant per node is not a portable assumption: participant ownership is implementation/version-specific.

Time contract: stamp at hardware capture (PTP/trigger), use ROS time for message stamps and bag replay, a monotonic steady clock for latency and watchdog durations, and never assume cross-topic DDS ordering. For six cameras plus a LiDAR sweep, exact sync is brittle without common triggers; approximate sync may wait a whole period. Sweep-anchored sync picks the nearest image within a bounded slop and carries a valid-view mask. Cache static TF from map → odom → base_link → sensors at configuration; never block a model callback on TF. Use continuous odom for derivatives: map → odom can jump on loop closure.

2 · Design QoS per link, not per project

Ask five questions: (1) Is a late sample worse than a missing one? (2) How large is it, and can it cross a process boundary without fragments? (3) Must a restarted subscriber receive the previous value? (4) What depth absorbs producer jitter without retaining stale work? (5) Which deadline, lifespan and liveliness events expose failure? KEEP_LAST(1) bounds a queue but silently drops data: publish sequence-gap counters.

Link (nominal)Reliability / history / durabilityDeadline · lifespanTrade-off / failure if wrong
6× camera raw (10–30 Hz, 2–6 MB)Best effort / last 1–2 / volatile1.5 periods · 100 msReliable retransmission of a fragmented stale frame blocks the next.
CameraInfo (on change)Reliable / last 1 / transient local—Late joiners need intrinsics; validate stamp and calibration revision.
LiDAR full sweeps (10–20 Hz, 3–10 MB)Best effort / last 1 / volatile1.5 periods · 150 msKeep-last 10 builds an old-sweep backlog; 1 trades completeness for age.
LiDAR raw packets (~kHz, small)Best effort / last 100–200 / volatile— · 50 msSmall bursts need depth; track missing azimuth slices.
Radar tracks (10–20 Hz)Best effort or reliable / last 5 / volatile2 periods · 200 msIndependent AEB fallback may warrant reliable on a separate executor.
IMU (100–1000 Hz)Best effort / last 10–50 / volatile2 periods · 20 msIntegrator needs ordered history; track gaps. Batched IMU reduces callback count.
GNSS fix / velocity (1–10 Hz)Reliable / last 5 / volatile2 periods · 1 sFew small fixes; reject poor status/covariance despite successful delivery.
Wheel/VIO odom (50–200 Hz)Best effort or reliable / last 10 / volatile2 periods · 100 msFreshest for control; history for state estimation.
Dynamic TFReliable / last 100 / volatile—Transform buffers need history for stamped lookup.
Static TFReliable / sufficient depth / transient local—Restarted nodes need extrinsics; check multiple-publisher retention.
SensorFrame / GPU tensors (intra-process)Best effort / last 1 / volatile120 ms · boundedNever latch process-local CUDA pointers or let them escape the container.
Detections3D (10 Hz)Reliable / last 5 / volatile150 ms · 300 msUseful for tracker, but consumer must reject stale timestamps.
TrackArray (10 Hz)Reliable / last 5 / volatile (optional transient local)150 ms · 500 msLatch helps restart but may deliver an obsolete world state; check age.
BEV segmentation / occupancy (5–10 Hz)Best effort / last 1 / volatile250 ms · 500 msLarge self-contained grids supersede earlier ones.
Static map (on change)Reliable / last 1 / transient local—Large but late subscribers require it; budget writer-cache memory.
Actuator command (50–200 Hz)Reliable / last 1 / volatile1.2 periods · 2 periodsAlso use an independent wall/steady-time actuator watchdog; DDS events may be delayed.
Joint states (100–500 Hz)Best effort / last 10 / volatile2 periods · 50 msLatest state wins.
Diagnostics / rosout (1–10 Hz)Reliable / last 10–100 / volatile—Important evidence but never on the model executor.
Parameter events / service endpointsService/default reliable profileNot a streaming freshness contractKeep configuration off the critical path.
Request/offered compatibility: best-effort publisher + reliable subscription = no match; reliable publisher + best-effort subscriber works. Volatile publisher + transient-local subscriber cannot match; the reverse can. A subscriber demanding a tighter deadline than the publisher offers cannot match. Depth and lifespan do not establish endpoint compatibility. If a sensor appears empty in rviz or ros2 topic echo, inspect offered QoS before debugging drivers.
ros2 topic info /lidar/points --verbose
ros2 topic echo /lidar/points --qos-reliability best_effort --qos-depth 1 --once

Wire requested/offered deadline, liveliness, and incompatible-QoS event callbacks into health_node. A deadline is a signal, not a hard scheduling guarantee. A manual liveliness lease must be asserted after completed inference, not before a GPU hang; separately poll the CUDA completion event. Size DDS resource limits, socket buffers and fragment limits, and prefer async publishing if large inter-process serialization would block a callback.

3 · Executors: dispatch is a scheduling decision

An executor waits on subscriptions, timers, services, clients and guard conditions then runs ready callbacks. The usual single/multithreaded executors do not promise strict priority, fairness or a worst-case dispatch time. Dispatch details (including timer ordering) change across distributions; measure and test on your pinned ROS version. A one-thread executor serializes everything; a multithreaded executor permits parallel groups, but its default mutually-exclusive group serializes all callbacks within each node.

ChoiceGood forCost / caution
SingleThreadedExecutorSmall deterministic eval containerOne blocking callback delays all others.
MultiThreadedExecutor (explicit thread count)Independent camera, LiDAR and head groupsNot a priority scheduler; unlimited threads contend with CUDA and DDS.
StaticSingleThreaded / EventsExecutorFixed/high-entity-count graph, when supportedDistribution/version availability and semantics require benchmarking.
Dedicated callback-group executors / WaitSetDifferent criticalities, explicit orderingMore lifecycle and shutdown plumbing; verify bounded work per turn.
ingest executor (cores 4–5): camera + LiDAR callbacks, short, independently reentrant GPU executor (cores 6–9): camera encoder || LiDAR encoder → fuser → detection / seg housekeeping (cores 10–11): services, parameters, TF, diagnostics, visualization separate GNC process (cores 2–3): high-priority control, never on a GPU thread

Put a stateful tracker and each single-context TensorRT stage into MutuallyExclusive groups; use Reentrant only where concurrent callbacks have independent storage. Explicitly routed groups must be created with automatically_add_to_executor_with_node=false, then attached by add_callback_group(group, node_base) before spinning. Put only remaining default groups into the housekeeping executor; do not add the same node indiscriminately to multiple executors.

auto gpu_group = node->create_callback_group(
    rclcpp::CallbackGroupType::MutuallyExclusive, false);
gpu_exec.add_callback_group(gpu_group, node->get_node_base_interface());
std::thread gpu_thread([&] {
    set_affinity({6,7,8,9});  // implemented in perception_common/thread_utils.hpp
    gpu_exec.spin();
});

Failure drills: same-group synchronous service call deadlocks; default groups unexpectedly serialize; an overactive timer starves sensor work; depth-one queues hide skips; reentrant tracker corrupts identities; engine deserialization or TF wait inside a callback freezes the pipeline; implicit hardware-concurrency thread counts thrash an embedded SoC. Use traces and counters to demonstrate each fix.

4 · One process, multiple nodes and threads

Register each component with the component macro and forward the incoming NodeOptions to its base constructor: remaps, parameter overrides and intra-process configuration otherwise disappear. For bring-up, use component_container_mt plus use_intra_process_comms=true; for bounded executor ownership use the custom perception_container_main with three executors. Dynamic components allow loading heads at runtime; the custom main makes allocation, affinities and explicit group routing inspectable.

ArchitectureGainRisk / boundary
Separate process per model stageFault isolationCDR copies, duplicate GPU contexts, discovery and memory costs.
One perception containerPointer handoffs, shared CUDA pool/context and parallel streamsAny segfault kills all stages; watchdog + process supervisor required.
Separate driver, GNC, eval, loggerPrivilege, scheduling and failure isolationCross-process results must be age-checked; heavy images need SHM or optimized intake.

Lifecycle bring-up: configure calibration, deserialize engines and allocate fixed pools before motion; activate publishers only after checks pass; on GPU fault deactivate, emit FAULT, then attempt cleanup/reconfigure while the GNC process owns safe stop. Hot-swapping a detector without checking input/output contracts and vehicle state is not safe merely because dynamic loading is available.

5 · Four different meanings of “zero copy”

MechanismScopePreconditionsWhat still copies?
A · intra-process ownership moveSame processunique_ptr publish, single owning subscriber, volatile, intra-process enabledAdding another owning subscriber or publishing by const reference may copy.
B · intra-process read-only shareSame process, fan-outConst shared callbacks, same allocation and refcounted lifetimeAn exclusive/mutating subscriber requires a copy.
C · rmw loan / SHMCross process, if supportedActually loanable fixed-size/POD message, matching rmw/SHM configurationDynamic ROS sequences, rmw fallback, CPU→GPU transfer.
D · TypeAdapter / GPU handleGPU data shared inside processDevice buffer owner, process guard, stream completion event, explicit wire conversionIngest H2D (unless true camera DMA/import), any external debug subscriber conversion.

A/B: a moved image has the same object address at publisher and owning subscriber. With multiple read-only consumers prefer a shared ownership callback; never retain a reference beyond callback lifetime. Volatile durability is necessary for the intra-process hot path; depth-one can discard an unprocessed frame, so count it. An external subscriber may cause a second serialized publication even while internal delivery is copy-free; never promise no serialization just because the nodes are composed.

auto image = std::make_unique<sensor_msgs::msg::Image>();
auto* before = image.get();
pub->publish(std::move(image)); // compare 'before' with owning callback's m.get()

D: the GPU adapter carries a pool-owned CUDA allocation, dimensions, encoding, producer PID and readiness event. The consumer calls cudaStreamWaitEvent(consumer_stream, ready_event), not device-wide synchronization. An address alone is not inter-process transferable: use CUDA IPC/NvSci with ownership protocol if needed, or stage into a safe wire type. On integrated-GPU SoCs evaluate mapped pinned/NvSci buffers; on discrete GPUs budget H2D and host pinning. “GPU-direct camera” requires a capture path that imports DMA buffers into the device; wrapping a CPU image in a pointer does not eliminate H2D.

Process-local GPU-handle topics must not be mistaken for a portable wire format. Reject mismatched producer PIDs, keep a refcounted pool allocation alive until the last consumer and its GPU work finish, and verify zero intermediate memcpy operations with Nsight. For debugging/rosbag, an explicit adapter produces standard Image or PointCloud2 at a chosen rate; that conversion intentionally copies.

6 · Loaned messages and the driver boundary

For a separate privileged LiDAR driver, small packets can cross via standard messages. If a decoded full sweep must cross processes without a CPU copy, use an rmw-supported, fixed-capacity point array with num_points; for a 1920×1080 RGB image the fixed RGB8 array needs 6,220,800 bytes. Standard Image.data and PointCloud2.data are dynamically sized, so borrow_loaned_message() cannot be assumed to provide true middleware-backed zero copy for them. can_loan_messages() and transport instrumentation distinguish a real loan from heap fallback.

Fixed-capacity PODStandard dynamic ROS message
SHM loan may avoid serialization and second CPU copyExcellent rviz, bag and ecosystem interoperability
Worst-case chunk × history depth; ABI/capacity constraintsUses actual payload size; may serialize/copy across process
Custom converter required for toolingEasy replay and recording

Prefer the decoder inside the perception container: de-skew each point with its time offset and ego odometry, then upload/decode into GPU tensors once. A fixed-capacity packet or sweep sample is useful only when measured cross-process cost justifies custom types. Fast DDS SHM + async publish and Cyclone PSMX/iceoryx both require release-specific configuration, sufficient chunk pools and verified fallback behavior; SHM transport is not synonymous with a middleware loan.

7 · Services, actions, parameters and lifecycle

NeedMechanismWhy not another mechanism?
Camera frames, tracks, healthTopics + per-link QoSServices serialize requests and have no streaming freshness contract.
Reset evaluation counters or retrieve one snapshotShort service in housekeeping/eval executorNever wait synchronously inside a model callback.
Recalibrate / load a long-running model with feedbackAction or managed lifecycle transition while parkedA service timeout cannot safely cancel prolonged work.
Calibration / map for a restarted subscriberReliable transient-local topic (versioned)Subscriber must validate stamp/revision before use.
Model path, point-cloud layoutRead-only startup parameter; change via deactivate/configureChanging tensor geometry in-flight corrupts buffers.
Detection threshold, camera slopValidated dynamic parameter with atomic snapshotValidate ranges and log version; avoid partial mid-frame changes.

Declare parameters with descriptors, ranges and read-only restrictions; validate through an on-set callback, snapshot a consistent configuration at the frame boundary, and version changes in recordings. Use configure → activate → deactivate → cleanup lifecycle transitions to prevent engine compilation inside an active deadline. Do not call spin_until_future_complete from a callback whose response needs the same single-threaded/mutually-exclusive executor; use asynchronous client callbacks or a separately routed group.

8 · Make a real-time claim falsifiable

Preallocate CUDA, host and message pools; prebuild TRT engines; lock critical memory and prefault stacks; avoid filesystem/allocations/blocking TF in callbacks. Pin critical threads and prioritize GNC separately from GPU work; SCHED_FIFO requires explicit privileges and bounded callback execution or it can starve the entire system. Use stream events for camera/LiDAR overlap and serialize the fuser only when its inputs are ready. Keep only the newest pending sweep when inference exceeds its 100 ms period; drop and count the rest rather than accumulating latency.

ClaimMeasurement / fail condition
One-process movePublisher/subscriber pointer identity in the one-owner case; refcounted fan-out separately.
Real SHM loancan_loan_messages(), rmw transport introspection and no serialize/copy trace.
No GPU intermediate copyNsight timeline: H2D at ingest only unless DMA import; no BEV tensor D2H/H2D.
Bounded freshnessp50/p95/p99/p99.9 capture-to-track age and drop rate in hot/cold, loss and thermal tests.
Controlled degradationStop one camera, hang GPU, restart planner, corrupt TF and impose disk load; verify GNC response.

Compare ROS tracepoints (publish → callback), CUDA stream events, PTP/hardware timestamps and steady-clock age. An executor hop microbenchmark is not a total system deadline proof. Also measure detection quality conditioned on dropped frames; a stack that meets 95 ms by discarding hard frames has not solved the vehicle problem.