@@ -238,116 +238,101 @@ the stock `ros2 topic hz` (and other rclpy-based verbs) runs on the C++ backend
238238** zero code changes** :
239239
240240``` bash
241- python -m rclcppyy.autoaccel install # once per environment (uninstall/status too)
241+ python -m rclcppyy.hook install # once per environment (uninstall/status too)
242242
243- RCLCPPYY_ENABLE_HOOK=1 ros2 topic hz /some_topic # accelerated: rclcpp executor under the hood
243+ RCLCPPYY_ENABLE_HOOK=1 ros2 topic hz /some_topic # runs on the rclcpp backend
244244ros2 topic hz /some_topic # env var unset -> ordinary rclpy, untouched
245245```
246246
247- Two environment variables gate it, side by side:
248-
249- | Variable | Effect |
250- | ---| ---|
251- | ` RCLCPPYY_ENABLE_HOOK=1 ` | turn the hook on for this process (unset = off) |
252- | ` RCLCPPYY_DISABLE_HOOK=1 ` | force it off even if ` RCLCPPYY_ENABLE_HOOK=1 ` — the opt-out wins |
253-
254- Mechanically, ` install ` writes a site-packages ` .pth ` (modelled on cppyy_kit's
255- auto-PCH bootstrap: guarded, never-raising, silent and near-zero-cost when the env
256- var is unset, ` RCLCPPYY_DISABLE_HOOK=1 ` opt-out, fully uninstallable). At interpreter
257- start the ` .pth ` registers a post-import hook on ` rclpy ` ; the first ` import rclpy `
258- then triggers ` enable_cpp_acceleration() ` — after rclpy is importable but before the
259- tool builds its node.
260-
261- Making ` ros2 topic hz ` work took one addition to the accelerator: it drives its own
262- loop with ` rclpy.spin_once ` (both in ros2cli's ` DirectNode ` discovery loop and the
263- hz loop), which the stock rclpy executor cannot use to service rclcpp-backed
264- entities — so ` enable_cpp_acceleration() ` now also patches ` rclpy.spin_once ` to
265- drive an rclcpp executor. Without that the CLI hangs at node bring-up.
266-
267- ** What acceleration buys ` ros2 topic hz ` , measured** (stock ` ros2 topic hz ` vs the
268- same binary under ` RCLCPPYY_ENABLE_HOOK=1 ` , against a C++ publisher of a 256×256 ` Image `
269- ≈ 0.19 MB, RELIABLE, ` --window 100 ` ; ` pixi run -e heavydemo demo-topic-hz-cli ` ;
270- absolute numbers vary by machine/run):
271-
272- | target Hz | publisher Hz | reported Hz (both) | stock CPU % | RCLCPPYY_ENABLE_HOOK CPU % | CPU ratio |
247+ ` RCLCPPYY_ENABLE_HOOK ` is the single control: ` RCLCPPYY_ENABLE_HOOK=1 ` turns the
248+ hook on for a process; unset, ` 0 ` , or any other value leaves it off. ` install `
249+ writes a ` .pth ` into the environment's site-packages that runs at every interpreter
250+ start; when the hook is off it is a near-zero-cost no-op, and `python -m
251+ rclcppyy.hook uninstall` removes it.
252+
253+ At interpreter start the ` .pth ` registers a post-import hook on ` rclpy ` ; the first
254+ ` import rclpy ` triggers ` enable_cpp_acceleration() ` , after rclpy is importable but
255+ before the tool builds its node. ` ros2 topic hz ` drives its loop with
256+ ` rclpy.spin_once ` (in ros2cli's ` DirectNode ` discovery loop and in the hz loop), so
257+ ` enable_cpp_acceleration() ` also patches ` rclpy.spin_once ` to run an rclcpp executor;
258+ otherwise the CLI would stall at node bring-up.
259+
260+ ** Measured** (stock ` ros2 topic hz ` vs the same binary under
261+ ` RCLCPPYY_ENABLE_HOOK=1 ` , against a C++ publisher of a ` sensor_msgs/Image ` ,
262+ BEST_EFFORT, ` --window 100 ` , with ` net.core.rmem_max ` raised (see below);
263+ ` pixi run -e heavydemo demo-topic-hz-cli ` ; numbers vary by machine and run):
264+
265+ | payload | target Hz | rclpy Hz | rclcppyy Hz | rclpy CPU % | rclcppyy CPU % |
273266| ---| ---| ---| ---| ---| ---|
274- | 50 | 50.0 | 50.0 | 1.6 | 1.4 | 1.1× |
275- | 100 | 100.0 | 100.0 | 3.8 | 2.1 | 1.8× |
276- | 200 | 200.0 | 200.0 | 6.3 | 2.7 | 2.3× |
277- | 500 | 500.0 | 500.0 | 11.7 | 6.3 | 1.9× |
278- | 700 | 700.0 | ~ 690 | 22.0 | 16.6 | 1.3× |
279-
280- Both report the true rate; the win is ** CPU** : acceleration runs the same hz on
281- roughly ** half to a third** less CPU at moderate rates. The win is narrower than for
282- a message-crunching node, and for an honest reason: ` ros2 topic hz ` (without
283- ` --filter ` ) subscribes with ` raw=True ` , so ** stock rclpy already skips
284- deserialisation** — the saving here is the C++ executor and message handling
285- replacing the Python executor, not avoiding a per-message copy. At very high rates
286- the gap narrows further because the accelerated path deserialises into a C++ proxy
287- while the stock raw path deserialises nothing.
288-
289- ** Startup: the acceleration is a one-time cost.** Time-to-first-hz-line
267+ | 3.0 MB | 100 | 99.0 | 97.0 | 48.7 | 18.8 |
268+ | 3.0 MB | 200 | 195.1 | 197.0 | 92.2 | 34.5 |
269+ | 3.0 MB | 300 | 291.4 | 296.7 | 102.7 | 42.6 |
270+ | 0.05 MB | 1000 | 998.2 | 999.7 | 19.3 | 7.3 |
271+ | 0.05 MB | 3000 | 2967.6 | 2985.0 | 46.6 | 17.2 |
272+
273+ Both backends deliver essentially the publisher's rate wherever the publisher can
274+ sustain it; the difference is CPU — the accelerated backend uses about a third to a
275+ half as much. ` ros2 topic hz ` (without ` --filter ` ) subscribes with ` raw=True ` , so
276+ stock rclpy does not deserialise the payload; the saving is the C++ executor and
277+ message handling replacing the Python executor, not avoiding a per-message copy.
278+ Delivered rates stay close because the raw stock reader is cheap enough to keep up
279+ where the publisher can feed it — but note where the CPU lands: on the 3 MB stream
280+ stock rclpy crosses one full core near 200–250 Hz (92–107 %), while the accelerated
281+ backend stays around a third of a core, so on a busier machine or at a higher rate
282+ the stock reader saturates first.
283+
284+ ** Startup: a one-time cost.** Time to first hz line
290285(` pixi run -e heavydemo demo-topic-hz-startup ` ):
291286
292287| ` ros2 topic hz ` first hz line | time |
293288| ---| ---|
294289| stock (no acceleration) | ~ 1.7 s |
295- | ` RCLCPPYY_ENABLE_HOOK=1 ` , cold PCH cache | ~ 8.0 s |
296- | ` RCLCPPYY_ENABLE_HOOK=1 ` , warm PCH cache | ~ 7.0 s |
297-
298- The suite's zero-config Cling PCH shaves ~ 1.1 s (the ` rclcpp ` header parse) off the
299- first line, cold→warm; the remaining ~ 5 s over stock is one-time rclcpp bring-up
300- plus first-use JIT of the message type and subscription glue, which the header PCH
301- does not cover. So acceleration adds a multi-second startup and ** pays off for
302- long-running ` hz ` sessions** , not short invocations. (Needs the newer auto-PCH; see
303- the dev-bridge note below.)
304-
305- ** On "why not 100 Hz?"** With the real tool the publisher hits its target exactly —
306- ` 50.0 … 700.0 ` published Hz in the table, confirmed by the publisher's own
307- ` PUB achieved_hz= ` line and by the stock hz reading. An earlier custom subscriber
308- that reported ~ 93 at a 100 Hz target was measuring its own per-second window with a
309- shallow RELIABLE queue that dropped a few messages, not a publisher shortfall.
310-
311- ** Honest boundary — heavy (3 MB) topics + BEST_EFFORT.** ` ros2 topic hz ` fixes its
312- reader to ` qos_profile_sensor_data ` (BEST_EFFORT). A 3 MB image fragments into
313- ~ 2000 UDP datagrams, and this machine's ` net.core.rmem_max ` is 208 KB, so the
314- socket buffer overflows and whole messages are lost — the stock hz reader often gets
315- ** nothing** on a 3 MB topic, and acceleration does not rescue it (same BEST_EFFORT
316- reader). This is a transport/kernel limit, not an rclpy-vs-rclcpp one. To measure a
317- genuinely heavy topic, raise the buffer first (owner action, needs root):
290+ | ` RCLCPPYY_ENABLE_HOOK=1 ` , cold cache | ~ 8.3 s |
291+ | ` RCLCPPYY_ENABLE_HOOK=1 ` , warm cache | ~ 4.1 s |
292+
293+ Two caches cut the warm start: the suite's Cling PCH removes the ` rclcpp ` header
294+ parse, and a per-message-type subscription cache removes the ~ 2.8 s one-time JIT of
295+ ` create_subscription<MsgT> ` (both built on the first run and loaded on later ones,
296+ cold → warm above). The warm figure is still above stock because the rest of the
297+ rclcpp bring-up — cppyy symbol discovery and the rclpy-compatibility adapters — is
298+ not yet cached. Acceleration therefore remains a one-time startup cost that pays off
299+ over a long-running ` hz ` session rather than a single short call. (Needs the newer
300+ suite; see the dev-bridge note below.)
301+
302+ ### Large messages under BEST_EFFORT QoS
303+
304+ ` ros2 topic hz ` subscribes with ` qos_profile_sensor_data ` (BEST_EFFORT). A 3 MB
305+ image fragments into roughly two thousand UDP datagrams, and if the OS socket
306+ receive buffer is smaller than one message a single dropped fragment loses the whole
307+ message. On a stock Linux install ` net.core.rmem_max ` defaults to about 200 KB, so a
308+ BEST_EFFORT reader receives little or nothing on a 3 MB topic — for stock rclpy and
309+ for the accelerated backend alike, since both use the same reader QoS. Raising the
310+ kernel limit resolves it (this needs root, and applies to any DDS user, not only
311+ rclcppyy):
318312
319313``` bash
320- sudo sysctl -w net.core.rmem_max=2147483647 # then re-run demo-topic-hz-cli --width 1024 --height 1024
314+ sudo sysctl -w net.core.rmem_max=2147483647
321315```
322316
323- The tests cover the feature hermetically (` test/test_autoaccel.py ` : install /
324- uninstall, ` RCLCPPYY_ENABLE_HOOK=1 ` accelerates a fresh ` import rclpy ` , unset and
325- ` RCLCPPYY_DISABLE_HOOK=1 ` leave stock rclpy untouched).
326-
327- ### Controlled comparison: a deserialising subscriber (dev bridge)
317+ With that in place, CycloneDDS's default socket buffer is large enough that a 3 MB
318+ BEST_EFFORT topic is received normally; no ` CYCLONEDDS_URI ` tuning is required. The
319+ 3 MB figures above were measured with this setting.
328320
329- Where ` ros2 topic hz ` uses raw subscriptions, a * normal* node that reads message
330- fields pays full per-message Python deserialisation on stock rclpy — and that is
331- where acceleration wins biggest. ` scripts/heavy_hz_demo/run_heavy_hz.py ` is a
332- controlled harness for that case (a C++ publisher + an hz-style subscriber that
333- reads the header, same script both ways via one ` enable_cpp_acceleration() ` line).
334- On a 3 MB image, RELIABLE, plain rclpy tops out ~ 210 Hz saturating ~ 1.6 cores while
335- the accelerated subscriber climbs past 330 Hz at half the CPU and a third of the
336- latency.
337-
338- ``` bash
339- pixi run -e heavydemo demo-heavy-hz # rclpy vs rclcppyy deserialising-subscriber table
340- pixi run -e heavydemo demo-heavy-startup # cold vs warm rclcppyy bring-up
341- ```
321+ The startup hook is covered by ` test/test_hook.py ` (install / uninstall / status;
322+ ` RCLCPPYY_ENABLE_HOOK=1 ` accelerates a fresh ` import rclpy ` ; unset and ` =0 ` leave
323+ stock rclpy untouched). For the acceleration on a node that * reads* message fields —
324+ where stock rclpy pays a per-message Python deserialisation that ` ros2 topic hz `
325+ (raw subscription) does not — ` scripts/heavy_hz_demo/run_heavy_hz.py ` is a controlled
326+ publisher/subscriber harness (` pixi run -e heavydemo demo-heavy-hz ` ).
342327
343- > ** Dev bridge.** The cold-vs-warm startup story and the fastest bring-up need the
328+ > ** Dev bridge.** The startup measurement and the fastest bring-up use the
344329> zero-config auto-PCH, which postdates the published suite 0.1.0. Until the next
345330> suite release the ` heavydemo ` pixi env bridges the newer ` rclcpp_kit ` /` cppyy_kit `
346- > from a sibling ` cppyy_kit ` ** source checkout** (default ` ../cppyy_kit ` , override
347- > ` CPPYY_KIT_SRC ` ) and isolates its PCH cache under ` .heavy_demo_cache/ ` . It is a
348- > ** dev/demo-only ** env: shares the default solve (no extra conda deps) and leaves
349- > the default env's published-channel dependency untouched
350- > ( ` workspace_activation.sh ` , gated on the ` heavydemo ` env).
331+ > from a sibling ` cppyy_kit ` source checkout (default ` ../cppyy_kit ` , override
332+ > ` CPPYY_KIT_SRC ` ) and isolates its PCH cache under ` .heavy_demo_cache/ ` . It shares
333+ > the default solve (no extra conda deps) and leaves the default env's
334+ > published-channel dependency untouched ( ` workspace_activation.sh ` , gated on the
335+ > ` heavydemo ` env).
351336
352337### Extra demos (optional env)
353338
0 commit comments