ZED Topic Benchmark Tool

Open in ClaudeOpen in ChatGPT

The ZED Benchmark tool measures the real performance of the topics published by the ZED ROS 2 nodes: how many messages per second actually reach a subscriber and how much bandwidth they consume. It is the recommended way to verify that a machine, a network, and a DDS configuration are able to deliver the camera data at the expected rate, and to quantify the benefit of Composition and IPC.

The tool is distributed in the tools/benchmark folder of the zed-ros2-examples repository.

For each received message the following information is available:

  • Topic type
  • Instantaneous frequency
  • Windowed average frequency
  • Message size
  • Instantaneous topic bandwidth
  • Windowed average topic bandwidth

The values are printed on the console in real time and published on a dedicated topic, so they can be plotted with tools like PlotJuggler or rqt_plot. When the test ends, a final report with the aggregated statistics of the whole session is printed and, optionally, saved to a file.

The benchmark node subscribes to the topic using a generic subscription, so it works with any ROS 2 topic, not only with the topics published by the ZED nodes.

The tool is available for ROS 2 Humble Hawksbill, Jazzy Jalisco, Lyrical Luth, and newer distributions. It is not available for ROS 2 Foxy and older releases, because they lack the generic subscription API required to subscribe to topics of an arbitrary type. On those distributions the three packages are still registered in the ament index, so the workspace build succeeds, but nothing is produced: no message, no component library, and no zed_topic_benchmark executable. A warning is printed by CMake during the build.

The packages

PackageDescription
zed_topic_benchmarkProvides the zed_topic_benchmark executable (a standalone process wrapping the component), the zed_test_ipc.launch.py multi-camera launch file, and the zed_check_ros2_config.sh helper script.
zed_topic_benchmark_componentThe stereolabs::TopicBenchmarkComponent ROS 2 component that implements the measurement engine. It can be loaded into any component container to leverage Intra Process Communication.
zed_topic_benchmark_interfacesThe custom message BenchmarkStatsStamped used to publish the statistics.

Installation

The benchmark packages are built together with the other packages of the zed-ros2-examples repository:

$cd ~/ros2_ws/src/ # use your current ROS 2 workspace folder
$git clone https://github.com/stereolabs/zed-ros2-examples.git
$cd ..
$sudo apt update
$rosdep update
$rosdep install --from-paths src --ignore-src -r -y --skip-keys scout_description
$colcon build --symlink-install --cmake-args=-DCMAKE_BUILD_TYPE=Release
$source ~/.bashrc

If you only need the benchmark tool, you can restrict the build to its packages:

$colcon build --symlink-install --cmake-args=-DCMAKE_BUILD_TYPE=Release \
> --packages-up-to zed_topic_benchmark

The benchmark packages do not depend on CUDA or on the ZED SDK, so they can also be installed on a remote workstation that only receives the data over the network. This is the best way to measure what a client machine really receives.

Usage

Start the ZED node, then open a new terminal console and run the benchmark node on the topic you want to measure:

$ros2 run zed_topic_benchmark zed_topic_benchmark --ros-args -p topic_name:=<name_of_the_topic_to_test>

For example:

$ros2 run zed_topic_benchmark zed_topic_benchmark --ros-args \
> -p topic_name:=/zed/zed_node/rgb/color/rect/image

The node prints the benchmark configuration, waits for the topic to be advertised (it polls the graph every 500 msec), and then starts measuring:

[INFO] [1665764376.862143962] [topic_benchmark]: ***** Benchmark parameters *****
[INFO] [1665764376.862355022] [topic_benchmark]: * Topic name: /zed/zed_node/rgb/color/rect/image
[INFO] [1665764376.862462857] [topic_benchmark]: Average window size: 500
[INFO] [1665764376.862496172] [topic_benchmark]: *** START BENCHMARK ***
[INFO] [1665764376.863484009] [topic_benchmark]: Advertised on topic: /zed/zed_node/rgb/color/rect/image_stats
[INFO] [1665764377.363430211] [topic_benchmark]: Found topic: '/zed/zed_node/rgb/color/rect/image' of type: 'sensor_msgs/msg/Image'
#119 | Freq 6.01/ 13.49 Hz | BW 42.28/ 94.87 Mbps | 901.12 KB

The last line is the live statistics line. It is updated in place; the counter is the number of processed messages, and frequency and bandwidth are reported as instant/average. The layout is fixed-width and stays within 80 columns so that it does not wrap on a default terminal.

By default the benchmark runs until you stop it with Ctrl+C. You can also limit it by time or by number of messages, and save the final report to a file.

Run for 30 seconds, then stop and print the report:

$ros2 run zed_topic_benchmark zed_topic_benchmark --ros-args \
> -p topic_name:=/zed/zed_node/rgb/color/rect/image \
> -p test_duration_sec:=30.0

Run until 1000 messages are received, then stop:

$ros2 run zed_topic_benchmark zed_topic_benchmark --ros-args \
> -p topic_name:=/zed/zed_node/rgb/color/rect/image \
> -p test_sample_count:=1000

Run for 60 seconds and also save the report to a log file:

$ros2 run zed_topic_benchmark zed_topic_benchmark --ros-args \
> -p topic_name:=/zed/zed_node/point_cloud/cloud_registered \
> -p test_duration_sec:=60.0 \
> -p log_file_path:=/tmp/zed_benchmark_report.txt

Parameters

ParameterDescriptionDefault
topic_nameName of the topic to benchmark.topic_name
avg_win_sizeSize of the moving window used to compute the average statistics, in number of messages.500
test_duration_secDuration of the test in seconds, counted from the first received message. When it elapses, the test stops and the report is generated. 0.0 means “run until interrupted”.0.0
test_sample_countNumber of messages to receive before stopping the test and generating the report. 0 means “run until interrupted”.0
log_file_pathPath of the file where the final report is written, in addition to the console. An existing file is overwritten. An empty string disables file logging.""
use_ros_logIf true, the live statistics and the report are printed through the ROS 2 logging system instead of the standard output. Useful when the node runs inside a launch file or a component container, where the in-place console update is not usable.false

All the parameters are read-only: they must be set when the node is started and cannot be modified at runtime.

When both test_duration_sec and test_sample_count are set, the test stops as soon as the first of the two limits is reached.

The value of avg_win_size defines how reactive the average is. A large window (e.g. 500) gives a very stable value, useful to characterize a steady stream; a small window (e.g. 30) reacts quickly and makes throughput drops easier to spot.

Final report

The report is generated when a test limit is reached or when the node is stopped with Ctrl+C. It is printed to the console and, if log_file_path is set, written to that file:

================ ZED TOPIC BENCHMARK REPORT ================
Topic name: /zed/zed_node/point_cloud/cloud_registered
Topic type: sensor_msgs/msg/PointCloud2
Stop reason: test completed (sample count reached)
Test duration: 10.00 s
Messages received: 150
-----------------------------------------------------------
Frequency [Hz] - mean: 15.00 | min: 14.82 | max: 15.13
Msg size - mean: 3.93 MB | min: 3.93 MB | max: 3.93 MB
Bandwidth [Mbps] - mean: 471.62 | min: 465.74 | max: 475.49
Total data: 589.82 MB
===========================================================

The report contains:

  • Topic name and topic type as discovered on the ROS 2 graph.
  • Stop reason: test completed (duration reached), test completed (sample count reached), or interrupted by the user (Ctrl+C).
  • Test duration: time between the first and the last received message.
  • Messages received and total data received during the test.
  • Mean / min / max of frequency, message size, and bandwidth.

If no message is received at all, the report reports it explicitly instead of printing meaningless statistics.

Understanding the statistics

  • The instantaneous frequency and bandwidth are computed from the last inter-arrival interval, measured with a monotonic clock (steady_clock), so the measurement is not affected by host clock adjustments (NTP/PTP).
  • The average frequency and bandwidth are computed on the message periods and sizes, i.e. as 1/mean(Δt) and mean(size)/mean(Δt). This is the unbiased windowed rate also used by ros2 topic hz. Averaging the instantaneous rates directly would systematically overestimate the real frequency, increasingly so on jittery topics.
  • In the final report, the mean frequency and bandwidth are computed over the whole test (total messages over total time and total data over total time).
  • The report min/max are tracked on the windowed average rate, and only after the averaging window has been filled. Tracking them on the raw instantaneous rate would report meaningless extremes caused by a single very short inter-arrival interval, e.g. two messages delivered back-to-back by the executor or a publisher burst. If the test is too short for the window to fill, min and max fall back to the overall mean values.
  • The message size is the size of the serialized message and is printed with adaptive units (B / KB / MB / GB), so small messages such as IMU samples are not rounded to 0.00 MB.
  • Bandwidth values are expressed in Mbps computed on binary multiples (1 Mb = 1024 × 1024 bits).
  • The very first received message only initializes the time reference: no frequency can be derived from a single message.

Frequencies measured by the benchmark tool can be lower than the publishing frequencies reported by the node diagnostic. The diagnostic measures the rate at which the node publishes, while the benchmark measures the rate at which a subscriber receives. A difference between the two means that messages are being lost or delayed by the middleware, by the network, or by the receiving machine; see DDS and Network Tuning.

Plotting the statistics

The node publishes the statistics on the topic <name_of_the_benchmarked_topic>_stats, e.g. /zed/zed_node/rgb/color/rect/image_stats, so they can be recorded with ros2 bag or plotted in real time.

The message is a custom type, zed_topic_benchmark_interfaces/msg/BenchmarkStatsStamped, defined as:

# Standard Header
std_msgs/Header header
# Instant Frequency
float32 topic_freq
# Average Frequency
float32 topic_avg_freq
# Instant Bandwidth
float32 topic_bw
# Average Bandwidth
float32 topic_avg_bw

For example, to plot the average frequency and bandwidth with rqt_plot:

$ros2 run rqt_plot rqt_plot \
> /zed/zed_node/rgb/color/rect/image_stats/topic_avg_freq \
> /zed/zed_node/rgb/color/rect/image_stats/topic_avg_bw

QoS

The benchmark subscriber uses a Best Effort, KEEP_LAST QoS with a queue depth of 1. A Best Effort subscriber is compatible with both Reliable and Best Effort publishers, so the tool connects out of the box to the sensor-data topics of the ZED nodes (images, depth, point clouds).

If you need to measure the topic with Reliable reliability, you can override the QoS at runtime:

$ros2 run zed_topic_benchmark zed_topic_benchmark --ros-args \
> -p topic_name:=<topic> \
> -p qos_overrides./<topic>.subscription.reliability:=reliable

A shallow queue (depth 1) is intentional: it makes the tool measure the data that is actually delivered in time, instead of hiding losses behind a large buffer.

Benchmarking IPC and composition

The zed_topic_benchmark_component package provides the ROS 2 component stereolabs::TopicBenchmarkComponent. Loading it in the same container as the ZED node, with Intra Process Communication enabled, lets you measure the performance of the zero-copy data path and compare it with the standard, DDS-based one.

Start the ZED node with IPC enabled:

$ros2 launch zed_wrapper zed_camera.launch.py camera_model:=<camera_model> enable_ipc:=true

then load the benchmark component into the ZED container:

$ros2 component load /zed/zed_container \
> zed_topic_benchmark_component stereolabs::TopicBenchmarkComponent \
> -e use_intra_process_comms:=true \
> -p topic_name:=/zed/zed_node/point_cloud/cloud_registered \
> -p use_ros_log:=true

You can verify that the benchmark node is running in the same process as the ZED node with ros2 component list:

$ros2 component list
$/zed/zed_container
$ 1 /zed/zed_node
$ 2 /topic_benchmark

Set use_ros_log:=true when the component runs inside a container: the in-place console update of the live statistics line is not usable when the output is multiplexed with the logs of the other nodes.

When the benchmark component reaches a test_duration_sec or test_sample_count limit, it requests the shutdown of the process it is running in. In a shared container this stops the whole container, including the ZED node and any other composed node. The reports are still generated before the shutdown. To benchmark composed nodes for an arbitrary time, leave both limits at their default (infinite) and stop the container with Ctrl+C.

Benchmarking a multi-camera setup

The zed_test_ipc.launch.py launch file of the zed_topic_benchmark package starts a multi-camera setup and one benchmark node per camera, either composed in the camera container using IPC or as separate processes:

$ros2 launch zed_topic_benchmark zed_test_ipc.launch.py \
> cam_names:=[zed_front,zed_back] \
> cam_models:=[zed2i,zed2] \
> cam_serials:=[12345678,87654321]

The available launch arguments are:

ArgumentDescriptionDefault
cam_namesArray with the names of the cameras, e.g. [zed_front,zed_back].required
cam_modelsArray with the models of the cameras, e.g. [zed2i,zed2].required
cam_serialsArray with the serial numbers of the cameras, e.g. [35199186,23154724].required
disable_tfDisable TF broadcasting for all the cameras, to fuse the visual odometry information externally.False
use_ipcIf True, the benchmark nodes are loaded in the ZED camera container using Intra Process Communication. If False, each benchmark runs in a separate process.True
topic_nameTopic to benchmark, without the /zed_multi/<cam_name> prefix./point_cloud/cloud_registered
test_duration_secDuration of the test in seconds. 0.0 runs until Ctrl+C.0.0
test_sample_countNumber of messages to acquire before stopping. 0 runs until Ctrl+C.0
log_file_pathFile where the report is saved. The camera name is appended to the file name, e.g. report.txt becomes report_zed_front.txt, so that concurrent benchmark nodes do not overwrite each other. An empty string disables file logging.""

For each camera the launch file benchmarks the topic /zed_multi/<cam_name><topic_name> with a node named benchmark_<index> in the zed_multi namespace. All the benchmark nodes use the ROS logging system (use_ros_log is forced to true); when use_ipc is False the averaging window is also increased to 5000 samples, to obtain very stable average values on long runs.

As explained in the previous section, with use_ipc:=True all the benchmark nodes share the camera container: a finite test_duration_sec/test_sample_count stops the whole container as soon as the first node reaches its limit. Keep the limits at their defaults and stop the launch with Ctrl+C to benchmark a composed multi-camera setup for an arbitrary time.

Testing your ROS 2 configuration

The zed_topic_benchmark package ships a helper script, zed_check_ros2_config.sh, that automates a fixed set of benchmarks against a real ZED camera and prints a report for each one. Its purpose is to verify that your ROS 2, DDS, and system configuration is able to deliver the camera data at the expected rate and bandwidth, a quick way to validate a new setup or to investigate performance problems.

The script benchmarks the following topics, each in both standard mode (the benchmark runs in a separate process, so the data goes through the DDS middleware) and IPC mode (the benchmark is loaded as a component in the same container as the ZED node, with Intra Process Communication):

TopicDepth mode
/zed/zed_node/rgb/color/rect/imageNONE
/zed/zed_node/depth/depth_registeredNEURAL_LIGHT
/zed/zed_node/point_cloud/cloud_registeredNEURAL_LIGHT

The RGB image is measured with depth disabled (NONE) to isolate the image throughput, while depth and point cloud are measured with the default depth mode (NEURAL_LIGHT).

For each of the six tests, the script starts the ZED node with the required enable_ipc and depth.depth_mode configuration, waits for the topic to be advertised, lets the node stabilize, runs the measurement, saves the report, and then stops the node to release the camera.

Usage

$ros2 run zed_topic_benchmark zed_check_ros2_config.sh <camera_model> [duration_sec]

For example, for a ZED 2i with 20 seconds of measurement per test:

$ros2 run zed_topic_benchmark zed_check_ros2_config.sh zed2i 20

The first argument is the camera model (zed, zedm, zed2, zed2i, zedx, zedxm, …) and defaults to zed2i; the second one is the duration in seconds of each measurement and defaults to 15. The behavior can be further tuned with environment variables:

VariableDescriptionDefault
CAMERA_MODELZED camera model. Overridden by the first positional argument.zed2i
DURATIONSeconds of measurement per test. Overridden by the second positional argument.15
WIN_SIZEAveraging window size of the benchmark node.100
TOPIC_TIMEOUTMaximum seconds to wait for a topic to appear before skipping the test.120
SETTLESeconds to wait between two tests, to let the camera be released.5
OUTPUT_DIRDirectory where the reports are stored../zed_config_reports/<timestamp>

The script starts and stops the ZED node itself, so no other node must be using the camera when you run it. Make sure that no ZED node is already running and that the camera is free.

The script must be run in a terminal where the ROS 2 and workspace environments are sourced (it checks that the ros2 command is available). Plan for a few minutes of execution: six tests, each including the start-up of the ZED node, the measurement, and the settle time.

Reading the results

A full report is saved for each test in the output directory, together with the log of the ZED node, useful if a test fails or if a topic never appears. At the end, a summary table is printed:

############################# SUMMARY #############################
TOPIC MODE MSGS FREQ[Hz] BW[Mbps]
-------------------------------------------------------------------
image standard 595 29.78 1953.21
depth standard 448 14.92 977.10
cloud standard 447 14.90 477.30
image ipc 598 29.92 1962.45
depth ipc 450 15.01 982.55
cloud ipc 451 15.04 481.92
-------------------------------------------------------------------

A NO DATA row means that no report was produced for that test: the topic was not published within TOPIC_TIMEOUT, or the ZED node failed to start. Check the corresponding zed_<mode>_<topic>.log file in the output directory.

Compare the measured frequencies with the expected ones, i.e. the values configured through general.pub_frame_rate and depth.point_cloud_freq (see Node Frequency Tuning), and compare the standard rows with the ipc ones to evaluate the gain provided by Composition and IPC.

When the results are not as expected

If the measured frequencies or bandwidths are lower than expected, or if the IPC mode does not improve on the standard mode, your ROS 2 middleware or your system is likely not tuned for high-throughput data. The following pages describe how to fix it:

  • DDS and Network Tuning for ROS 2: the DDS and kernel settings required to reliably transport large messages such as images and point clouds.
  • Using Zenoh as Middleware: an alternative middleware, recommended for distributed and Wi-Fi based setups.
  • Node Frequency Tuning: how to configure the publishing frequency of each module to match the available computational power and bandwidth.
  • Composition and IPC: how to remove the middleware overhead entirely when publisher and subscriber run on the same machine.

Testing the packages

The statistics engine, i.e. the sliding-window averages and the frequency/bandwidth math, is covered by C++ unit tests; the end-to-end behavior, i.e. report generation and self-termination on a sample-count limit, is covered by a launch_testing integration test that runs the node against a synthetic publisher.

Always run colcon from the root of your ROS 2 workspace (the folder containing src/, build/, and install/). Running it from another directory makes colcon scan that directory recursively, which can pull in unrelated setup.py files and abort with package-identification errors.

$cd ~/ros2_ws # your workspace root
$
$# Unit tests (statistics engine)
$colcon build --packages-select zed_topic_benchmark_component --cmake-args -DBUILD_TESTING=ON
$colcon test --packages-select zed_topic_benchmark_component --ctest-args -R "test_winavg|test_statistics"
$
$# Integration test (node + synthetic publisher)
$colcon build --packages-select zed_topic_benchmark --cmake-args -DBUILD_TESTING=ON
$colcon test --packages-select zed_topic_benchmark --ctest-args -R "test_benchmark_integration"
$
$# Show detailed results
$colcon test-result --all --verbose

The integration test can also be run directly with launch_test:

$launch_test src/zed-ros2-examples/tools/benchmark/zed_benchmark/test/test_benchmark_integration.py