ros2_tracing/doc/ros_2.md
2019-05-24 16:09:30 +02:00

7.3 KiB

ROS 2 design notes for instrumentation

The goal is to document ROS 2's design/architecture in order to properly design the instrumentation for it.

Notes on client libraries

ROS 2 has changed the way it deals with client libraries. It offers a base ROS client library (rcl) written in C. This client library is the base for any language-specific implementation, such as rclcpp and rclpy.

However, rcl is obviously fairly basic, and still does leave a fair amount of implementation work up to the client libraries. For example, callbacks are not at all handled in rcl, and are left to the client library implementations.

This means that some instrumentation work might have to be re-done for every client library that we want to trace. We cannot simply instrument rcl, nor can we only instrument the base rmw interface if we want to dig into that.

This document will mainly discuss rcl and rclcpp, but rclpy should eventually be added and supported.

Flow description

Process creation

In the call to rclcpp::init(argc, argv), an rclcpp::Context object is created and CLI arguments are parsed. Much of the work is actually done by rcl through a call to rcl_init().

This has to be done once per process, and usually at the very beginning. The components that are then instanciated share this context.

sequenceDiagram
    participant process
    participant rclcpp
    participant rcl

    process->>rclcpp: rclcpp::init()
    Note over rclcpp: allocates <div></div> rclcpp::Context object
    rclcpp->>rcl: rcl_init()
    Note over rcl: validates & processes context object

Note/component creation

In ROS 2, a process can contain multiple nodes. These are sometimes referred to as "components."

These components are instanciated by the containing process. They are usually classes that extend rclcpp::Node, so that the node initialization work is done by the parent constructor.

This parent constructor will allocate its own rcl_node_t handle and call rcl_node_init(), which will validate the node name/namespace. rcl will also call rmw_create_node() the node's rmw handle (rmw_node_t) to be used later by publishers and subscriptions.

sequenceDiagram
    participant process
    participant component
    participant rclcpp
    participant rcl
    participant rmw

    process->>component: Component()
    component->>rclcpp: : Node()
    Note over rclcpp: allocates rcl_node_t handle
    rclcpp->>rcl: rcl_node_init()
    Note over rcl: checks node name/namespace
    Note over rcl: populates rcl_note_t
    rcl->>rmw: rmw_create_node()
    Note over rmw: creates rmw_node_t handle

Publisher creation

The component calls create_publisher(), a rclcpp::Node method for convenience. That ends up creating an rclcpp::Publisher object which extends rclcpp::PublisherBase. The latter allocates an rcl_publisher_t handle, fetches the corresponding rcl_node_t handle, and calls rcl_publisher_init() in its constructor. rcl does topic name expansion/remapping/validation. It creates an rmw_publisher_t handle by calling rmw_create_publisher() of the given rmw implementation and associates with the node's rmw_node_t handle and the publisher's rcl_publisher_t handle.

If intra-process publishing/subscription is enabled, it will be set up after creating the publisher object, through a call to PublisherBase::setup_intra_process(), which calls rcl_publisher_init().

sequenceDiagram
    participant component
    participant rclcpp
    participant rcl
    participant rmw

    component->>rclcpp: create_publisher()
    Note over rclcpp: through a lot of interfaces
    Note over rclcpp: allocates rcl_publisher_t handle
    rclcpp->>rcl: rcl_publisher_init()
    Note over rcl: populates rcl_publisher_t
    rcl->>rmw: rmw_create_publisher()
    Note over rmw: creates rmw_publisher_t handle

    opt is intra process
        rclcpp->>rcl: rcl_publisher_init()
    end

Subscription creation

Subscription creation is done in a very similar manner.

The componenent calls create_publisher(), which ends up creating an rclcpp::Subscription object which extends rclcpp::SubscriptionBase. The latter allocates an rcl_subscription_t handle, fetches its rcl_node_t handle, and calls rcl_subscription_init() in its constructor. rcl does topic name expansion/remapping/validation. It creates an rmw_subscription_t handle by calling rmw_create_subscription() of the given rmw implementation and associates it with the node's rmw_node_t handle and the subscription's rcl_subscription_t handle.

If intra-process publishing/subscription is enabled, it will be set up after creating the subscription object, through a call to Subscription::setup_intra_process(), which calls rcl_subscription_init().

sequenceDiagram
    participant component
    participant rclcpp
    participant rcl
    participant rmw

    component->>rclcpp: create_subscription()
    Note over rclcpp: allocates rcl_subscription_t handle
    rclcpp->>rcl: rcl_subscription_init()
    Note over rcl: populates rcl_subscription_t
    rcl->>rmw: rmw_create_subscription()
    Note over rmw: creates rmw_publisher_t handle

    opt is intra process
        rclcpp->>rcl: rcl_subscription_init()
    end

Executors and callbacks

An rclcpp::executor::Executor object is created for a given process. It can be a SingleThreadedExecutor or a MultiThreadedExecutor.

Components are instanciated, usually as a shared_ptr through std::make_shared<Component>(), then added to the executor with Executor::add_node().

After all the components have been added, Executor::spin() is called. SingleThreadedExecutor::spin() simply loops forever until the process' context isn't valid anymore. It fetches the next rclcpp::AnyExecutable (e.g. subscription, timer, service, client), and calls Executor::execute_any_executable() with it. This then calls the relevant execute*() method (e.g. execute_timer(), execute_subscription(), execute_intra_process_subscription(), execute_service(), execute_client()).

For subscriptions, callbacks are wrapped by an rclcpp::AnySubscriptionCallback object, which is registered when creating the rclcpp::Subscription object. Subscriptions are handled in the rclcpp layer.

In execute_*subscription(), the Executor allocates a message and calls rcl_take(). If that is successful, it then passes that on to the subscription through rclcpp::SubscriptionBase::handle_message(). Finally, this calls dispatch() on the rclcpp::AnySubscriptionCallback object, which calls the actual std::function with the right signature.

sequenceDiagram
    participant process
    participant executor
    participant subscription
    participant anycallback
    participant rcl

    process->>executor: Executor()
    Note over process: instanciates components
    process->>executor: add_node(component)
    process->>executor: spin()
    loop until shutdown
        Note over executor: get_next_executable()
        Note over executor: execute_any_executable()
        Note over executor: execute_subscription()
        executor->>rcl: rcl_take()
        executor->>subscription: handle_message()
        subscription->>anycallback: dispatch()
        Note over anycallback: std::function::operator(...)
    end