On-Orin test runbook for ros2_pulse¶
Goal: on the real robot, reproduce the same data we measured locally: per-topic Hz (inter +
intra), CPU overhead (probe ON vs OFF), network cost, SHM on/off, and the bake-off vs eBPF /
LTTng. Run everything inside the ROS container on the Orin. Adjust ros2_dev /
/root/ros2_ws to your actual container name / workspace path.
Nothing here modifies the running stack except toggling one env var + restarting (Phase 2/3).
Results of the 2026-08-23 run on a production AGX Orin: RESULTS.md, raw
files under out/.
Phase 0: build the package on the Orin¶
# The repo is private and the robot must not end up holding credentials for it. Two ways in,
# neither leaves a .git/ or a remote on the robot:
# (a) no token at all: archive from your checkout, copy over ssh:
git archive --format=tar.gz --prefix=ros2_pulse/ -o /tmp/ros2_pulse-v0.3.0.tar.gz v0.3.0
scp /tmp/ros2_pulse-v0.3.0.tar.gz <orin>:/tmp/
# on the Orin host: docker cp /tmp/ros2_pulse-v0.3.0.tar.gz ros2_dev:/tmp/
# in the container: tar -xzf /tmp/ros2_pulse-v0.3.0.tar.gz -C /root/ros2_ws/src/
# (b) robot has egress: short-lived read-only token, pasted without echo or history:
# in the container:
# set +o history; read -rs PAT
# mkdir -p /root/ros2_ws/src/ros2_pulse
# curl -fsSL -H "Authorization: Bearer $PAT" \
# https://api.github.com/repos/TanayK07/ros2_pulse/tarball/v0.3.0 \
# | tar -xz --strip-components=1 -C /root/ros2_ws/src/ros2_pulse
# unset PAT; set -o history # and revoke the token on GitHub when the run is over
# then, inside the container:
cd /root/ros2_ws
colcon build --packages-select ros2_pulse
source install/setup.bash
Teardown afterwards: remove src/ros2_pulse, build/ros2_pulse, install/ros2_pulse (on the
host side if the workspace is a bind mount), the probe's output files, and any env-file lines
added for Phase 2, then restart the stack once so no process still references the deleted
.so. Revoke the token if (b) was used.
Phase 1: does it work here, and what data do we get? (the core ask)¶
# 1a. Confirm the Orin image is instrumented (must be non-zero):
TT=$(find /opt/ros -name 'libtracetools.so*' | head -1)
nm -D "$TT" | grep -c ros_trace # expect >0 (rclcpp calls these; our probe hooks them)
# 1b. Check the LTTng-backend question that captured 0 events locally:
ldd "$TT" | grep -i lttng || echo "NOT linked to lttng-ust -> ros2_tracing needs a rebuild here too"
# 1c. Run a quick controlled graph under the probe (no need to touch the real stack yet):
export LD_PRELOAD=$(find /root/ros2_ws/install -name libros2_pulse.so | head -1)
export ROS_TOPIC_STATS_OUTPUT_FILE=/tmp/orin_probe.log
export ROS_TOPIC_STATISTICS_PUBLISH_PERIOD=2.0
# inter-process (2 procs):
ros2 run demo_nodes_cpp talker & ros2 run demo_nodes_cpp listener &
sleep 8; kill %1 %2
cat /tmp/orin_probe.log # expect TOPIC /chatter <hz> + RECV ... + NODE lines
To see the real stack's topics, just launch the stack normally; LD_PRELOAD is already wired
into the deploy env (docker_v2/config/ros2_env_common.conf), so topic_freq.log
(/root/ssd2tb/logs/topic_freq.log) fills with every real topic's inter/intra Hz + active nodes.
Watch it live:
tail -f /root/ssd2tb/logs/topic_freq.log
v0.3.0 path change: the default output path is now
$TMPDIR/topic_freq.<pid>.log(/tmpfallback),/root/ssd2tb/logs/...only fills if the deploy env still setsROS_TOPIC_STATS_OUTPUT_FILEexplicitly. Check the env conf; if it relies on the old default, the log moved to/tmp/topic_freq.<pid>.log(one file per process).
Look specifically for intra=<nonzero> on composable/point-cloud topics, that's the data no
rmw-level tool or built-in stat can give you.
# 1d. v0.3.0 feature spot-checks against the demo pair from 1c (30 s total):
export ROS_TOPIC_STATS_OUTPUT_FILE=/tmp/orin_v030.log
ROS_TOPIC_STATS_JITTER=1 timeout 10 ros2 run demo_nodes_cpp talker & timeout 10 ros2 run demo_nodes_cpp listener; wait
grep JITTER /tmp/orin_v030.log # expect: JITTER /chatter pub|recv max_dt_ms=~1000±jitter
ROS_TOPIC_STATS_FORMAT=jsonl timeout 10 ros2 run demo_nodes_cpp talker & timeout 10 ros2 run demo_nodes_cpp listener; wait
tail -1 /tmp/orin_v030.log | python3 -m json.tool >/dev/null && echo "jsonl parses"
ROS_TOPIC_STATS_QUIET=1 timeout 5 ros2 run demo_nodes_cpp talker 2>&1 | grep -c ros2_pulse # expect: 0
Phase 1.5: hot-path microbench: the +24 ns / Tegra clock question (REQUIRED before public)¶
Every published perf number is x86-64. The one most at risk on Tegra: ROS_TOPIC_STATS_JITTER=1
costs +24 ns/msg, ~96% of which is one CLOCK_MONOTONIC read through the vDSO. Some Tegra
kernels route that read through a syscall (~200 ns) instead. One script answers it:
# on the HOST first: pin the power state or the numbers are noise:
sudo nvpmodel -m 0 && sudo jetson_clocks
# then inside the container, from the repo:
test/orin/run_hotpath_orin.sh # 10 trials x 8 threads, ~5-8 min on Orin
Expected (vDSO works): raw_clock_1thread_ns ~20-40, R5_fixed_CPU_ns/msg ~+20-40, script
prints vDSO path works. The x86 story holds; the Orin row goes in the docs as measured.
Also a valid result (syscall fallback): raw_clock_1thread_ns >=150, R5 cost ~+150-250.
That is not a failure; it is the finding: the README R5 cost table gains an Orin-specific
row with the measured number and the vDSO-fallback explanation. Do not average it with x86.
Not acceptable: in-between clock numbers (~60-150 ns) or trial-to-trial swings > ~10%,
that is an unpinned governor / thermal throttle, not silicon. Re-pin (nvpmodel + jetson_clocks),
re-run. Verdict logic is in the script; artifacts land in test/orin/out/hotpath/, commit
that directory, the docs site consumes it.
Phase 2: CPU + network overhead (probe ON vs OFF)¶
# with the real stack running under the probe (LD_PRELOAD set):
graph-monitor/ros2_pulse/test/orin/run_orin_probe_test.sh 60 perception
# -> per-node CPU over 60s, socket-count delta (probe opens none), captured topic data
# then A/B: comment the LD_PRELOAD line in the env conf, restart the stack, rerun the same:
graph-monitor/ros2_pulse/test/orin/run_orin_probe_test.sh 60 perception
# diff the per-node cpu_s between the two runs = the probe's real overhead on Orin.
Phase 3: iceoryx SHM on vs off¶
# point CYCLONEDDS_URI at the SHM profile, restart stack, confirm loaned/zero-copy receives count:
export CYCLONEDDS_URI=/root/ros2_ws/src/10xCode/setup/deployment/docker_v2/planner_computer/config/cyclonedds_default_shm.xml
# ... restart stack, tail topic_freq.log, confirm intra/inter counts on SHM topics ...
# then repeat with cyclonedds_default_no_shm.xml and compare.
Phase 4: bake-off vs eBPF + LTTng, on Orin hardware¶
This is the important portability test: does eBPF even work on the Jetson kernel?
# needs a privileged container on the Orin (eBPF: debugfs + CAP_SYS_ADMIN + BTF):
docker run --rm --privileged \
-v /root/ros2_ws/src/10xCode/graph-monitor/ros2_pulse:/pkg \
-v /tmp/bench:/work \
<your-ros-image> bash /pkg/bench/run_bakeoff.sh
Expected Orin-specific outcomes to capture:
- eBPF leg may report n/a if the Jetson kernel lacks uprobe/BTF, that itself is the result
(eBPF not portable to the fleet).
- LTTng leg = 0 events unless the Orin image links lttng-ust (Phase 1b tells you).
- ours should match the local ~0% overhead.
What to send back¶
Everything below feeds the public docs site verbatim, so capture files, not screenshots:
| # | Artifact | From | Proves |
|---|---|---|---|
| 1 | Phase 1a/1b terminal output (paste into test/orin/out/instrumentation.txt) |
Phase 1 | probe hooks exist on the Orin image; lttng-ust link status |
| 2 | test/orin/out/hotpath/ (platform.txt, summary.csv, summary.txt, trial_*.txt) |
Phase 1.5 | the +24 ns claim on Tegra, vDSO verdict |
| 3 | Two topic_freq windows: real stack + SHM-on (test/orin/out/stack_window.log, stack_window_shm.log) |
Phases 1/3 | real topics incl. intra= nonzero; SHM behavior |
| 4 | Both run_orin_probe_test.sh reports, probe ON and OFF (test/orin/out/report_on/, report_off/) |
Phase 2 | per-node CPU delta, zero sockets opened |
| 5 | Phase 4 bake-off table (test/orin/out/bakeoff.txt) |
Phase 4 | eBPF/LTTng portability vs ours on Jetson kernel |
| 6 | Spot-check outputs from 1d (test/orin/out/v030_features.txt) |
Phase 1 | JITTER/jsonl/QUIET work on the target |
Commit test/orin/out/** on a branch and open a PR, or paste raw; either way the numbers get
turned into the on-Orin RESULTS section + docs-site page. Redact topic names if the stack's
graph is sensitive; rates and node counts are what the docs need.