@@ -238,53 +238,48 @@ 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.
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.
288283
289284** Startup: the acceleration is a one-time cost.** Time-to-first-hz-line
290285(` pixi run -e heavydemo demo-topic-hz-startup ` ):
@@ -302,52 +297,40 @@ does not cover. So acceleration adds a multi-second startup and **pays off for
302297long-running ` hz ` sessions** , not short invocations. (Needs the newer auto-PCH; see
303298the dev-bridge note below.)
304299
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.
300+ ### Large messages under BEST_EFFORT QoS
310301
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):
302+ ` ros2 topic hz ` subscribes with ` qos_profile_sensor_data ` (BEST_EFFORT). A 3 MB
303+ image fragments into roughly two thousand UDP datagrams, and if the OS socket
304+ receive buffer is smaller than one message a single dropped fragment loses the whole
305+ message. On a stock Linux install ` net.core.rmem_max ` defaults to about 200 KB, so a
306+ BEST_EFFORT reader receives little or nothing on a 3 MB topic — for stock rclpy and
307+ for the accelerated backend alike, since both use the same reader QoS. Raising the
308+ kernel limit resolves it (this needs root, and applies to any DDS user, not only
309+ rclcppyy):
318310
319311``` bash
320- sudo sysctl -w net.core.rmem_max=2147483647 # then re-run demo-topic-hz-cli --width 1024 --height 1024
312+ sudo sysctl -w net.core.rmem_max=2147483647
321313```
322314
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)
315+ With that in place, CycloneDDS's default socket buffer is large enough that a 3 MB
316+ BEST_EFFORT topic is received normally; no ` CYCLONEDDS_URI ` tuning is required. The
317+ 3 MB figures above were measured with this setting.
328318
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- ```
319+ The startup hook is covered by ` test/test_hook.py ` (install / uninstall / status;
320+ ` RCLCPPYY_ENABLE_HOOK=1 ` accelerates a fresh ` import rclpy ` ; unset and ` =0 ` leave
321+ stock rclpy untouched). For the acceleration on a node that * reads* message fields —
322+ where stock rclpy pays a per-message Python deserialisation that ` ros2 topic hz `
323+ (raw subscription) does not — ` scripts/heavy_hz_demo/run_heavy_hz.py ` is a controlled
324+ publisher/subscriber harness (` pixi run -e heavydemo demo-heavy-hz ` ).
342325
343- > ** Dev bridge.** The cold-vs-warm startup story and the fastest bring-up need the
326+ > ** Dev bridge.** The startup measurement and the fastest bring-up use the
344327> zero-config auto-PCH, which postdates the published suite 0.1.0. Until the next
345328> 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).
329+ > from a sibling ` cppyy_kit ` source checkout (default ` ../cppyy_kit ` , override
330+ > ` CPPYY_KIT_SRC ` ) and isolates its PCH cache under ` .heavy_demo_cache/ ` . It shares
331+ > the default solve (no extra conda deps) and leaves the default env's
332+ > published-channel dependency untouched ( ` workspace_activation.sh ` , gated on the
333+ > ` heavydemo ` env).
351334
352335### Extra demos (optional env)
353336
0 commit comments