Skip to content

Verification log

Dated measurements behind development.md: the rows of the scale harness and the hand-run and scripted verifications of the transport rules. New rows are appended at the bottom of each table; the how-to (commands, budgets, what each run records) stays in development.md.

Budget history

The budgets of Scale verification changed while the rows below were taken; a row is judged by the rules of its date.

  • --watch frame: the run without --stats joined the three runs in #135. Before #177 the p95 alone was judged at 250 ms (over: watch_frame_p95_ms in the older rows): a 60 s run drew 16-22 frames once the settle rule of #168 delayed the first one, the p95 of that few is the worst frame, and the host is not still within a harness run (the load's statistics writers work while the tool is matched, so every run heats the host for the next one - /proc/loadavg climbed from 3.6 to 24 on 8 CPUs over six back-to-back runs). The median is the frame a user sees; the p95 keeps a stall from hiding. At large the load alone takes 6.7 to 7.5 of the 8 cores, so the frame time there says more about the host than about the tool.
  • --watch statistics coverage replaces the "dropped statistics samples = 0" budget (stats_dropped_samples in the older rows, #147): the counters are cumulative and the tool prints last - first, so a lost sample costs nothing until it leaves a pair without a measurement, and a run could lose 925130 of 1040228 samples while measuring every pair. The dropped samples are still recorded - growth of sample_lost - sample_lost_latency + sample_rejected after the first --watch frame, the higher of the two --stats runs, with the late-join losses of the one-shot runs (sample_lost_at_start, #134) "at start" - and judged by nothing.
  • --stats coverage: a delivery proof alone counted until #153; since then only measured packets do. Until #168 the default one-shot was a fixed --timeout 5; now it stops once every matched RTPS_SENT writer has been heard and the measured entries stop growing for 3 s, at 30 s at the latest. The pairs with a delivery proof but no packet are recorded as delivered_unmeasured_default / _30s, and how the default run stopped as default_stopped_on / settled_at_s / default_wall_s; judged by nothing.

Scale results

Columns: - participants / topics / pairs: from the 30 s --stats document; - one-shot: the discovery wait / table time, with the lowest pair count of the three runs; - host: architecture, CPUs, memory and, since #177, the whole-VM cores the load took before the tool started (load_before.vm_cores_load_only, 5 s sample); - --watch frame: median / p95. Rows from before #177 were judged on the p95 alone, at 250 ms (over: watch_frame_p95_ms), and are left as they were; since #177 the median is judged at 250 ms and the p95 at 500 ms; - tool CPU (cores) / peak RSS; - dropped samples: growth during --watch, with the late-join losses of the one-shot --stats runs "at start", the one-shot coverage (a fixed 5 s window before #168) and, since #147, the --watch coverage. Rows from before #147 were judged on the dropped samples (over: ... stats_dropped_samples) and are left as they were; the number is record-only since. Rows from before #153 counted latency-only pairs (a delivery proof without a measured packet) as measured in the 5 s coverage, which is why a Lyrical row can show a 5 s coverage of 1.0 next to a --watch coverage of 0.0; they too are left as they were; - -v lines; - the load's RSS and the memory still free.

The rows dated 2026-09-17 were measured after #141 (the drain thread and the per-topic reader QoS); the earlier rows are before it.

Date Scenario ROS (Fast DDS) Host Participants / topics / pairs One-shot --watch frame Tool CPU / RSS Dropped / coverage -v lines Load memory Result
2026-09-16 small jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB 10 / 102 / 500 1.4 s / 18 ms (pairs 36) 45.8 / 54.0 ms (-v 50.9 / 61.6 ms) 0.19 / 61 MB 0 (14056 at start) / 1.0 605 382 MB, 6888 MB free pass
2026-09-16 medium jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB 20 / 502 / 2400 1.4 s / 27 ms (pairs 1) 377.9 / 470.2 ms (-v 389.2 / 519.9 ms) 0.86 / 200 MB 165 (114294 at start) / 0.7376 346 1120 MB, 6268 MB free over: watch_frame_p95_ms, stats_dropped_samples, stats_coverage
2026-09-16 large jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB 40 / 1002 / 5600 3.0 s / 660 ms (pairs 1385) 6585.1 / 12818.9 ms (-v 6766.0 / 13472.2 ms) 0.35 / 260 MB 682142 (2923377 at start) / 0.0 6605 2862 MB, 5264 MB free over: watch_frame_p95_ms, stats_dropped_samples, stats_coverage
2026-09-16 large_multi jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB 40 / 1002 / 5600 3.1 s / 711 ms (pairs 213) 4099.3 / 50402.4 ms (-v 2619.3 / 6762.3 ms) 0.38 / 234 MB 5161885 (4270192 at start) / 0.0 5401 1354 MB, 5269 MB free over: watch_frame_p95_ms, stats_dropped_samples, stats_coverage
2026-09-16 large humble (2.6.12) aarch64, 8 CPU, 7.7 GB 40 / 1002 / 5600 3.1 s / 486 ms (pairs 5600) 551.5 / 585.2 ms (-v 541.5 / 596.8 ms) 0.22 / 203 MB 0 (0 at start) / n/a 6605 2063 MB, 5553 MB free over: watch_frame_p95_ms
2026-09-16 large kilted (3.2.4) aarch64, 8 CPU, 7.7 GB 37 / 1001 / 3399 3.0 s / 730 ms (pairs 4000) 747.2 / 2057.6 ms (-v 430.0 / 2231.6 ms) 0.55 / 259 MB 182655 (2432463 at start) / 0.0 5005 3068 MB, 4797 MB free over: watch_frame_p95_ms, stats_dropped_samples, stats_coverage
2026-09-16 large lyrical (3.6.2) aarch64, 8 CPU, 7.7 GB 40 / 1002 / 4000 3.0 s / 347 ms (pairs 1438) 4554.7 / 6093.1 ms (-v 6852.3 / 8749.9 ms) 0.43 / 312 MB 1582022 (1252624 at start) / 0.0 5005 4109 MB, 4105 MB free over: watch_frame_p95_ms, tool_rss_mb, stats_dropped_samples, stats_coverage
2026-09-16 nav2 jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB 4 / 113 / 1195 1.6 s / 19 ms (pairs 1195) 27.8 / 44.7 ms (-v 34.4 / 45.2 ms) 1.00 / 71 MB 0 (31183 at start) / 0.9918 1311 0 MB, 6558 MB free over: tool_cpu_cores
2026-09-16 limit_p60 jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB 60 / 1502 / 9600 3.1 s / 4255 ms (pairs 5875) 42506.1 / 62980.4 ms (-v 33072.6 / 55212 ms) 0.19 / 385 MB 7314533 (3068233 at start) / 0.0 11105 5204 MB, 3572 MB free records only
2026-09-16 limit_p90 jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB 90 / 1993 / 13799 3.1 s / 388 ms (pairs 602) 2625.4 / 12455.3 ms (-v 3386.1 / 5664 ms) 0.82 / 445 MB 1098338 (4751274 at start) / 0.0 1030 9354 MB, 890 MB free records only
2026-09-16 limit_p135 jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB 0 / 0 / 0 3.2 s / 126 ms (pairs 0) 822.5 / 4717 ms (-v 2038.5 / 13400.6 ms) 0.39 / 259 MB 132744 (826 at start) / – 65 11812 MB, 676 MB free records only; stopped: under 10 % of the memory free
2026-09-17 medium jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB 20 / 502 / 2400 2.5 s / 79 ms (pairs 0) 343.7 / 667.4 ms (-v 496.7 / 680.1 ms) 0.81 / 182 MB 23 (149817 at start) / 1.0 2905 1103 MB, 6379 MB free over: watch_frame_p95_ms, stats_dropped_samples
2026-09-17 large jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB 40 / 1002 / 5600 3.1 s / 743 ms (pairs 4612) 8877.1 / 35068.8 ms (-v 5948.8 / 16910.6 ms) 0.39 / 284 MB 1211707 (1363220 at start) / 1.0 688 2835 MB, 5221 MB free over: watch_frame_p95_ms, stats_dropped_samples
2026-09-17 medium jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB 20 / 502 / 2400 2.2 s / 69 ms (pairs 1008) 151.6 / 203.9 ms (-v 169.4 / 195.6 ms, no --stats 53.5 / 81.4 ms) 0.65 / 176 MB 5823 (112679 at start) / 1.0 2905 1108 MB, 6451 MB free over: stats_dropped_samples
2026-09-17 large jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB 40 / 1002 / 5600 3.0 s / 496 ms (pairs 19) 1571.5 / 6606.7 ms (-v 1387.0 / 12013.3 ms, no --stats 438.3 / 945.6 ms) 0.45 / 283 MB 1923308 (1495561 at start) / 0.9718 1172 2833 MB, 5288 MB free over: watch_frame_p95_ms, stats_dropped_samples
2026-09-17 medium jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB 20 / 502 / 2400 2.1 s / 79 ms (pairs 2400) 155.2 / 186.1 ms (-v 180.0 / 206.4 ms, no --stats 50.7 / 85.5 ms) 0.70 / 175 MB 1 (149934 at start) / 1.0 / watch 1.0 2905 1100 MB, 6458 MB free pass
2026-09-17 medium lyrical (3.6.2) aarch64, 8 CPU, 7.7 GB 20 / 502 / 2000 3.0 s / 73 ms (pairs 1302) 128.0 / 152.0 ms (-v 143.5 / 172.5 ms, no --stats 59.1 / 93.0 ms) 1.04 / 224 MB 0 (197712 at start) / 1.0 / watch 0.0 1658 1709 MB, 5981 MB free over: tool_cpu_cores, stats_watch_coverage (before #152)
2026-09-18 medium lyrical (3.6.2) aarch64, 8 CPU, 7.7 GB 20 / 502 / 2000 3.0 s / 77 ms (pairs 1824) 185.3 / 255.4 ms (-v 223.8 / 320.1 ms, no --stats 57.8 / 88.9 ms) 0.73 / 214 MB 0 (185522 at start) / 1.0 / watch 1.0 2505 1712 MB, 5926 MB free over: watch_frame_p95_ms (heartbeat period 500 ms, #152)
2026-09-18 large lyrical (3.6.2) aarch64, 8 CPU, 7.7 GB 40 / 1002 / 4000 3.0 s / 506 ms (pairs 1652) 13042.5 / 19190.7 ms (-v 5559.8 / 10462.5 ms, no --stats 412.5 / 703.1 ms) 0.42 / 323 MB 1990969 (608359 at start) / 0.9842 / watch 0.0 5005 4144 MB, 4077 MB free over: watch_frame_p95_ms, tool_rss_mb, stats_watch_coverage (heartbeat period 500 ms, #152)
2026-09-18 medium jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB 20 / 502 / 2400 1.5 s / 64 ms (pairs 17) 149.9 / 180.5 ms (-v 181.0 / 218.3 ms, no --stats 53.4 / 84.0 ms) 0.69 / 182 MB 0 (188924 at start) / 1.0 / watch 1.0 2905 1099 MB, 6427 MB free pass (HISTORY_LATENCY reader depth 100, #143)
2026-09-19 medium jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB 20 / 502 / 2400 2.1 s / 78 ms (pairs 2400) 167.4 / 216.1 ms (-v 187.8 / 227.7 ms, no --stats 52.7 / 85.7 ms) 0.69 / 182 MB 0 (141900 at start) / 0.0 / watch 1.0 2905 1102 MB, 6451 MB free over: stats_coverage (5 s coverage counts packets only, #153; see #168)
2026-09-19 medium lyrical (3.6.2) aarch64, 8 CPU, 7.7 GB 20 / 502 / 2000 3.0 s / 84 ms (pairs 1806) 171.4 / 198.4 ms (-v 213.1 / 242.4 ms, no --stats 57.6 / 89.6 ms) 0.73 / 221 MB 0 (234990 at start) / 0.0 / watch 1.0 2325 1758 MB, 5818 MB free over: stats_coverage (5 s coverage counts packets only, #153; see #168)
2026-09-20 medium jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB 20 / 502 / 2400 2.1 s / 77 ms (pairs 689) 196.5 / 314.6 ms (-v 207.3 / 386.0 ms, no --stats 51.8 / 69.2 ms) 0.75 / 186 MB 11873 (140328 at start) / 0.0 / watch 1.0 2905 1189 MB, 6354 MB free over: watch_frame_p95_ms, stats_coverage
2026-09-20 medium jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB 20 / 502 / 2400 1.6 s / 45 ms (pairs 0) 216.5 / 281.7 ms (-v 280.3 / 1161.7 ms, no --stats 115.6 / 185.1 ms) 0.85 / 184 MB 0 (245119 at start) / 0.9575 (settled 16.6 s) / watch 1.0 237 1188 MB, 6261 MB free over: watch_frame_p95_ms
2026-09-20 medium lyrical (3.6.2) aarch64, 8 CPU, 7.7 GB 20 / 502 / 2000 3.0 s / 74 ms (pairs 1273) 202.8 / 281.9 ms (-v 461.2 / 1603.6 ms, no --stats 61.2 / 146.0 ms) 0.91 / 260 MB 0 (355522 at start) / 1.0 (settled 18.3 s) / watch 1.0 2415 1842 MB, 5808 MB free over: watch_frame_p95_ms
2026-09-20 medium jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB, load 3.2 cores 20 / 502 / 2400 2.1 s / 85 ms (pairs 2400) 190.3 / 239.7 ms (-v 228.3 / 327.7 ms, no --stats 51.0 / 82.3 ms) 0.86 / 184 MB 17526 (229780 at start) / 1.0 / watch 1.0 2905 1190 MB, 6367 MB free pass
2026-09-20 medium jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB, load 3.1 cores 20 / 502 / 2400 2.1 s / 76 ms (pairs 2400) 186.1 / 249.6 ms (-v 208.6 / 265.5 ms, no --stats 52.8 / 80.0 ms) 0.79 / 182 MB 0 (144837 at start) / 1.0 / watch 1.0 2905 1205 MB, 6375 MB free pass
2026-09-20 medium jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB, load 2.9 cores 20 / 502 / 2400 2.1 s / 79 ms (pairs 2400) 195.7 / 227.3 ms (-v 224.8 / 323.1 ms, no --stats 52.7 / 83.4 ms) 0.77 / 186 MB 0 (254613 at start) / 1.0 / watch 1.0 4 1199 MB, 6314 MB free pass
2026-09-20 medium lyrical (3.6.2) aarch64, 8 CPU, 7.7 GB, load 2.8 cores 20 / 502 / 2000 2.3 s / 62 ms (pairs 1050) 183.5 / 248.9 ms (-v 218.5 / 273.6 ms, no --stats 59.8 / 89.3 ms) 0.69 / 236 MB 0 (298345 at start) / 1.0 / watch 1.0 2505 1853 MB, 5766 MB free pass
2026-09-20 medium lyrical (3.6.2) aarch64, 8 CPU, 7.7 GB, load 2.9 cores 20 / 502 / 2000 3.0 s / 70 ms (pairs 1110) 218.0 / 243.2 ms (-v 234.7 / 300.3 ms, no --stats 59.9 / 87.8 ms) 0.79 / 226 MB 0 (180147 at start) / 1.0 / watch 1.0 2505 1849 MB, 5824 MB free pass
2026-09-20 medium lyrical (3.6.2) aarch64, 8 CPU, 7.7 GB, load 2.8 cores 20 / 502 / 2000 3.0 s / 70 ms (pairs 570) 195.5 / 275.8 ms (-v 223.1 / 267.9 ms, no --stats 59.4 / 88.9 ms) 0.76 / 230 MB 0 (239275 at start) / 0.0 / watch 1.0 2027 1846 MB, 5824 MB free over: stats_coverage (#179: settled at 8.0 s with no pair measured)
2026-09-20 medium lyrical (3.6.2) aarch64, 8 CPU, 7.7 GB, load 2.8 cores 20 / 502 / 2000 3.0 s / 79 ms (pairs 1194) 211.8 / 282.5 ms (-v 233.4 / 259.4 ms, no --stats 58.1 / 85.5 ms) 0.75 / 225 MB 0 (220777 at start) / 1.0 / watch 1.0 1893 1859 MB, 5735 MB free pass
2026-09-20 medium lyrical (3.6.2) aarch64, 8 CPU, 7.7 GB, load 2.7 cores 20 / 502 / 2000 3.0 s / 78 ms (pairs 1800) 174.2 / 211.6 ms (-v 211.2 / 308.3 ms, no --stats 60.9 / 88.7 ms) 0.73 / 235 MB 0 (346030 at start) / 1.0 / watch 1.0 2335 1851 MB, 5818 MB free pass
2026-09-20 medium lyrical (3.6.2) aarch64, 8 CPU, 7.7 GB, load 2.7 cores 20 / 502 / 2000 3.0 s / 76 ms (pairs 1624) 189.0 / 221.0 ms (-v 223.6 / 256.7 ms, no --stats 59.7 / 88.4 ms) 0.68 / 234 MB 0 (190833 at start) / 1.0 / watch 1.0 2505 1849 MB, 5816 MB free pass
2026-09-20 medium jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB, load 2.9 cores 20 / 502 / 2400 2.1 s / 79 ms (pairs 1319) 180.9 / 210.8 ms (-v 185.3 / 243.0 ms, no --stats 54.3 / 92.3 ms) 0.76 / 187 MB 2 (143079 at start) / 1.0 / watch 1.0 2905 1198 MB, 6248 MB free pass
2026-09-21 medium jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB, load 2.9 cores 20 / 502 / 2400 2.4 s / 78 ms (pairs 148) 161.3 / 200.8 ms (-v 187.0 / 213.6 ms, no --stats 53.9 / 90.7 ms) 0.75 / 183 MB 46 (248043 at start) / 1.0 / watch 1.0 2905 1190 MB, 6263 MB free pass
2026-09-21 medium lyrical (3.6.2) aarch64, 8 CPU, 7.7 GB, load 2.8 cores 20 / 502 / 2000 3.0 s / 80 ms (pairs 1811) 193.6 / 222.7 ms (-v 236.9 / 281.0 ms, no --stats 60.2 / 90.6 ms) 0.76 / 233 MB 0 (303797 at start) / 1.0 / watch 1.0 2505 1856 MB, 5784 MB free pass
2026-09-21 medium jazzy (2.14.6) aarch64, 8 CPU, 7.7 GB, load 2.7 cores 20 / 502 / 2400 2.2 s / 83 ms (pairs 2400) 177.9 / 212.3 ms (-v 203.4 / 278.2 ms, no --stats 55.0 / 80.9 ms) 0.71 / 191 MB 0 (356894 at start) / 0.95 / watch 1.0 4 1189 MB, 6381 MB free pass
2026-09-21 medium lyrical (3.6.2) aarch64, 8 CPU, 7.7 GB, load 2.7 cores 20 / 502 / 2000 3.0 s / 68 ms (pairs 672) 211.0 / 576.9 ms (-v 715.0 / 2839.8 ms, no --stats 60.9 / 83.2 ms) 0.87 / 236 MB 370249 (216643 at start) / 0.0 / watch 0.0 2505 1851 MB, 5769 MB free over: watch_frame_median_ms, watch_frame_p95_ms, stats_watch_coverage, stats_coverage (host busy; the same rung fails the same way on the binary without #200)

Web viewer on the 30 s --stats documents, measured on the Docker host (Apple M3, 8 CPU, 24 GB, macOS 26.6) in headless Chrome 153. The times are medians of five loads, and the budget is 100 ms for every action at large. The rows dated 2026-09-20 were taken after #136: their filter cells include the viewer's 100 ms typing debounce (the work is the cell minus 100 ms), and the harness waits for the viewer's render counter rather than two frames; the select column did not exist before:

Date Scenario Document Arrows / nodes First render Topic filter / node filter / clear Select / deselect Result
2026-09-16 small 1.8 MB 90 / 10 19 ms 31 / 33 / 33 ms — pass
2026-09-16 medium 8.5 MB 378 / 20 44 ms 34 / 34 / 33 ms — pass
2026-09-16 nav2 2.4 MB 244 / 24 25 ms 25 / 33 / 33 ms — pass
2026-09-16 large 14.7 MB 1467 / 40 156 ms 43 / 51 / 136 ms — over: clearing a filter
2026-09-16 large_multi 14.5 MB 1467 / 40 145 ms 42 / 67 / 133 ms — over: clearing a filter
2026-09-16 limit_p60 23.7 MB 2927 / 60 424 ms 47 / 83 / 399 ms — records only
2026-09-16 limit_p90 30.7 MB 4217 / 90 718 ms 46 / 83 / 689 ms — records only
2026-09-20 medium 9.1 MB 378 / 20 33 ms 133 / 133 / 133 ms (debounce included) 33 / 34 ms pass
2026-09-20 large 16.8 MB 1467 / 40 53 ms 120 / 123 / 146 ms (debounce included) 35 / 33 ms pass

Replaying a recording (#82), measured the same way on 2026-09-22 with headless Chrome through web/test/cdp.js: the medium document of 2026-09-21 (2400 pairs) written compactly 60 times with observed_at one second apart (5.5 MB per frame, 332 MB), opened with ?src=, three loads. The first frame was on screen after 0.6-0.8 s, the whole file read after 1.4-1.6 s, and a step to the next frame (▶, measured until the render counter moved plus one animation frame) took 33 ms in all 24 steps, the two-frame floor. The page's JavaScript heap was 45 MB after the scan (126 MB in the first load, before a collection): the viewer keeps byte ranges and a few numbers per pair and frame, never the documents.

The live history (#218), measured on 2026-09-22 with headless Chrome through web/test/cdp.js: transport_viz_web running a stand-in producer that printed the same medium document with a fresh observed_at every second (5.7 MB per frame as serve.py sends it), ?live=1 with the default 512 MB bound, 30 frames followed and then 70 more after ◀, the synchronous work of each document event timed in the page. Following: 17 ms median, 20 ms max; paused: 12 ms median, 17 ms max. The bound was reached at frame 91: nine frames were dropped in that event, 14 ms in all, and 90 stayed kept. The JavaScript heap was 49 MB after 30 frames and 38 MB after 100 (the frames' text is in Blobs, outside the heap), and switching match by to guid read the 90 kept frames again in 0.8 s.

The Prometheus endpoint (#83), measured on 2026-09-22 in the Jazzy container: metrics_text() of web/serve.py over the medium document of 2026-09-21 (502 topics, 2400 pairs, --stats) built 24 413 series, 7.5 MB (344 KB gzipped; the endpoint does not compress), in 46.7 ms median and 53.0 ms max; a scrape of /metrics with curl through a running transport_viz_web took 45-77 ms.

The limit_p135 document has no pair to draw. First render stays far below 3 s. Until #136, each input event (every keystroke, and also a click on an arrow or node) rebuilt the model and rendered every visible arrow again, with one getTotalLength per label: a filter that hid most arrows was fast, clearing it rendered them all at about 0.16 ms per arrow, which is the 136 ms of the 2026-09-16 large row. Since #136 the model is built once per document, a click only toggles the selected classes and re-renders the panel, the label sits at a midpoint computed from the curve's control points, and typing is debounced by 100 ms; on 2026-09-20 clearing a filter on large takes 46 ms above the debounce, and a selection 35 ms, of which 33 ms are the two-frame floor of the measurement at 60 Hz. The medium and large documents were regenerated that day, hence their sizes.

The one-shot pair counts below the total are the --quiet 1 stops described in Scale verification (#133). What falls over first:

  • small holds every budget.
  • medium:
  • the --watch frame was over budget (344 / 667 ms): collect 218 ms (apply_stats 93 ms, resolve 50 ms, the drain collect still does 26 ms) plus update 115 ms. Since #135 it holds it, at 152 / 204 ms with --stats and 54 / 81 ms without: the ROS graph is queried only while discovery goes on (2 to 4 of the 28 frames of a run, resolve 0 ms on the others), and update no longer copies the frame and applies the statistics to the copy (8 ms). What is left is apply_stats, 76 ms of a 137 ms collect;
  • the counter samples lost over a 60 s --watch run vary between runs of one build: 0 in the --stats run and 5823 in the -v run of the same row (#147). Since #147 that number is recorded and no longer judged: what is judged is the --watch coverage, the pairs with measured packets among those HISTORY_LATENCY proves delivered, and it is 1.0 (2000 of 2000 /scale pairs at the last frame of both --stats runs, one counter sample lost, no stats-samples-lost). A first attempt judged the RTPS_SENT instances holding two or more samples instead and read 0.85 to 0.91 in runs that lost nothing: an instance with one sample is most often a locator nothing was sent to since the tool started (113 of 1270), so only a delivery proof tells a starved pair from an idle one;
  • statistics used to go missing here - 26 % of the /scale pairs were not measured within 5 s. Since #141 every one of them is (coverage 1.0, from 0.74) and the one-shot table shows all 2400 pairs instead of one. What is left is 23 counter samples over a 60 s --watch run, which cost no pair its measurement: a counter is read as last - first, so only a loss that leaves an instance with fewer than two samples in the window does. The best-effort HISTORY_LATENCY losses are counted apart (stats.samples_lost_latency) and not judged;
  • the --stats one-shot used to stop at 5 s, and the statistics writers had not finished handing their transient-local history to the tool's readers by then: coverage 0.0 on both Jazzy and Lyrical (#168). The last RTPS_SENT writer is first heard from at 16-20 s and measured entries keep arriving until 22-27 s, with pauses of up to 2 s in between. Since #168 the one-shot ends on the settle rule (every matched RTPS_SENT writer heard, measured entries unchanged for 3 s), at 16.6-20.1 s on Jazzy (coverage 0.953, 1.0, 0.9575 over three runs) and 16.9-22.7 s on Lyrical (1.0 in three runs), with --timeout 30 as the cap;
  • the --watch frame gate judged the worst frame of a run on a host the harness itself heats (#177): once the settle rule of #168 delayed the first frame, a 60 s run drew 16-22 frames, whose p95 is the max, and the load's statistics writers work only while the tool is matched, so every run of the harness raises the host load for the next one (/proc/loadavg 3.6 to 24 on 8 CPUs over six back-to-back 60 s runs; the -v run, late in the order, read 753 / 925 ms once, and 198-216 ms median on its own). Since #177 --watch --stats starts at discovery quiet and 5 s (27-29 frames per run), the median is judged at 250 ms and the p95 at 500 ms, and the row says what the host was doing. -v itself costs a flat 27 ms per frame of render (33-38 ms against 8 ms) on 2000 pairs. Six runs of one build (Jazzy and Lyrical, three each): median 184-218 ms with --stats, 209-235 ms with -v, p95 227-328 ms, all within budget;
  • the settle rule of #168 counted every RTPS_SENT entry with a delta, and on Lyrical the entries to multicast metatraffic (239.255.0.1:7400), to the SHM metatraffic ports 7000-7019 and to the tool's own port all move within the first seconds of any participant: one run settled at 8.0 s with 47 such entries, every writer heard from, and no /scale pair measured (coverage 0.0, #179). Since #179 only entries towards a discovered reader's unicast (kind, port) count (stats.measured_instances): coverage 1.0 in three Lyrical runs (settled at 27.7 s, 24.3 s, and once the 30 s cap with 545 entries measured and the handoff still producing them) and one Jazzy run (15.4 s). The Lyrical handoff finishes later than the Jazzy one, so its one-shot takes 24-30 s with the default --timeout 30;
  • the first of the three one-shot runs can still see no pair at all ("pairs 0"): it starts 20 s after the load and the graph is not complete yet (#133 reports this), while the two that follow both show 2400.
  • large:
  • the load alone keeps 6.7 of the 8 cores busy, and the tool gets about a third of a core;
  • a frame took 6.6 to 8.9 s (resolve 2.9 s: two rmw graph queries per topic every frame). Since #135 the median is 1.6 s with --stats and 0.44 s without, resolve 0 ms on all but 3 to 6 frames of a run and update 0.11 s; the rest is apply_stats (0.44 s) and the drain (0.38 s) on the third of a core the load leaves, with a p95 of 6.6 to 12 s. The frame budget is not judged at this size;
  • the statistics readers lose millions of samples and used to measure no /scale pair at all. The tool says so (stats.samples_lost, the stats-samples-lost warning, #134), and since #141 the loss no longer costs the measurements: the 5 s coverage was 0.62, 0.97 and 1.0 over three runs of the same build, where before it was 0.0 in all three. The counter samples that are still lost (1.2 M in the last run) are nearly all intermediate values of a last - first the tool never needed, which is why a run can lose a million of them and still measure every pair;
  • this rung is not reproducible on this host, and the coverage above is the spread it produces: the load alone takes 6.7 to 7.5 of the 8 cores before the tool starts, and the 0.62 is the run that started from 7.5. The best-effort HISTORY_LATENCY losses (1.7-1.9 M) are counted apart in stats.samples_lost_latency and judged by nothing;
  • the one-shot table still takes 0.7 s.
  • large_multi behaves the same way.
  • large on the other distributions:
  • on Kilted and Lyrical the nodes no longer subscribe to /parameter_events, so there are 4000 pairs instead of 5600;
  • Humble has no statistics (the statistics budgets are n/a, and drain is empty), and a frame still takes 550 ms, 370 ms of it in resolve: the graph queries alone exceed the budget at this size (#135);
  • Lyrical behaves like Jazzy (frames of 4.5–6.9 s, 2.1–3.5 s of them in drain) and its peak RSS is 312 MB, in the 30 s --stats run;
  • on Kilted the tool's view is unstable: the 30 s --stats document holds 37 of 40 participants, and the --watch frames lose and regain pairs (159–3327 of 4000), while the one-shot tables before them saw all 4000. The load's VM CPU dropped from 6.5 to 3.3–4.8 cores during the --watch runs (#138, closed without a fix: Kilted is out of scope from 1.1.0 on, the cause was not found, and Jazzy, Humble and Lyrical do not show it).
  • RATE of sporadic writers: a writer that sends one burst gets a single PUBLICATION_THROUGHPUT sample of megabytes per second. Examples: /map in Nav2 shows 11.5 MB/s, and /parameter_events in large shows 267 MB/s on Jazzy and 1.7 GB/s on Lyrical (#137). Fixed by removing the column and the subscription: the statistic is a per-write payload/interval sample, not a rate. A rate that is actually a rate is #143.
  • nav2:
  • frames and memory are small;
  • with --stats, the tool uses a whole core in every run. All of it is one Fast DDS UDP receive thread (dds.udp.<port>: 0.995 cores; the tool without --stats uses none): the statistics readers take only UDP (#106), and one thread per port is also the ceiling behind the losses at large (#141);
  • in a first run, both 60 s --watch runs saw no pair at all while the one-shot runs before them saw 1195. Not reproduced by hand or in the second run; this is why the harness now fails such a run.
  • limit:
  • the load alone keeps all 8 cores busy from 60 processes on, so these steps show a starved tool;
  • 60 processes: the table takes 4.3 s after discovery, peak RSS is 385 MB in the --stats runs, and a --watch frame takes 40 s (drain 27 s, resolve 10 s): two frames per minute;
  • 90 processes: the tool's view is incomplete and unstable: one-shots show 600–1075 of 13 500 pairs, --watch frames 0 to 15 000, and the 30 s --stats run needs 129 s;
  • 135 processes: discovery practically stops. One-shots see 0–4 pairs, the 30 s --stats document is empty and that run was killed after 180 s. The load's summed RSS (11.8 GB, shared pages counted per process) leaves 676 MB free, which ends the ladder.

Verification results

Date Scenario Arch Fast DDS Result Reproduce
2026-09-04 two bridged containers arm64 2.14 (ros:jazzy) UDPv4, different-host scripts/integration_test.sh multi_container
2026-09-05 two bridged containers x86_64 2.14.6 UDPv4, different-host scripts/integration_test.sh multi_container
2026-09-05 two bridged containers, --stats x86_64 2.14.6 measured UDPv4, host names differ (container ids) scripts/integration_test.sh stats_multi_container
2026-09-05 two hostnet containers x86_64 2.14.6 SHM, same-host-guid, no host-id-match-but-ip-differs scripts/integration_test.sh hostnet_shm
2026-09-05 LARGE_DATA, two bridged containers, --stats x86_64 2.14.6 TCPv4, common-tcpv4-locator, measured TCPv4 (physical port matches RTPS_SENT) scripts/integration_test.sh large_data_tcp
2026-09-05 LARGE_DATA, one host x86_64 2.14.6 TCPv4 announced, SHM chosen (both-shm-locators) test_large_data_same_host.py
2026-09-05 DEFAULTv6, two bridged containers (IPv6 network) x86_64 2.14.6 UDPv6, common-udpv6-locator scripts/integration_test.sh udpv6_multi_container
2026-09-05 UDPv6, one host x86_64 2.14.6 UDPv6, same-host-guid (needs an IPv6 interface; multicast only, ROS_STATIC_PEERS is IPv4-only in Jazzy) test_udpv6.py
2026-09-05 ROS_DISCOVERY_SERVER (fastdds discovery server on 127.0.0.1:11811) x86_64 2.14.6 SHM pair seen as SUPER_CLIENT; a plain CLIENT does not see /chatter test_discovery_server.py
2026-09-05 ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST x86_64 2.14.6 SHM, nodes announce 127.0.0.1 only; OFF sees nothing (warning printed) test_localhost_range.py
2026-09-05 2 MB samples over SHM, --stats x86_64 2.14.6 SHM, measured SHM, bytes consistent with the sample size test_large_shm.py
2026-09-05 full launch test suite on Fast DDS 3.x (Kilted 3.2.4, Rolling 3.6.2) x86_64 3.2.4 / 3.6.2 all pass; Rolling: demo nodes publish example_interfaces/msg/String, Discovery Server relays every endpoint to plain clients ROS_DISTRO=kilted docker compose build dev + colcon test
2026-09-05 two physical hosts on one Wi-Fi LAN: x86_64 Ubuntu (Docker hostnet) ↔ macOS arm64 (native RoboStack Jazzy), Discovery Server on the x86 host x86_64 + arm64 2.14.6 both UDPv4, different-host, common-udpv4-locator observed (writer host:010f0956, reader on ubuntu2404-desktop01); no --stats measurement: the Mac's Fast DDS sendto() intermittently fails with EHOSTUNREACH while plain UDP from the Mac works (#15) see "Two physical hosts"
2026-09-06 reliability counters over Wi-Fi: x86_64 ↔ Jetson Orin NX (Docker hostnet, both directions, --stats with all 11 aliases) x86_64 + arm64 2.14.6 both LOSS 0 (no RTPS_LOST, no resends at 1 Hz), 3–4 heartbeats and acknacks per 10 s observation, LATENCY ≈ 46 ms mean / 93 ms max (includes the clock offset of the two hosts) "Two physical hosts"
2026-09-06 shared-memory line: nodes in the tool's IPC namespace visible (hostnet_shm), bridged containers reported shm-not-visible (multi_container) x86_64 2.14.6 as expected scripts/integration_test.sh multi_container, hostnet_shm, test_shm.py
2026-09-06 two physical hosts on one Wi-Fi LAN: x86_64 Ubuntu 24.04 (Docker hostnet) ↔ Jetson Orin NX, JetPack 6 / Ubuntu 22.04 arm64 (Docker hostnet, Jazzy image), plain multicast discovery, --stats on both nodes, tool on the x86 host x86_64 + arm64 2.14.6 both both directions: UDPv4, different-host, certain, measured UDPv4 (measured=UDPv4 7pkt 1.06 kB Jetson → x86, 8pkt 1.16 kB x86 → Jetson), hosts jetson-orin-nx01 / ubuntu2404-desktop01 from PHYSICAL_DATA, RATE 24 B/s. Found and fixed: a reader on the tool's host is announced as 127.0.0.1, so the remote writer's RTPS_SENT did not match (delivered-without-measured-traffic) Two physical hosts
2026-09-10 full test suite on ROS 2 Lyrical Luth (Ubuntu 26.04) x86_64 3.6 (ros:lyrical) 341 tests, 0 failures, 49 skipped; demo nodes publish example_interfaces/msg/String and the Discovery Server relays every endpoint to plain clients, as on Rolling; the vendored statistics types generated from 3.2.4 build unchanged ROS_DISTRO=lyrical docker compose build dev + colcon test
2026-09-13 shared-memory line, host network: the tool alone; talker + listener in one container with network_mode: host and without ipc: host; the same with ipc: host (tool in hostnet) arm64 2.6 (ros:humble), 2.14.6 (ros:jazzy), 3.6 (ros:lyrical) alone: checked_ports empty (Lyrical reported its own ports [7000, 19411] as the nodes'); without host IPC: every port of the nodes missing, shm-not-visible (including 7000/7001, the same numbers as the tool's own); with host IPC: visible. /chatter is SHM, certain in both node cases, yet without host IPC a listener in a second such container receives nothing (#101). Also with rmw_fastrtps_dynamic_cpp on Lyrical. Found and fixed: the tool's rmw participant was recognised only through endpoints resolving to its node name, none on Lyrical/Rolling (#51) scripts/integration_test.sh hostnet_noipc_shm, test_shm_own_ports.py
2026-09-13 same host id, separate IPC namespaces: talker and listener on the host network in an IPC namespace each, the tool in hostnet; the same with another node started first in the talker's container and the tool in the talker's IPC namespace; unit suites on the three distributions arm64 2.6 (ros:humble), 2.14.6 (ros:jazzy), 3.6 (ros:lyrical) /chatter NONE, certain, !shm-ipc-namespace-split with shm-port-collision (both nodes on port 7000 on Jazzy/Lyrical, the same pid-based port on Humble), the listener receives nothing; from the talker's IPC namespace with different numbers: shm-reader-port-not-visible, no collision (Jazzy, Lyrical; Humble has no such placement to tell). hostnet_shm / hostnet_noipc_shm unchanged (SHM). colcon test: Jazzy 424 tests, Lyrical 416, Humble 409, 0 failures (#101) scripts/integration_test.sh hostnet_split_shm, hostnet_split_shm_visible, web/sample/shm_split.json
2026-09-13 --stats with the tool in a third IPC namespace: talker and listener with FASTDDS_STATISTICS on the host network, an IPC namespace each, the tool in hostnet; unit suites on the three distributions arm64 2.6 (ros:humble), 2.14.6 (ros:jazzy), 3.6 (ros:lyrical) the writer's statistics arrive (before: none, !stats-not-enabled-on-writer); /chatter stays NONE, !shm-ipc-namespace-split, with measured-shm-traffic and no delivery (Jazzy, Lyrical; Humble has no statistics module). hostnet_split_shm, hostnet_shm, stats_multi_container unchanged. colcon test: Jazzy 431 tests, Lyrical 420, Humble 413, 0 failures (#106) scripts/integration_test.sh hostnet_split_stats
2026-09-13 full test suite on rmw_fastrtps_dynamic_cpp, ROS 2 Jazzy arm64 2.14.6 (ros:jazzy) 370 tests, 0 failures, 51 skipped (cppcheck per-file placeholders only); every verdict as on rmw_fastrtps_cpp. The former "not verified yet" warning line was 143 columns wide and broke test_watch_tty.py's 60-column check; gone with the warning RMW_IMPLEMENTATION=rmw_fastrtps_dynamic_cpp docker compose run --rm dev bash + colcon test
2026-09-13 full test suite on rmw_fastrtps_dynamic_cpp, ROS 2 Lyrical arm64 3.6 (ros:lyrical) 363 tests, 0 failures, 52 skipped (cppcheck placeholders and the Fast DDS 3.6 Discovery Server skip, as on rmw_fastrtps_cpp) RMW_IMPLEMENTATION=rmw_fastrtps_dynamic_cpp ROS_DISTRO=lyrical docker compose run --rm dev bash + colcon test
2026-09-13 all five multi-container scenarios on rmw_fastrtps_dynamic_cpp (Jazzy image) arm64 2.14.6 UDPv4 / measured UDPv4 / SHM / TCPv4 measured TCPv4 / UDPv6, reason codes identical to the rmw_fastrtps_cpp runs RMW_IMPLEMENTATION=rmw_fastrtps_dynamic_cpp scripts/integration_test.sh all
2026-09-13 Easy Mode (ROS2_EASY_MODE=127.0.0.1), one host: two hostnet containers, and the launch test on a private domain x86_64 3.6.2 (ros:lyrical), 3.2.4 (ros:kilted) SHM, same-host-guid, both-shm-locators; every endpoint announces SHM + TCPv4 unicast and no multicast (P2P); the auto-started server listens on 7400 + 250 × domain + 2. Found and fixed: the fastdds discovery CLI that Fast DDS runs for every participant printed on stdout and corrupted --json ROS_DISTRO=lyrical scripts/integration_test.sh easy_mode_shm, test_easy_mode.py
2026-09-13 Easy Mode, two hosts: bridged containers with fixed addresses, ROS2_EASY_MODE=<talker address>, --stats, tool on the talker's host x86_64 3.6.2 (ros:lyrical), 3.2.4 (ros:kilted) TCPv4, different-host, common-tcpv4-locator, measured TCPv4; the remote endpoint shows its TCPv4 locator only. From a third host without a node of the topic's type the tool (and ros2 topic list) never see /chatter: Fast DDS 3 relays an endpoint only once its type is resolved and the server-to-server lookup does not complete (see how-it-works.md, Easy Mode) ROS_DISTRO=lyrical scripts/integration_test.sh easy_mode_tcp
2026-09-13 Easy Mode on two physical hosts over Wi-Fi: x86_64 Ubuntu 24.04 (Docker hostnet, master, ROS2_EASY_MODE=192.168.1.8) ↔ Jetson Orin NX, Ubuntu 24.04 arm64 (docker run --network host --ipc host, same variable), Lyrical image on both, --stats on both nodes, both directions x86_64 + arm64 3.6.2 both both directions: TCPv4, different-host, common-tcpv4-locator, measured TCPv4 (23–49 packets per 8 s observation), hosts ubuntu2404-desktop01 / jetson-orin-nx-16gb from PHYSICAL_DATA, LATENCY ≈ 94 ms with the clock offset (latency-clock-skew-suspected in one run); the tool saw the pair from the master host and from the Jetson alike, each next to one of the nodes; the remote endpoint shows its TCPv4 locator only, no multicast anywhere Two physical hosts
2026-09-14 same host id, separate IPC namespaces, data-sharing: bounded_pub and bounded_sub with datasharing_auto.xml on the host network in an IPC namespace each, the tool in hostnet; the same with FASTDDS_BUILTIN_TRANSPORTS=UDPv4 on both nodes and the tool in the publisher's IPC namespace; unit suites on the three distributions arm64 2.6 (ros:humble), 2.14.6 (ros:jazzy), 3.6 (ros:lyrical) /bounded NONE, certain, !shm-ipc-namespace-split with datasharing-qos-enabled-both, both-shm-locators and shm-port-collision (all three distributions; before the fix DATA_SHARING / likely while the subscriber received nothing); UDPv4 only: datasharing-reader-segment-not-visible and no SHM reason, the publisher's IPC namespace holds its history but not the subscriber's notification segment (Jazzy, Lyrical; Humble has no FASTDDS_BUILTIN_TRANSPORTS). hostnet_split_shm (Jazzy, Lyrical) and hostnet_shm (Jazzy) unchanged. colcon test: Jazzy 433 tests, Lyrical 422, Humble 415, 0 failures (Humble after a line its older uncrustify formats differently was rewritten) (#110) scripts/integration_test.sh hostnet_split_datasharing, hostnet_split_datasharing_udp
2026-09-14 same host id, separate IPC namespaces with --stats: talker and listener with FASTDDS_STATISTICS on the host network in an IPC namespace each, the tool in a third; unit suites on the three distributions arm64 2.6 (ros:humble), 2.14.6 (ros:jazzy), 3.6 (ros:lyrical) /chatter NONE, certain, warnings only shm-ipc-namespace-split (no shm-ipc-namespace-split-but-non-shm-traffic; the measured traffic is SHM only) on Jazzy and Lyrical. colcon test: Jazzy 434 tests, Lyrical 423, Humble 416, 0 failures (#111) scripts/integration_test.sh hostnet_split_stats
2026-09-14 node names across IPC namespaces: the host-network scenarios with the nodes in IPC namespaces other than the tool's; unit suites on the three distributions arm64 2.6 (ros:humble), 2.14.6 (ros:jazzy), 3.6 (ros:lyrical) writer_node / reader_node are the real /talker, /listener, /bounded_pub, /bounded_sub (before: _NODE_NAMESPACE_UNKNOWN_/_NODE_NAME_UNKNOWN_) in hostnet_noipc_shm and every hostnet_split_* scenario on Jazzy and Lyrical, and in hostnet_noipc_shm, hostnet_split_shm and hostnet_split_datasharing on Humble. On Humble the reader on the raw participant matched the nodes' writers but received nothing: Fast DDS 2.6 gives a participant with SHM only the SHM locator of a same-host endpoint, so the reader has a participant of its own without SHM there. hostnet_shm unchanged. colcon test: Jazzy 446 tests, Lyrical 421, Humble 428, 0 failures (#112) scripts/integration_test.sh hostnet_noipc_shm, hostnet_split_*
2026-09-15 same host id, separate IPC namespaces, shared 7000+ number: a node started first in the talker's and the listener's containers and a second one in the listener's, which takes the talker's 7001, the tool in the talker's IPC namespace; unit suites on the three distributions arm64 2.6 (ros:humble), 2.14.6 (ros:jazzy), 3.6 (ros:lyrical) /chatter NONE, certain, !shm-ipc-namespace-split with shm-reader-port-not-visible and no shm-port-collision (before: SHM, certain, no warning, the talker undecidable because 7001 was announced twice) on Jazzy and Lyrical; skipped on Humble, whose single SHM port per participant cannot tell this case. hostnet_split_shm (three distributions), hostnet_split_shm_visible, hostnet_split_stats, hostnet_noipc_shm, hostnet_shm (Jazzy, Lyrical) unchanged. colcon test: Jazzy 450 tests, Lyrical 443, Humble 450, 0 failures (#118) scripts/integration_test.sh hostnet_split_shm_shared_port
2026-09-15 native-buffer companions: large_array_pub (2 MB, 1 Hz) and large_array_sub with FASTDDS_STATISTICS on one host with rmw_fastrtps_cpp and with rmw_fastrtps_dynamic_cpp; unit suites on the three distributions arm64 2.6 (ros:humble), 2.14.6 (ros:jazzy), 3.6 (ros:lyrical) Lyrical, rmw_fastrtps_cpp: /large_array SHM with buffer-companion-folded, delivered 8 samples, 132 DATA submessages, 82 heartbeats (all through /large_array/_buf_cpu; before: 0 DATA, 0 heartbeats, not delivered on the parent pair); the companion topic only with --all, its writer and reader naming their parents in buffer_parent_guid. Lyrical with rmw_fastrtps_dynamic_cpp and Jazzy: no _buf_cpu topic, no buffer code, delivered 8, 231 DATA submessages on /large_array. colcon test: Jazzy 443 tests, Lyrical 436, Humble 443, 0 failures after a one-line loop uncrustify formats differently was rewritten; node --test 25 tests (#119) test/launch/test_large_shm.py
2026-09-15 RTPS_LOST direction: talker (ros2 topic pub, 20 Hz) and listener with FASTDDS_STATISTICS in two bridged containers, tc netem dropping 30 % of one node's packets to the other, the tool in a third container; unit suites on the three distributions arm64 2.6 (ros:humble), 2.14.6 (ros:jazzy), 3.6 (ros:lyrical) listener dropping: /chatter pair and topic lost_packets 0, no rtps-packets-lost; talker dropping: 38 lost packets on Jazzy, 36 and 45 on Lyrical, rtps-packets-lost, topic total equal to the pair (before: the talker's drops never showed). Skipped on Humble. Dropping a node's whole egress hid the listener from the tool on Lyrical (discovery and statistics lost in a 6 s observation), hence the filter on the peer's address. stats_multi_container unchanged (Jazzy, Lyrical). colcon test: Jazzy 445 tests, Lyrical 438, Humble 445, 0 failures; node --test 25 tests (#122) scripts/integration_test.sh stats_loss_multi_container
2026-09-16 per-distribution build directories: colcon build, colcon test and colcon test-result in dev on the three distributions back to back without cleaning, then one integration scenario each back to back; docker compose exec dev (no entrypoint); scripts/coverage.sh arm64 2.6 (ros:humble), 2.14.6 (ros:jazzy), 3.6 (ros:lyrical) three trees build/<distro>/{build,install,log}, each setup.bash and CMake cache naming only its own /opt/ros/<distro>; Humble configured right after Jazzy (before: the Jazzy CMakeCache.txt pointed Humble at /opt/ros/jazzy/src/gtest_vendor). colcon test: Jazzy 463 tests, Humble 463, Lyrical 456, 0 failures, each colcon test-result counting its own distribution only. hostnet_shm (Jazzy), hostnet_split_shm (Humble, Lyrical) pass. exec builds into build/jazzy/ too; coverage lands in build/jazzy/coverage/ (95.8 % lines). No top-level install/ or log/ created; a leftover install/setup.bash is not sourced, the entrypoint says so on stderr (#123) ROS_DISTRO=<distro> docker compose run --rm dev bash + colcon build && colcon test
2026-09-16 scale verification: scale_load small / medium / large / large_multi / limit and Nav2 + TurtleBot3 on Jazzy, large on Humble, Kilted and Lyrical, the tool inside the load container; web viewer timings in headless Chrome on the host; unit suites on the three distributions arm64 2.6.12 (ros:humble), 2.14.6 (ros:jazzy), 3.2.4 (ros:kilted), 3.6.2 (ros:lyrical) small and Nav2 within budget except the tool CPU next to Nav2 (one UDP receive thread, 1.0 core); from medium on the --watch frame, statistics losses and 5 s coverage are over budget, clearing a viewer filter from large on; discovery collapses at 135 processes. Numbers and what falls over first in Scale results. colcon test: Jazzy 466 tests, Humble 466, Lyrical 459, 0 failures after an uncrustify fix on Jazzy (#74, follow-ups #133–#138) scripts/scale_test.sh <scenario>, scripts/scale_viewer.js
2026-09-17 RATE removal: the demo and verification nodes with FASTDDS_STATISTICS in dev, a one-shot table, a --watch frame and fresh --json captures; unit and launch suites on the three distributions arm64 2.6 (ros:humble), 2.14.6 (ros:jazzy), 3.6 (ros:lyrical) no RATE column in the table, the --watch frames or the viewer, and no _fastdds_statistics_publication_throughput reader on any distribution; in a fresh capture topics[].throughput_bytes_per_s and measured.throughput_bytes_per_s are null and stats.throughput is {}, while the pairs' packets / bytes, stats.data_count and stats.traffic are unchanged, so a rate can still be computed from them. colcon test: Jazzy 489 tests, Humble 489, Lyrical 482, 0 failures (test_stats.py 7 tests, none skipped on Jazzy); node --test 26 tests (#137) scripts/render_examples.sh, web/sample/sample.json, test/launch/test_stats.py
2026-09-17 statistics throughput: the demo and verification nodes with FASTDDS_STATISTICS in dev, the scale_load medium and large rungs on Jazzy (three one-shot runs and a 60 s --watch each, the large rung three times over), unit and launch suites on the three distributions arm64 2.6 (ros:humble), 2.14.6 (ros:jazzy), 3.6 (ros:lyrical) medium (20 processes, 2400 pairs): 5 s statistics coverage 1.0 (before: 0.7376) and all 2400 pairs measured in the one-shot table (before: 1); large (40 processes, 5600 pairs): coverage 0.62 / 0.97 / 1.0 over three runs of the same build (before: 0.0 in three), a spread that is the host rather than the tool - the load alone takes 6.7 to 7.5 of the 8 cores before the tool starts. stats.samples_lost grows with the best-effort HISTORY_LATENCY reader and is counted apart in samples_lost_latency (1.7-1.9 M in a 60 s run at 40 processes); the counter part costs no measurement (the run that reported 925130 of 1040228 samples lost measured every pair), and watch_frame_p95_ms stays over budget (#135, #136). colcon test: Jazzy 491 tests, Humble 491, Lyrical 484, 0 failures; node --test 26 tests. One launch-test error on Lyrical (test_tool_with_statistics_in_its_own_environment: the /listener node stopped receiving mid-run and had to be killed at shutdown) reproduced neither alone nor in a full re-run (#141) scripts/scale_test.sh medium, scripts/scale_test.sh large, web/sample/diff.json
2026-09-17 --watch frame cost: the scale_load medium and large rungs on Jazzy (60 s --watch with --stats, --stats -v and without --stats), unit and launch suites on Jazzy arm64 2.14.6 (ros:jazzy) medium (20 processes, 2400 pairs): frame 152 / 204 ms with --stats (before: 344 / 667 ms), 54 / 81 ms without, within the 250 ms budget; resolve 0 ms on all but 2 to 4 frames of a run (before: 50 ms on every frame), update 8 ms (before: 115 ms). large (40 processes, 5600 pairs, not judged: the load takes most of the host): median 1.6 s (before: 8.9 s), 0.44 s without --stats. stats_dropped_samples stays over budget and varies between runs (#147). colcon test: Jazzy 493 tests, 0 failures; two launch-test errors in test_unbounded_vs_bounded.py in the first full run did not reproduce in a re-run (#135) scripts/scale_test.sh medium, scripts/scale_test.sh large
2026-09-17 lost measurements instead of lost samples: the scale_load medium rung on Jazzy (three one-shot runs, --stats at 5 s and 30 s, 60 s --watch with --stats, --stats -v and without), unit and launch suites on the three distributions arm64 2.6 (ros:humble), 2.14.6 (ros:jazzy), 3.6 (ros:lyrical) medium (20 processes, 2400 pairs): every budget holds. --watch coverage 1.0 - 2000 pairs with a delivery proof, none without a measured packet, in both --stats runs - with 1 counter sample dropped, which the budget it replaces (#147) would have failed; 5 s coverage 1.0; the 30 s document lost 18052 samples, all of them best-effort HISTORY_LATENCY gaps, and carries no stats-samples-lost; frame 155 / 186 ms (-v 180 / 206 ms, without --stats 51 / 86 ms) scripts/scale_test.sh medium
2026-09-18 stalled statistics counters on Fast DDS 3.6: the scale_load medium rung on Lyrical with heartbeat_period 100 ms, 250 ms, 500 ms and 1 s on the counter writers of statistics.xml, large on Lyrical and medium on Jazzy with the generated profiles arm64 2.14.6 (ros:jazzy), 3.6.2 (ros:lyrical) Lyrical medium (20 processes, 2000 pairs): --watch coverage 1.0 / 1.0 / 1.0 / 0.21 with 0 dropped samples (before: 0.0, #152); 500 ms is shipped, and scripts/scale_test.sh medium now fails on Lyrical below 0.95 (seen failing at 1 s). The tool works for the first time at this rung on Lyrical and the frame p95 goes from 152 ms to 239-401 ms over three runs (not judged: the budgets are Jazzy's). Lyrical large (not judged): the samples now arrive and the tool falls behind, 1990969 dropped, --watch coverage 0.0, frame p95 19 s (before: 2.5 s with almost no sample to process). Jazzy: heartbeat_period is heartbeatPeriod in Fast DDS 2.x and the 3.x spelling makes 2.14 drop the whole profile, so the installed profiles are generated by CMake; the Jazzy one differs from the previous file by its comment only. Jazzy medium on this day and host: frame p95 179 ms with the previous tree, 207 / 303 / 343 / 214 / 218 ms (-v 235 ms against 269 / 348 / 437 / 330 / 865 ms) with this one, coverage 1.0 in all - a spread of the host, with an identical run-time configuration
2026-09-18 delivered rate (HZ): rate_load pairs at 10 / 100 / 1000 Hz on Jazzy and Lyrical (rate_stats), the scale_load medium rung on Jazzy with the HISTORY_LATENCY reader depth raised from 10 to 100, unit and launch suites on Jazzy and Lyrical, the web viewer tests arm64 2.14.6 (ros:jazzy), 3.6.2 (ros:lyrical) every rate_stats pair within 3 % of its publish rate on both distributions (Lyrical 1000 Hz: 998.0 / 997.9 / 997.5) and none a lower bound; medium (20 processes, 2400 pairs, 20000 delivered samples/s): every budget holds with depth 100 - frame 150 / 181 ms (-v 181 / 218 ms, without --stats 53 / 84 ms), coverage 1.0 / 1.0 - and all 2000 delivered pairs carry a rate, every one a lower bound (≥, 6.7 to 10.0 /s over 30 s) because the load is ten times the reader ceiling of 2000 samples/s (55478 HISTORY_LATENCY gaps in 30 s); the 5 s document has the same 2000 lower bounds with samples_lost_latency 0, the in-reader overwrite the counter does not see scripts/integration_test.sh rate_stats, scripts/scale_test.sh medium
2026-09-19 statistics writer QoS of the shipped profiles (#154): the scale_load medium rung on Jazzy and Lyrical with three profile variants - as shipped (push mode, default flow controller), push mode on FastDDSStatisticsFlowControllerDefault, and Fast DDS's own writer QoS (pull mode, fastdds.push_mode=false, on that controller) - three runs each, six for the adopted one on Jazzy arm64 2.14.6 (ros:jazzy), 3.6.2 (ros:lyrical) pull mode, Jazzy (heartbeat 3 s): --watch proves no pair delivered in 60 s (pairs_delivered 0 in all three runs, coverage undefined) and 1974 / 3281 / 2037 counter samples lost (-v 306 / 36293 / 6841) - keep-last 10 overwrites the instances before the reader's ACKNACK; the 5 s one-shot still measured 2000 pairs. pull mode, Lyrical (heartbeat 500 ms): coverage 1.0 / 1.0 / 1.0, 0 lost, frame p95 155 / 170 / 168 ms, tool CPU 0.50 / 0.48 / 0.49 cores (as shipped: 0.69 / 0.72 / 0.73). push mode + statistics flow controller: Jazzy coverage 1.0 in six runs, frame p95 219 / 233 / 278 / 190 / 209 / 197 ms, CPU 0.67-0.95 cores, dropped 0 in ten of twelve --stats runs and 19226 / 36246 in the other two (as shipped: 0 / 0 / 23, -v 0 / 4 / 6; the same host spread as the 5823 of #141); Lyrical coverage 1.0, 0 dropped, p95 214 / 200 / 238 ms (as shipped 157 / 310 / 216 ms). Adopted: push mode on the statistics flow controller on both distros; pull mode fails the Jazzy budget and would need a per-version split for Lyrical alone. colcon test: Jazzy 513 tests, 0 failures, 60 skipped; Lyrical 508, 0, 58; Humble 513, 0, 74 scripts/scale_test.sh medium, src/fastdds_transport_viz/test/test_stats_profiles.py
2026-09-19 per-participant SHM ports in --json (#125): the split-namespace scenarios with the tool in the talker's IPC namespace (shared 7000+ number) and in a third one, on Jazzy and Lyrical; unit suites on the three distributions; the web viewer tests arm64 2.6 (ros:humble), 2.14.6 (ros:jazzy), 3.6.2 (ros:lyrical) shared number, from the talker's namespace: the talker's participant visible with 7001 held announced by 2 and not a proof, its own port held announced by 1 and a proof; the listener's not-visible with a port absent; the tool's participants own, unprobed. Third namespace (hostnet_split_shm): both nodes not-visible, 7000 announced by 3 (the tool's own number) and each node's other port absent, shm.unknown_ports empty; verdicts and warnings unchanged in both. web/sample/shm_split.json re-taken from that run (both node names real since #112), web/sample/diff.json regenerated (shm.unknown_ports only). colcon test: Jazzy 517 tests, Humble 517, Lyrical 513, 0 failures, after two brace initializers Humble's uncrustify formats differently were rewritten; node --test 28 tests scripts/integration_test.sh hostnet_split_shm_shared_port, hostnet_split_shm, web/sample/shm_split.json, test/fixtures/participants.json
2026-09-19 per-endpoint data-sharing segment visibility in --json (#163): the split data-sharing scenarios with the tool in a third IPC namespace and, UDPv4 only, in the publisher's, on Jazzy and Lyrical; unit and launch suites on the three distributions; the web viewer tests arm64 2.6 (ros:humble), 2.14.6 (ros:jazzy), 3.6.2 (ros:lyrical) third namespace: both /bounded endpoints not-visible; publisher's namespace (UDPv4): writer visible with its 4016-byte history, reader not-visible, matching datasharing-reader-segment-not-visible. The demo talker (data-sharing OFF) reads unprobed, the launch data-sharing pair visible on both sides. Verdicts and warnings unchanged. web/sample/diff.json regenerated (the new key, unprobed, only). colcon test: Jazzy 520 tests, Humble 520, Lyrical 516, 0 failures; node --test 29 tests scripts/integration_test.sh hostnet_split_datasharing, hostnet_split_datasharing_udp, test/fixtures/datasharing.json
2026-09-20 web viewer rendering (#136): the scale_load medium and large rungs regenerated on Jazzy, scripts/scale_viewer.js in headless Chrome 153 on the host, five loads each; the web viewer tests arm64 2.14.6 (ros:jazzy) large (1467 arrows): first render 53 ms (before 156), topic / node / clear filters 120 / 123 / 146 ms with the 100 ms typing debounce inside (clear was 136 ms of pure redraw before), select / deselect 35 / 33 ms; medium 33 ms, 133 / 133 / 133 ms, 33 / 34 ms - every action under the 100 ms budget, 33 ms being the two-frame floor of the harness. node --test 43 tests (web/test/scene.test.js added) scripts/scale_viewer.js, web/scene.js
2026-09-20 Discovery Servers in --json, the -v footer and the web viewer (#86): test_discovery_server.py (legacy fastdds discovery server, 11811) on Humble, Jazzy and Lyrical, test_easy_mode.py and the easy_mode_tcp / easy_mode_shm scenarios on Lyrical; unit suites on the three distributions; the web viewer tests arm64 2.6 (ros:humble), 2.14.6 (ros:jazzy), 3.6.2 (ros:lyrical) legacy server: one SERVER participant with no endpoints, prefix 44.53.00.5f... on 2.x and name eProsima Guidless Server (random prefix) on 3.6, UDPv4 127.0.0.1:11811, every node CLIENT (Humble/Jazzy) or SUPER_CLIENT (Lyrical announces clients so) attributed to it, the tool's own participants never. Easy Mode across bridged containers: two DiscoveryServerAuto on two host ids, port 7402, each node a SUPER_CLIENT of its host's server; the server takes the host name of its host's nodes. Verdicts and warnings unchanged. colcon test: Jazzy 232 tests in the touched subset, Humble 226, Lyrical 224, 0 failures; node --test 48 tests scripts/integration_test.sh easy_mode_tcp, easy_mode_shm, web/sample/easy_mode.json
2026-09-20 exit code of the scale harness (#167): scripts/scale_test.sh medium on Jazzy with the judged rung, test_scale_measure.py and the linters on Jazzy arm64 2.14.6 (ros:jazzy) scale_measure.py --judge prints FAIL - watch_frame_p95_ms 386.0 (must be < 250.0), stats_coverage 0.0 (must be >= 0.95) and scale_test.sh exits 1 (before: 0 with the same row). The 5 s coverage is #168; the frame p95 (314 ms with --stats, 386 ms with -v, 69 ms without) is the host spread already logged on 2026-09-18 (207 to 343 ms) and is recorded, not hidden: the gate names it. A first run had --judge swallowed by run_synthetic's optional split argument and exited 0 with judged: false; the arguments are explicit now. colcon test (pytest and lint subset): 272 tests, 0 failures scripts/scale_test.sh medium, src/fastdds_transport_viz/test/test_scale_measure.py
2026-09-20 the --stats one-shot stop rule (#168): the scale_load medium rung on Jazzy (three runs) and Lyrical (three runs) with the settle rule, the 30 s reference at --quiet 0; a bounded investigation at 10 x 100, 20 x 100, 20 x 300, 20 x 500 and 10 x 500 with and without HISTORY_LATENCY and with the counter writers at depth 1, reported on the issue; test_stats.py::test_default_one_shot_settles (talker/listener: stopped_on settled); the unit tests of stats_settled Jazzy, Lyrical medium: coverage 0.953 / 1.0 / 0.9575 and 1.0 / 1.0 / 1.0 within budget, settled at 16.6-22.7 s; watch_frame_p95_ms still over (#177); one Lyrical --watch --stats run measured 2 % of the pairs (stats_watch_coverage 0.02), not reproduced in the other five runs
2026-09-20 the --watch frame gate (#177): the scale_load medium rung on Jazzy (three runs) and Lyrical (three runs) with the median and p95 budgets and the --watch start rule; a bounded diagnosis of the Lyrical -v run (three runs each of -v and --stats alone, FTV_PROFILE per frame); test_scale_measure.py (median / p95 judged apart, the host share on the FAIL line, results without load_before); the unit tests of watch_ready arm64 2.14.6 (ros:jazzy), 3.6.2 (ros:lyrical) frame median 186 / 190 / 196 ms (Jazzy) and 184 / 218 / 196 ms (Lyrical) with --stats, 209-235 ms with -v, p95 227-328 ms: the gate passes on both distributions in all six runs (rows before this one were judged on the p95 alone at 250 ms). 27-29 frames per 60 s run (before: 16-22). The -v slowdown of the previous rows (753 / 925 ms) was the host: /proc/loadavg climbs from 3.6 to 24 over six back-to-back runs, and -v alone costs 27 ms of render per frame. One Lyrical run failed stats_coverage (0.0: the default one-shot settled at 8.0 s with no pair measured, #179) scripts/scale_test.sh medium, src/fastdds_transport_viz/test/test_scale_measure.py
2026-09-20 the --stats one-shot settle rule counting reader-bound entries only (#179): the scale_load medium rung on Lyrical (three runs) and Jazzy (one run); the unit tests of reader_ports / measures_a_pair and the stats.measured_instances round-trip; colcon test on the three distributions arm64 2.14.6 (ros:jazzy), 3.6.2 (ros:lyrical), 2.6 (ros:humble, unit suites) stats_coverage 1.0 in all four runs (before: one Lyrical run 0.0, settled at 8.0 s with 47 metatraffic / own-port entries). Settled at 27.7 s and 24.3 s on Lyrical, once at the 30 s cap (stopped_on timeout, 545 entries measured, coverage 1.0), at 15.4 s on Jazzy; 525-640 measured_instances. Frame median 174-233 ms, p95 211-308 ms, all within budget. colcon test: Jazzy 545 tests, Lyrical 541, Humble 545, 0 failures scripts/scale_test.sh medium, test_decision.cpp, test_render_json.cpp
2026-09-20 rate_stats on GitHub-hosted runners (#186): the CI integration jobs of that day (x86_64 and arm64 Jazzy, arm64 Rolling) and the scenario on the dev host after the change x86_64 + arm64 (runners), arm64 (host) 2.14.6 (ros:jazzy), 3.x head (ros:rolling) the 1000 Hz rung failed three CI jobs with every pair a lower bound at 975.7 / 983.7 / 982.0 /s (10 and 100 Hz at 100.0 /s every time, the same code passing on the runs around them and on re-run); the assertion now accepts a lower bound within [0.9, 1.03] × rate, the strict ±3 % stays for a rate without one. Dev host: 10 / 100 / 1000 Hz pass, no lower bound scripts/integration_test.sh rate_stats
2026-09-20 the web viewer in a browser (#81): web/test/browser.test.js and web/test/live.test.js against headless Chrome on the host, plus the existing Node unit tests arm64 (host) - (viewer only) node --test "web/test/*.test.js" 67 tests, 0 failures in 12 s with Node 22.17.0 and Chrome 153.0.8010.48 (48 before: 14 browser tests and one live-mode test with 4 subtests added). Without a browser those 15 tests skip with a reason and the 48 unit tests still run; FTV_REQUIRE_BROWSER=1 fails instead, and a $CHROME that is not an executable fails rather than skipping. A removed pair turned out to have no ghost arrow when its nodes are gone from the after document (sceneEdges() keeps ghosts only between known nodes), so that case is asserted in the table, where the ghost row is web/test/cdp.js, web/test/fake_transport_viz.js, web/serve.py
2026-09-20 type mismatches on the same topic (#85): the type-name case as a launch test (ros2 topic pub Int32 and ros2 topic echo String on one topic) on the three distributions; a type-hash probe with two builds of the same message package (int32 data against string data, so the same DDS type name with two REP-2011 hashes) published and subscribed the same way, with and without --stats; unit suites and node --test arm64 2.14.6 (ros:jazzy), 3.6.2 (ros:lyrical), 2.6 (ros:humble) name mismatch: one NONE / certain pair with type-name-mismatch and an empty unmatched_reasons on all three (before: no pair at all and a topic-level reason). Hash mismatch: both sides announce a hash on Jazzy and Lyrical (RIHS01_a299ad13… against RIHS01_369ac5ac…, the same values on both), the pair stays SHM x1 / certain and gains !type-hash-mismatch, and ros2 topic echo printed nothing on either. With --stats the two behaviours the explanation names: Jazzy measured 35 delivered samples, 38 SHM packets and 0.41 ms latency (Fast DDS 2.x matches the pair and the rmw drops the samples), Lyrical 0 DATA submessages and delivered false (3.x never matches). Humble announces no hash at all (type_hash "" on both endpoints), so no warning although the subscription receives nothing - the documented limitation. colcon test: Jazzy 557 tests, Lyrical 553, Humble 557, 0 failures; node --test "web/test/*.test.js" 68 tests test/launch/test_type_mismatch.py, test_decision.cpp, web/test/model.test.js
2026-09-21 the live reconnect banner and the recovery after it (#191): a TCP proxy in front of web/serve.py drops the browser's SSE socket, serve.py never hearing about it; the whole web suite run 8x on the host arm64 (host) - (viewer only) the banner appeared 1-11 ms after the cut and the recovery 1002 ms later, from the latest document serve.py sends to every new connection - no new frame needed; the same probe on the browser's own default took 3032 ms, which is what retry: 1000 replaces. A frame stepped after the recovery still arrived, so the stream was live and not merely reconnected. node --test "web/test/*.test.js" 72 tests (68 before), 13 s, 8/8 runs green with Node 22.17.0 and Chrome 153.0.8010.48. Two flakes were found and fixed on the way: a ?src= page renders once while it is still empty, so goto() could return before the fetch and a filter test then asserted an empty graph (1 failure in 5 runs before goto() waited for #meta); and SIGTERM kills serve.py before its cleanup runs, orphaning the transport_viz it started - that orphan holds the test's stderr pipe and node never exits, so the test lets the producer stop itself first web/test/live.test.js, web/test/cdp.js, web/serve.py
2026-09-21 multicast stamping (#130): a 20 Hz BEST_EFFORT talker and two ros2 topic echo readers on 239.255.0.7:7900 (defaultMulticastLocatorList) with FASTDDS_STATISTICS, the talker on one, two and three container networks and the readers on one of them; the same pair in a single network namespace; an interface whitelist on unicast traffic; three 45 s --stats runs per rung, the tool in a container of its own arm64 2.14.6 (ros:jazzy), 3.6.2 (ros:lyrical) confirmed: a receiver's RTPS_LOST divided by the sender's RTPS_SENT on the group is the sender's interface count. Jazzy, 12 rows per rung (4 reporting participants x 3 runs): 0.93-1.03 on one interface, 1.86-2.00 on two, 2.74-2.92 on three, 0.00 in every row of the one-namespace control, all five rungs pass. Lyrical: 0.91-1.00, 1.63-2.00, 2.29-2.98 (one row of twelve under the 20 % band, its own reporter reading 2.65 and 2.75 in the other two runs), 0.00 in the control. One interface already costs one false loss per message: the any-address socket sends a copy of its own with IP_MULTICAST_IF on localhost, which is why nothing shows when sender and receiver share a namespace. Nothing is lost on the wire - the /chatter pair delivers 898 of 898 samples at 20 Hz with lost_packets 0, multicast destinations being excluded (#122). Unicast with an <interfaceWhiteList> of all three addresses reports no loss either (487-911 packets per run, ratio 0.00); the duplicate datagram per whitelisted socket is read from the source, not measured here. Not run on Humble, whose Fast DDS 2.6 binaries have no statistics module. Side finding: a pair that receives on a multicast locator only never satisfies the one-shot settle rule (measures_a_pair counts discovered reader unicast ports, #179), so every run ended at its --timeout warning no measured RTPS_SENT entry to a discovered reader although the pair was fully measured (#196). colcon test: Jazzy 570 tests, Lyrical 566, Humble 558, 0 failures scripts/multicast_stamping_test.sh
2026-09-21 the settle rule counting the multicast groups a reader announces (#196): the mcast2 rig of the multicast profile (readers on 239.255.0.7:7900 and no unicast locator) on Jazzy and Lyrical, before and after; the scale_load medium rung on both for the unicast case; colcon test on the three distributions arm64 2.14.6 (ros:jazzy), 3.6.2 (ros:lyrical), 2.6 (ros:humble, unit suites) Before: --timeout 45 on both distributions, measured_instances 0 and the "no measured RTPS_SENT entry" warning, while the pairs themselves were measured (878, 876 and 893 packets). After: settled at 8.02-8.10 s in five runs (Jazzy 8.03, 8.04, 8.05, 8.05, 8.05; Lyrical 8.02, 8.04, 8.10), no warning, 1-5 measured instances and 117-150 packets per pair in the 8 s window; with --quiet 0 the same 45 s window measures 902 packets, as before. The medium rung passed on both (stats_coverage 1.0, watch 1.0), and the readers there announce no multicast at all, so the rule is unchanged for them. colcon test: Jazzy 570 tests, Lyrical 566, Humble 570, 0 failures FTV_MCAST_IFACES=2 docker compose --profile multicast up -d talker_mcast listener_mcast listener_mcast2 + transport_viz --json --stats --timeout 45, scripts/scale_test.sh medium
2026-09-21 intra-process pairs and the one-shot settle rule (#201): the new intra_process scenario (a single rate_load both process on /rate_intra, with and without --stats) on Jazzy, Lyrical and Humble; rate_stats at 10 / 100 / 1000 Hz on Jazzy; the scale_load medium rung on Jazzy and Lyrical for the cross-process case; colcon test on the three distributions and the web suite arm64 2.14.6 (ros:jazzy), 3.6.2 (ros:lyrical), 2.6 (ros:humble, unit suites and the non---stats half) Before: the --stats run burned its whole --timeout and ended on the warning no measured RTPS_SENT entry to a discovered reader, the pair reading SHM (unmeasured, delivered) with delivered-without-measured-traffic and rtps-sent-absent, whose remedy named a Fast DDS 3.6 build flag that cannot apply. After: stats.measurable_pairs 0, settled at 5.0 s (the minimum window), pairs_delivered 0, stopped_on settled, stderr empty, and the pair reads SHM (intra-process) with the intra-process reason - also without --stats (stopped_on quiet) and on Humble, where no statistics module exists. rate_stats: only /rate_intra carries the reason, the SHM and data-sharing pairs across two processes keep their verdicts and their measured packets, and no pair anywhere reads delivered-without-measured-traffic. The medium rung, where every pair spans two processes, is unchanged: Jazzy stats_coverage 1.0 / watch 1.0, settled 2.4 s, frame median 161.3 ms; Lyrical 1.0 / 1.0, 3.0 s, 193.6 ms - both pass. colcon test: Jazzy 581 tests, Humble 581, Lyrical 577, 0 failures; node --test "web/test/*.test.js" 73 tests (72 before) scripts/integration_test.sh intra_process, rate_stats, scripts/scale_test.sh medium, test_decision.cpp
2026-09-21 the reader destinations of the statistics drain (#200): the talker_stats / listener_stats rig on Jazzy, --stats at --quiet 0, at the default --quiet, at --timeout 3 and as --watch --json, before and after; the scale_load medium rung on Jazzy and Lyrical for the #177 frame budget; colcon test on the three distributions arm64 2.14.6 (ros:jazzy), 3.6.2 (ros:lyrical), 2.6 (ros:humble, unit suites) Before: stats.measured_instances 0 in every mode but the default one-shot - --quiet 0 (pairs measured 25 packets) and --timeout 3 (2 packets) both warned no measured RTPS_SENT entry to a discovered reader, and all 174 frames of a --watch run read 0 while the pair carried up to 473 packets; only the default one-shot measured (2 instances, settled at 8.1 s). After: --quiet 0 5 instances and no warning, --timeout 3 1 instance and no warning, the default one-shot unchanged (2, settled at 8.07 s), and all 10 frames of a --watch --interval 2 run 2 instances - the same measure in every mode. The medium rung passed on Jazzy (frame median 177.9 ms, p95 212.3 ms, stats_coverage 0.95, watch 1.0); on Lyrical it went over the frame budget in three runs (median 560-936 ms on the -v run, whole-VM load 6.2-6.7 cores during it), and so did the binary without this change under the same load (875.0 / 2506.8 ms, watch coverage 0.0), so the rung is not judged here. colcon test: Jazzy 593 tests, Lyrical 589, Humble 593, 0 failures (61 / 58 / 78 skipped) docker compose up -d talker_stats listener_stats + transport_viz --json --stats at each mode, scripts/scale_test.sh medium, colcon test
2026-09-21 whether Fast DDS's own type description can replace the missing ROS 2 type hash on Humble (#193): a throwaway listener probe printing has_type_id() / has_type() / has_type_information() (3.x: type_information.assigned() and both TypeIdentifier hashes) and the USER_DATA of every discovered endpoint, run against demo_nodes_cpp talker + listener with default settings and no profiles file, one ROS_DOMAIN_ID per distribution; the emission conditions read in the Fast DDS 2.6.x and 2.14.x sources arm64 2.6.12 (ros:humble), 2.14.6 (ros:jazzy), 3.6.2 (ros:lyrical) Humble announces none of the three: 0 of 36 endpoints, across 17 distinct DDS type names (std_msgs::msg::dds_::String_, rcl_interfaces::msg::dds_::Log_, the eight parameter service types, rmw_dds_common::msg::dds_::ParticipantEntitiesInfo_), and its USER_DATA is empty rather than merely missing the typehash key; rmw_fastrtps_dynamic_cpp gives the same 0 of 36, so it is not an rmw-variant effect. Jazzy is the same, 0 of 40 - the legacy 2.x fields are dead on both 2.x distributions, and Jazzy is covered by the USER_DATA typehash alone. Lyrical fills the XTypes 1.3 field on 38 of 38 endpoints, EK_MINIMAL (0xf1) and EK_COMPLETE (0xf2) hashes both non-empty (rt/chatter: minimal 59656d08...9cb1, complete bd8756fc...eb7b, identical on writer and reader); three structurally identical rcl_interfaces request types share one minimal hash while their complete hashes differ (18 type names, 16 distinct minimal, 18 distinct complete). Why, in the 2.6 sources: serialisation is unconditional on a non-empty value (WriterProxyData.cpp:549/557/593) and auto_fill_type_information defaults to true (TopicAttributes.h:46), so the flags are not the blocker - EDP.cpp:305-345 asks TypeObjectFactory::get_type_information(typeName), which returns nullptr when no TypeIdentifier is registered for that name (TypeObjectFactory.cpp:228-236), and registration only happens for a DynamicPubSubType (DomainParticipantImpl.cpp:1479-1483: a failed dynamic_cast does nothing), while rmw_fastrtps's TypeSupport is a plain TopicDataType. The same code is in 2.14.x. The gap is therefore publisher-side and cannot be closed from the tool: with no value on the wire there is nothing to compare, so the two-build message-package rig of #85 was not needed. No detection shipped; the limitation is now documented as a measured fact tools/probes/p193/ (type_probe.cpp, in the style of src/fastdds_transport_viz/src/discovery_observer.cpp; build.sh compiles it in each image against the installed Fast DDS, libfastrtps on 2.x and libfastdds on 3.x, and run.sh starts it next to the demo nodes); docker compose up talker listener per distribution on its own domain
2026-09-21 grouping service and action endpoints under --all (#84): the new test_service_action.py rig (demo_nodes_cpp add_two_ints_server with a client that keeps calling, and an example_interfaces/action/Fibonacci server and client that keep sending goals, so the members stay discoverable) on the three distributions; a --all --json capture of a whole Jazzy system as the viewer sample; the unit suites, the web suite and mkdocs build --strict arm64 2.14.6 (ros:jazzy), 3.6.2 (ros:lyrical), 2.6 (ros:humble) the wire agrees with the rule on all three: a service is two members of one group (rq/add_two_intsRequest to_server, rr/add_two_intsReply to_client) and an action is eight, three to the server and five back (send_goal / cancel_goal / get_result requests out, their replies plus feedback and status back), kind service / action with group /add_two_ints and /fibonacci; the table shows exactly one SERVICE /add_two_ints /ftv_srv_client -> /add_two_ints_server row and one ACTION /fibonacci /ftv_act_client -> /ftv_act_server row (1 | 1 and 3 | 5 pairs, SHM -> SHM), with no rq/, rr/ or /_action/ name anywhere in the output, and /parameter_events and /rosout keep kind topic with no group. A parameter service nobody calls is still one SERVICE row, half open (- -> /node, 0 | 0, both no-matching-reader and no-matching-writer on it). The sample capture (web/sample/services.json, a Jazzy talker + listener + the rig) holds 97 DDS topics - 86 service members, 8 action members, 3 plain topics - which the rule turns into 44 groups (43 services, 1 action), 42 of them parameter services nobody calls: the table goes from 117 rows to 67, and the service and action part of it from 94 to 44. colcon test: Jazzy 604 tests, Humble 604, Lyrical 600, 0 failures (61 / 78 / 58 skipped), the new launch test 6 tests in 37.8 / 38.3 / 39.6 s; node --test "web/test/*.test.js" 77 tests (73 before); mkdocs build --strict clean. One Jazzy failure (test_chatter_uses_shm, 4 pairs instead of 1) was cross-talk from a second distribution's suite running on the same ROS_DOMAIN_ID at the same time, and does not reproduce when the run has the network to itself test/launch/test_service_action.py, test/launch/service_action_rig.py, web/sample/services.json
2026-09-21 a differing type definition under one type name when a peer announces no ROS 2 type hash (#206): a throwaway non-ROS Fast DDS 3.x application (no rclcpp, no rmw; DynamicType registered with register_type_object_representation()) with a writer and a reader per side on rt/chatter under example_interfaces::msg::dds_::String_, side a struct { string data; } and side b struct { string data; int32 extra; }, next to demo_nodes_cpp talker + listener, on its own ROS_DOMAIN_ID; env knobs P206_EXT=final|appendable|mutable, P206_MEMBER_NAME, P206_NO_TYPEOBJECT=1; unit suites on the three distributions and the web suite arm64 3.6.2 (ros:lyrical), 2.14.6 (ros:jazzy), 2.6 (ros:humble) the non-ROS endpoints announce no USER_DATA and a non-empty EK_COMPLETE hash; the ROS 2 endpoints bd8756fc…eb7b on both sides. Default extensibility (APPENDABLE): the two sides 6a06ad52… and f9dac419…, both unlike ROS 2, so every pair between two definitions - a↔b, a↔ROS, b↔ROS in both directions, 6 of 9 - carries !type-information-mismatch and stays SHM / certain; a↔a, b↔b and talker→listener stay quiet. Fast DDS 3.x does not match them: each non-ROS side matched only itself and the ROS listener heard nothing from the app (before: the pair was indistinguishable from an idle one). P206_EXT=final: side a's hash is byte-identical to the ROS 2 one, its pairs with ROS lose the warning, and side b keeps it (4 of 9). P206_NO_TYPEOBJECT=1: both sides announce "", no warning anywhere, and the listener printed the app's samples of both definitions - matched on the type name alone, left to a follow-up. Jazzy and Humble announce no TypeInformation (#193), so type_information_hash is "" and the rule never fires there. web/sample/diff.json regenerated (the new key, "", only). colcon test: Lyrical 624 tests, Jazzy 628, Humble 628, 0 failures; node --test "web/test/*.test.js" 77 tests tools/probes/p206/nonros_app.cpp (tools/probes/p206/build.sh in the Lyrical image, whose fastcdr headers really live under includefastcdr; run_tool.sh, run_tool_table.sh, run_tool_stats.sh), test_decision.cpp
2026-09-21 whether anything tells a peer that announces no TypeInformation with a differing definition from one that is fine (#210): the #206 non-ROS app extended with per-writer DATA/DROP lines (P206_NO_TYPEOBJECT=1), a 3.x discovery-listener probe printing product_version, vendor, type_information, max_serialized_size, representation and type consistency, an SPDP/SEDP sniffer, one Lyrical + Jazzy + Humble mixed run, and a --stats run; the matching and statistics paths read in the Fast DDS 3.6.2 and 2.14.6 sources arm64 3.6.2 (ros:lyrical), 2.14.6 (ros:jazzy), 2.6.12 (ros:humble) no discriminating signal. A 3.x peer without a TypeObject announces no TypeInformation at all (PID 0x0075 absent, assigned() false; fill_type_information assigns only a non-TK_NONE identifier), exactly like every Jazzy and Humble endpoint. EDP::valid_matching compares TypeInformation only when both sides carry it, otherwise the type name alone; 3.x also ignores it from other vendors. The data, ROS talker -> struct { string data; int32 extra; }: take() returned NO_DATA for 9 of 17 samples (Hello World: 1..9) and extra=0 for the longer ones, with sample_lost / sample_rejected 0 and no log (DynamicPubSubType::deserialize returns false silently); the other directions decoded. --stats reported that pair exactly like talker -> listener (delivered_samples 10, no warning): HISTORY_LATENCY is recorded when the change enters the reader history, deserialization happens at take(). PID_TYPE_MAX_SIZE_SERIALIZED is sent but never parsed (0 for every remote writer) and on the wire collides both ways (12 for the ROS talker and 264 for a FINAL string data DynamicType of the same type, while 268 is both an APPENDABLE string data and a FINAL string data; int32 extra). product_version tells 3.x (3.6.2.0) from 2.x (0.0.0.0) but only says unchecked, never different. Documented as a limitation; the two gaps it found in the #206 rule (a minimal-hash match Fast DDS accepts, MINIMAL_BANDWIDTH peers) are a follow-up tools/probes/p210/ (probe.cpp, nonros210.cpp, sniff_sedp.py, build.sh, run_*.sh; the Fast DDS and rmw_fastrtps sources it was read against are listed in tools/probes/README.md)
2026-09-21 the tool's type rule against Fast DDS 3.x EDP::valid_matching / is_same_type (#213): the #210 non-ROS app extended with P213_STRUCT_NAME (registered struct name), P213_NESTED_NAME and P213_MODE=header (std_msgs/msg/Header via ros2 topic pub/echo), P213_TP=enabled|minimal_bandwidth|disabled|registration_only (fastdds.type_propagation), next to ROS 2 endpoints on rt/chatter; a probe printing both type identifiers; eleven ROS 2 message variants of one package (ws_*: member type changed, member added, default changed, constant added, member renamed, comment, bound changed, constant renamed, default removed) published and subscribed across rclcpp, rclpy and dynamic typesupport; every case run before the fix and again with the fixed tool, each on its own ROS_DOMAIN_ID arm64 3.6.2 (ros:lyrical) Fast DDS 3.x compares the complete or the minimal identifier and ignores the names when both sides announce a TypeInformation. The minimal identifier keeps member names (as a 4-byte hash), types, order and extensibility, and leaves out type names and annotations: a renamed struct and a renamed nested type (hdr_nested) matched and delivered, where #206 had warned; a peer with minimal_bandwidth announces the minimal identifier only (complete TK_NONE, PID present) and matched on it (mb_same, mb_rename_struct), except with an extra member or APPENDABLE extensibility (mb_extra, mb_appendable), which #206 had missed; disabled / registration_only announce no TypeInformation and are matched by name (#210). A different topic type name with the same definition (typename_diff) matched and delivered both ways (the app took 34 samples from the ROS talker), where #85 said NONE. Among ROS 2 versions of one message the REP-2011 hash and the TypeInformation differed together in every variant: type, added member, rename and bound never matched (0 received), default, constant and comment changes matched (29 received); the TypeInformation is identical across rclcpp, rclpy and dynamic typesupport. With the fix: rename_struct, hdr_nested, hdr_same, mb_same, mb_rename_struct quiet SHM x4; mb_extra, mb_appendable, rename_member, tp_enabled_extra NONE type-information-mismatch on the 2 cross pairs; tp_disabled_extra, tp_regonly_extra quiet (unchecked, #210); typename_diff SHM with type-names-differ-same-type on the 2 cross pairs; the #206 baselines flag the same pairs as before, now NONE: default 6 of 9, P206_EXT=final 4 of 9, P206_NO_TYPEOBJECT=1 0. ROS 2 rig: 25 of 25 cases agree with delivery - the 7 that received nothing (a_type, a_add, d_rename, e_bound, dyn_a_type, dyn_d_rename, py_py2py_a_type) are NONE type-information-mismatch + type-hash-mismatch (before: SHM + type-hash-mismatch), the 18 that received are SHM without a type warning. web/sample/diff.json regenerated (the new key, "", only). colcon test: Lyrical 629 tests, Jazzy 633, Humble 633, 0 failures; node --test "web/test/*.test.js" 77 tests tools/probes/p213/ and tools/probes/p213_ros/ (nonros213.cpp, probe.cpp, ws_*, build.sh, build_all.sh, run_*.sh, summarize.py), test_decision.cpp
2026-09-22 recording and replaying in the web viewer (#82): the new record_flip scenario on Jazzy, Lyrical and Humble; transport_viz_web --record tests in test_web_serve.py; the replay unit and browser tests; a 60-frame medium recording in headless Chrome arm64 2.14.6 (ros:jazzy), 3.6.2 (ros:lyrical), 2.6 (ros:humble, without --stats) record_flip passed on all three: Jazzy 25 frames with changes on frames 5, 6, 15 and 16 (kept as web/sample/recording.jsonl), Lyrical 25 frames, Humble 29 frames, /chatter SHM -> UDPv4 -> SHM in every one, and every line of the file a document after SIGTERM. The 332 MB medium recording: first frame 0.6-0.8 s, whole file 1.4-1.6 s, 33 ms per frame step, 45 MB heap. node --test "web/test/*.test.js" 97 tests (14 replay unit tests and 6 replay browser tests added) scripts/integration_test.sh record_flip, web/test/replay.test.js, web/test/browser.test.js
2026-09-22 live history in the web viewer (#218): the SSE id: test in test_web_serve.py, the stream-id / bound / compaction unit tests, the live browser tests, a 100-frame medium stream in headless Chrome arm64 — (a stand-in producer; no Fast DDS involved) node --test "web/test/*.test.js" 109 tests, three runs in a row (5 unit tests in replay.test.js, 1 new and 2 reworked tests with 10 subtests in live.test.js). The medium stream: 17 ms per frame following, 12 ms paused, 90 frames in 512 MB, a nine-frame drop in 14 ms, 38-49 MB heap, match by over the kept frames in 0.8 s web/test/replay.test.js, web/test/live.test.js, src/fastdds_transport_viz/test/test_web_serve.py
2026-09-22 metrics export (#83): /metrics tests in test_web_serve.py, test_render_csv, the --csv tests in test_cli_args.py and ros2transport's test_cli.py; a live demo_nodes_cpp talker / listener with FASTDDS_STATISTICS; the medium document through metrics_text() arm64 2.14.6 (ros:jazzy) 617 tests, 0 failures (the new and touched tests plus the linters). Live: --csv and ros2 transport list --csv print the header and the /chatter and /rosout rows, --watch --csv prints the header once, /metrics has the pair, measured, latency, warning, lost / resent and shm_* series with the host name and the startup line names it. medium: 24 413 series, 7.5 MB, 47 ms median src/fastdds_transport_viz/test/test_web_serve.py, src/fastdds_transport_viz/test/test_render_csv.cpp, src/fastdds_transport_viz/test/test_cli_args.py