diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..92d356a --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,32 @@ +image: ros:dashing-ros-base + +variables: + DOCKER_DRIVER: overlay2 + +before_script: + - apt update + - rosdep update + - rosdep install -y --from-paths . -i . + - ln -s src + +build: + script: + - colcon build + - colcon test + artifacts: + paths: + - install + - build + reports: + junit: build/**/Test.xml + +build_enabled: + script: + - colcon build --cmake-args -DWITH_LTTNG=ON + - colcon test + artifacts: + paths: + - install + - build + reports: + junit: build/**/Test.xml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/doc/design_ros_2.md b/doc/design_ros_2.md new file mode 100644 index 0000000..d2bfa37 --- /dev/null +++ b/doc/design_ros_2.md @@ -0,0 +1,518 @@ +# ROS 2 tracing + +Design document for ROS 2 tracing, instrumentation, and analysis effort. + +## Introduction + +Tracing allows to record run-time data from a system, both for system data (e.g., when a process +is being scheduled, or when I/O occurs) and for user-defined data. This package helps with +user-defined trace data within the ROS2 framework, e.g. to trace when messages arrive, +when timers fire, when callbacks are being run, etc. + +## Goals and requirements + +### Goals + +1. Provide low-overhead tools and resources for robotics software development based on ROS 2. +2. Make tracing easier to use with ROS. + +### Requirements: instrumentation + +Instrumentation should be built around the main uses of ROS 2, and should include relevant information: + +1. Overall + 1. When creating a publisher/subscriber/service/client/etc., appropriate references should be kept in order to correlate with other tracepoints related to the same instance. +1. Publishers & subscriptions + 1. When creating a publisher/subscription, the effective topic name should be included (i.e. including namespace and after remapping). + 2. When publishing a message, some sort of message identifier should be included in the tracepoint so it can be tracked through DDS up to the subscriber's side. +3. Callbacks (subscription, service, client, timer) + 1. Callback function symbol should be included, whenever possible. + 2. Information about callback execution (e.g. start & end) should be available. +4. Timers + 1. Information about the period should be available. +5. Executors + 1. Information about spin cycles & periods should be available. +6. Others + 1. Provide generic tracepoints for user code. + +### Requirements: analysis & visualization + +Analyses process trace data. They should be general enough to be useful for different use-cases, e.g.: + +* Callback duration +* Time between callbacks (between two callback starts and/or a callback end and a start) +* Message age (as the difference between processing time and message timestamp) +* Message size +* Memory usage +* Execution time/proportion accross a process' nodes/components +* Interruptions (noting that these may be more useful as time-based metrics instead of overall statistics): + * scheduling events during a callback + * delay between the moment a thread becomes ready and when it's actually scheduled + * CPU cycles + +with mean, stdev, etc. when applicable. + +Generic tracepoints for ROS 2 user code could be applied to a user-provided model for higher-level behaviour statistics and visualization. + +### Tools/accessibility + +To make tracing ROS 2 more accessible and easier to adopt, we can put effort into integrating LTTng session setup & recording into the ROS 2 launch system. + +This might include converting existing `tracetools` scripts to more flexible Python scripts, and then plugging that into the launch system. + +## Instrumentation design + +This section includes information about ROS 2's design & architecture through descriptions of the main execution flows. The instrumentation can then be built around that. + +### Flow description + +#### Process creation + +In the call to `rclcpp::init()`, a process-specific `rclcpp::Context` object is fetched and CLI arguments are parsed. Much of the work is actually done by `rcl` through a call to `rcl_init()`. This call processes the `rcl_context_t` handle, which is wrapped by the `Context` object. Also, inside this call, `rcl` calls `rmw_init()` to process the `rmw` context (`rmw_context_t`) as well. This `rmw` handle is itself part of the `rcl_context_t` handle. + +This has to be done once per process, and usually at the very beginning. The components that are then instanciated share this context. + +```mermaid +sequenceDiagram + participant process + participant rclcpp + participant Context + participant rcl + participant rmw + participant tracetools + + Note over rmw: (implementation) + + process->>rclcpp: rclcpp::init(argc, argv) + Note over rclcpp: fetches process-specific Context object + rclcpp->>Context: init(argc, argv) + Note over Context: allocates rcl_context_t handle + Context->>rcl: rcl_init(out rcl_context_t) + Note over rcl: validates & processes rcl_context_t handle + rcl->>rmw: rmw_init(out rmw_context_t) + Note over rmw: validates & processes rmw_context_t handle + + rcl-->>tracetools: TP(rcl_init, rcl_context_t *) +``` + +#### Node/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()` to get the node's `rmw` handle (`rmw_node_t`). This will be used later by publishers and subscriptions. + +```mermaid +sequenceDiagram + participant process + participant Component + participant rclcpp + participant rcl + participant rmw + participant tracetools + + Note over rmw: (implementation) + + process->>Component: Component() + Component->>rclcpp: : Node(node_name, namespace) + Note over rclcpp: allocates rcl_node_t handle + rclcpp->>rcl: rcl_node_init(out rcl_node_t, node_name, namespace) + Note over rcl: validates node name/namespace + Note over rcl: populates rcl_note_t + rcl->>rmw: rmw_create_node(node_name, local_namespace) : rmw_node_t + Note over rmw: creates rmw_node_t handle + + rcl-->>tracetools: TP(rcl_node_init, rcl_node_t *, rmw_node_t *, node_name, namespace) +``` + +#### 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()`. + +```mermaid +sequenceDiagram + participant Component + participant rclcpp + participant Publisher + participant rcl + participant rmw + participant tracetools + + Note over rmw: (implementation) + + Component->>rclcpp: create_publisher(topic_name, options, use_intra_process) + Note over rclcpp: (...) + rclcpp->>Publisher: Publisher(topic_name, options) + Note over Publisher: allocates rcl_publisher_t handle + Publisher->>rcl: rcl_publisher_init(out rcl_publisher_t, rcl_node_t, topic_name, options) + Note over rcl: populates rcl_publisher_t + rcl->>rmw: rmw_create_publisher(rmw_node_t, topic_name, qos_options) : rmw_publisher_t + Note over rmw: creates rmw_publisher_t handle + + rcl-->>tracetools: TP(rcl_publisher_init, rcl_node_t *, rcl_publisher_t *, rmw_publisher_t *, topic_name, depth) + + opt use_intra_process + rclcpp->>Publisher: setup_intra_process() + Publisher->>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()`. This is very similar to a normal (inter-process) subscription, but it sets some flags for later. + +```mermaid +sequenceDiagram + participant Component + participant rclcpp + participant Subscription + participant rcl + participant rmw + participant tracetools + + Note over rmw: (implementation) + + Component->>rclcpp: create_subscription(topic_name, callback, options, use_intra_process) + Note over rclcpp: (...) + rclcpp->>Subscription: Subscription(topic_name, callback, options) + Note over Subscription: allocates rcl_subscription_t handle + Subscription->>rcl: rcl_subscription_init(out rcl_subscription_t, rcl_node_t, topic_name, options) + Note over rcl: populates rcl_subscription_t + rcl->>rmw: rmw_create_subscription(rmw_node_t, topic_name, qos_options) : rmw_subscription_t + Note over rmw: creates rmw_subscription_t handle + + rcl-->>tracetools: TP(rcl_subscription_init, rcl_node_t *, rcl_subscription_t *, rmw_subscription_t *, topic_name, depth) + + opt use_intra_process + rclcpp->>Subscription: setup_intra_process() + Subscription->>rcl: rcl_subscription_init(...) + end + + rclcpp-->>tracetools: TP(rclcpp_subscription_callback_added, rcl_subscription_t *, &any_callback) +``` + +#### Executors + +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()`, 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()`). + +```mermaid +sequenceDiagram + participant process + participant Executor + participant tracetools + + process->>Executor: Executor() + Note over process: instanciates components + process->>Executor: add_node(component) + process->>Executor: spin() + loop until shutdown + Executor-->>tracetools: TP(?) + + Note over Executor: get_next_executable() + Note over Executor: execute_any_executable() + Note over Executor: execute_*() + end +``` + +#### Subscription callbacks + +Subscriptions are handled in the `rclcpp` layer. Callbacks are wrapped by an `rclcpp::AnySubscriptionCallback` object, which is registered when creating the `rclcpp::Subscription` object. + +In `execute_*subscription()`, the `Executor` asks the `Subscription` to allocate a message though `Subscription::create_message()`. It then calls `rcl_take*()`, which calls `rmw_take_with_info()`. If that is successful, the `Executor` then passes that on to the subscription through `rclcpp::SubscriptionBase::handle_message()`. This checks if it's the right type of subscription (i.e. inter vs. intra process), then it calls `dispatch()` on the `rclcpp::AnySubscriptionCallback` object with the message (cast to the actual type). This calls the actual `std::function` with the right signature. + +Finally, it returns the message object through `Subscription::return_message()`. + +```mermaid +sequenceDiagram + participant Executor + participant Subscription + participant AnySubscriptionCallback + participant rcl + participant rmw + participant tracetools + + Note over rmw: (implementation) + + Note over Executor: execute_subscription() + Executor->>Subscription: create_message(): std::shared_ptr + Executor->>rcl: rcl_take*(rcl_subscription_t, out msg) : ret + rcl->>rmw: rmw_take_with_info(rmw_subscription_t, out msg, out taken) + Note over rmw: copies available message to msg if there is one + opt RCL_RET_OK == ret + Executor->>Subscription: handle_message(msg) + Note over Subscription: casts msg to its actual type + Subscription->>AnySubscriptionCallback: dispatch(typed_msg) + AnySubscriptionCallback-->>tracetools: TP(callback_start, this, is_intra_process) + Note over AnySubscriptionCallback: std::function(...) + AnySubscriptionCallback-->>tracetools: TP(callback_end, this) + end + Executor->>Subscription: return_message(msg) +``` + +#### Message publishing + +To publish a message, an object is first allocated and then populated by the `Component` (or equivalent). Then, the message is sent to the `Publisher` through `publish()`. This then passes that on to `rcl`, which itself passes it to `rmw`. + +TODO add inter- vs. intra-process execution flow +TODO talk about IntraProcessManager stuff? + +```mermaid +sequenceDiagram + participant Component + participant Publisher + participant rcl + participant rmw + participant tracetools + + Note over rmw: (implementation) + + Note over Component: creates a msg + Component->>Publisher: publish(msg) + Note over Publisher: ... + Publisher->>rcl: rcl_publish(rcl_publisher_t, msg) + rcl->>rmw: rmw_publish(rmw_publisher_t, msg) + rmw-->>tracetools: TP(?) +``` + +#### Service creation + +Service server creation is similar to subscription creation. The `Component` calls `create_service()` which ends up creating a `rclcpp::Service`. In its constructor, it allocates a `rcl_service_t` handle, then calls `rcl_service_init()`. This processes the handle and validates the service name. It calls `rmw_create_service()` to get the corresponding `rmw_service_t` handle. + +```mermaid +sequenceDiagram + participant Component + participant rclcpp + participant Service + participant rcl + participant rmw + participant tracetools + + Note over rmw: (implementation) + + Component->>rclcpp: create_service(service_name, callback) + Note over rclcpp: (...) + rclcpp->>Service: Service(rcl_node_t, service_name, callback, options) + Note over Service: allocates a rcl_service_t handle + Service->>rcl: rcl_service_init(out rcl_service_t, rcl_node_t, service_name, options) + Note over rcl: validates & processes service handle + rcl->>rmw: rmw_create_service(rmw_node_t, service_name, qos_options) : rmw_service_t + Note over rmw: creates rmw_service_t handle + rcl-->>tracetools: TP(rcl_service_init, rcl_node_t *, rcl_service_t *, rmw_service_t *, service_name) + Service-->>tracetools: TP(rclcpp_service_callback_added, rcl_service_t *, &any_callback) +``` + +#### Service callbacks + +Service callbacks are similar to subscription callbacks. In `execute_service()`, the `Executor` allocates request header and request objects. It then calls `rcl_take_request()` and passes them along with the service handle. + +`rcl` calls `rmw_take_request()`. If those are successful, then the `Executor` calls `handle_request()` on the `Service`. This casts the request to its actual type, allocates a response object, and calls `dispatch()` on its `AnyServiceCallback` object, which calls the actual `std::function` with the right signature. + +For the service response, `Service` calls `rcl_send_response()` which calls `rmw_send_response()`. + +```mermaid +sequenceDiagram + participant Executor + participant Service + participant AnyServiceCallback + participant rcl + participant rmw + participant tracetools + + Note over rmw: (implementation) + + Note over Executor: execute_service() + Note over Executor: allocates request header and request + Executor->>rcl: rcl_take_request(rcl_service, out request_header, out request) : ret + rcl->>rmw: rmw_take_request(rmw_service_t, out request_header, out request, out taken) + opt RCL_RET_OK == ret + Executor->>Service: handle_request(request_header, request) + Note over Service: casts request to its actual type + Note over Service: allocates a response object + Service->>AnyServiceCallback: dispatch(request_header, typed_request, response) + AnyServiceCallback-->>tracetools: TP(callback_start, this) + Note over AnyServiceCallback: std::function(...) + AnyServiceCallback-->>tracetools: TP(callback_end, this) + Service->>rcl: rcl_send_response(rcl_service_t, request_header, response) + rcl->>rmw: rmw_send_response(rmw_service_t, request_header, response) + end +``` + +#### Client creation + +Client creation is similar to publisher creation. The `Component` calls `create_client()` which ends up creating a `rclcpp::Client`. In its constructor, it allocates a `rcl_client_t` handle, then calls `rcl_client_init()`. This validates and processes the handle. It also calls `rmw_create_client()` which creates the `rmw_client_t` handle. + +```mermaid +sequenceDiagram + participant Component + participant Node + participant Client + participant rcl + participant rmw + participant tracetools + + Note over rmw: (implementation) + + Component->>Node: create_client(service_name, options) + Node->>Client: Client(service_name, options) + Note over Client: allocates a rcl_client_t handle + Client->>rcl: rcl_client_init(out rcl_client_t, rcl_node_t, service_name, options) + Note over rcl: validates and processes rcl_client_t handle + rcl->>rmw: rmw_create_client(rmw_node_t, service_name, qos_options) : rmw_client_t + Note over rmw: creates rmw_client_t handle + rcl-->>tracetools: TP(rcl_client_init, rcl_node_t *, rcl_client_t *, rmw_client_t *, service_name) +``` + +#### Client request + +A client request has multiple steps. The `Component` (or the owner of the `Client`) first creates a request object. It then calls `Client::async_send_request()` with the request. It can also provide a callback, but it's optional. The `Client` passes that on to `rcl` by calling `rcl_send_request()`. `rcl` generates a sequence number and assigns it to the request, then calls `rmw_send_request()`. Once this is done, the `Client` puts this sequence number in an internal map along with the created promise and future objects, and the callback (which might simply be empty). + +At this point, the `Client` could simply let its callback be called. It can also use the future object returned by `async_send_request()`, and call `rclcpp::spin_until_future_complete()`. This waits until the future object is ready, or until timeout, and returns. + +If this last call was successful, then the `Component` can get the result and do something with it. + +```mermaid +sequenceDiagram + participant Component + participant Executor + participant Client + participant rclcpp + participant rcl + participant rmw + participant tracetools + + Note over rmw: (implementation) + + Note over Component: creates request + Component->>Client: async_send_request(request[, callback]) : result_future + Client->>rcl: rcl_send_request(rcl_client_t, request, out sequence_number) + Note over rcl: assigns sequence_number + rcl-->>tracetools: TP?(rcl_send_request, rcl_client_t *, sequence_number) + rcl->>rmw: rmw_send_request(rmw_client_t, request, sequence_number) + Note over Client: puts sequence_number in a map with promise+callback+future + + Component->>rclcpp: spin_until_future_complete(result_future) : result_status + + Note over Executor: execute_client() + Note over Executor: creates request_header and response objects + Executor->>rcl: rcl_take_response(rcl_client_t, out request_header, out response) : ret + rcl-->>tracetools: TP?() + rcl->>rmw: rmw_take_response(rmw_client_t, out request_header, out response, out taken) + opt RCL_RET_OK == ret + Executor->>Client: handle_response(request_header, response) + Note over Client: gets sequence_number from request_header + Client-->>tracetools: TP?() + Note over Client: gets promise+callback+future from its map + Note over Client: callback(future) + end + + rclcpp->>Component: ready or timeout + opt SUCCESS == result_status + Note over Component: result_future.get() : result + Note over Component: do something with result + end +``` + +#### Timer creation + +Timer creation is similar to subscription creation. The `Component` calls `create_service()` which ends up creating a `rclcpp::WallTimer`. In its constructor, it creates a `rclcpp::Clock` object, which (for a `WallTimer`) is simply a nanosecond clock. It then allocates a `rcl_timer_t` handle, then calls `rcl_timer_init()`. This processes the handle and validates the period. + +Note that `rcl_timer_init()` can take a callback as a parameter, but right now that feature is not used anywhere (`nullptr` is given), and callbacks are instead handled in the `rclcpp` layer. + +```mermaid +sequenceDiagram + participant Component + participant Node + participant WallTimer + participant rcl + participant tracetools + + Component->>Node: create_wall_timer(period, callback) + Node->>WallTimer: WallTimer(period, callback, Context) + Note over WallTimer: creates a Clock object + Note over WallTimer: allocates a rcl_timer_t handle + WallTimer->>rcl: rcl_timer_init(out rcl_timer_t, Clock, rcl_context_t, period) + Note over rcl: validates and processes rcl_timer_t handle + rcl-->>tracetools: TP(rcl_timer_init, rcl_timer_t *, period) + WallTimer-->>tracetools: TP(rclcpp_timer_callback_added, rcl_timer_t *, &callback) +``` + +#### Timer callbacks + +Timer callbacks are similar to susbcription callbacks. In `execute_timer()`, the `Executor` calls `execute_callback()` on the `WallTimer`. The timer then calls `rcl_timer_call()` with its `rcl_timer_t` handle and checks if the callback should be called. + +If it that is the case, then the timer will call the actual `std::function`. Depending on the `std::function` that was given when creating the timer, it will either call the callback without any parameters or it will pass a reference of itself. + +```mermaid +sequenceDiagram + participant Executor + participant WallTimer + participant rcl + participant tracetools + + Note over Executor: execute_timer() + Executor->>WallTimer: execute_callback() + WallTimer->>rcl: rcl_timer_call(rcl_timer_t) : ret + Note over rcl: validates and updates timer + opt RCL_RET_TIMER_CANCELED != ret && RCL_RET_OK == ret + WallTimer-->>tracetools: TP(callback_start, &callback) + Note over WallTimer: std::function(...) + WallTimer-->>tracetools: TP(callback_end, &callback) + end +``` + +## Design & implementation notes + +### Targeted tools/dependencies + +The targeted tools or dependencies are: + +* LTTng for tracing +* pandas and Jupyter for analysis & visualization + +### Design + +The plan is to use LTTng with a ROS wrapper package like `tracetools` for ROS 1. The suggested setup is: + +* a tracing package (e.g. `tracetools`) wraps calls to LTTng +* ROS 2 is instrumented with calls to the tracing package, therefore it becomes a dependency and ships with the core stack +* by default, the tracing package's functions are empty -- they do not do anything +* if users wants to enable tracing, they need to + * install LTTng + * compile the tracing package from source, setting the right compile flag(s) + * overlay it on top of their ROS 2 installation +* use other package(s) for analysis and visualization + +## Architecture + +![](img/tracing_architecture.png) + +### Timeline + +The first goal is to statically instrument ROS 2, aiming for it to be in the ROS 2 E-turtle release (Nov 2019). + +This includes transposing the existing ROS 1 instrumentation to ROS 2, wherever applicable. This step may not include instrumenting DDS implementations, and thus may be limited to the layer(s) right before `rmw`. + +### Notes on client libraries + +ROS offer a client library (`rcl`) written in C as 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 handled in `rcl`, and are left to the client library implementations. + +This means that some instrumentation work will 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 effort should first focus on `rcl` and `rclcpp` , but `rclpy` should eventually be added and supported. + +### ROS 1/2 compatibility + +We could look into making analyses work on both ROS 1 and ROS 2, through a common instrumentation interface (or other abstraction). diff --git a/doc/img/tracing_architecture.png b/doc/img/tracing_architecture.png new file mode 100644 index 0000000..5396b13 Binary files /dev/null and b/doc/img/tracing_architecture.png differ diff --git a/doc/public_post_plan.md b/doc/public_post_plan.md new file mode 100644 index 0000000..67a0c17 --- /dev/null +++ b/doc/public_post_plan.md @@ -0,0 +1,23 @@ + +Plan for a first public post/announcement. +This week. + +Simple example: +* Jupyter notebook to plot: + * subscription callback duration + * CPU cycles/callback +* and resolve/demangle symbols +* Include all files/scripts needed to get the result, but keep it simple +* To be hosted somewhere +* With some documentation/guide on how to try it out + +Post: +* Mention that we're working on this +* Talk about goals in general +* Target = next release +* Show off example +* Invite people to ask questions/discuss + +Architecture considerations (right now but also later): +* Try to use a dataframe as a ROS model abstraction, instead of first going through a layer with python data structures +* \ No newline at end of file diff --git a/tracetools/CMakeLists.txt b/tracetools/CMakeLists.txt new file mode 100644 index 0000000..a6327a5 --- /dev/null +++ b/tracetools/CMakeLists.txt @@ -0,0 +1,96 @@ +cmake_minimum_required(VERSION 3.5) +project(tracetools) + +# Default to C++14 +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 14) +endif() + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic -fPIC) +endif() + +find_package(ament_cmake REQUIRED) + +option(WITH_LTTNG "Include support for tracing with LTTng" OFF) +if(WITH_LTTNG) + # Try to find LTTng + find_package(PkgConfig REQUIRED) + pkg_check_modules(LTTNG REQUIRED lttng-ust) +endif() +if(LTTNG_FOUND) + set(LTTNG_TP_FILES + include/tracetools/tp_call.h + src/tp_call.c + ) + set(TRACING_ENABLED TRUE) + message("LTTng found: tracing enabled") +elseif(WITH_LTTNG) + message("LTTng NOT found: tracing disabled") +endif() + +include_directories(include) + +# Status checking tool +add_executable(status + src/status.c + src/tracetools.c + src/utils.cpp +) +target_link_libraries(status + ${PROJECT_NAME} +) +ament_target_dependencies(status + ${PROJECT_NAME} +) +install(TARGETS + status + DESTINATION lib/${PROJECT_NAME} +) + +# Tracetools lib +set(SOURCES + src/tracetools.c + src/utils.cpp +) +if(TRACING_ENABLED) + list(APPEND SOURCES ${LTTNG_TP_FILES}) +endif() + +add_library(${PROJECT_NAME} ${SOURCES}) +if(TRACING_ENABLED) + target_compile_definitions(${PROJECT_NAME} PUBLIC TRACETOOLS_LTTNG_ENABLED) + target_link_libraries(${PROJECT_NAME} ${LTTNG_LIBRARIES} -ldl) +else() + target_link_libraries(${PROJECT_NAME}) +endif() + +ament_export_interfaces(${PROJECT_NAME}_export HAS_LIBRARY_TARGET) +install( + DIRECTORY include/ + DESTINATION include +) +install( + TARGETS ${PROJECT_NAME} + EXPORT ${PROJECT_NAME}_export + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib + RUNTIME DESTINATION bin + INCLUDES DESTINATION include +) + +ament_export_include_directories(include) +if(TRACING_ENABLED) + ament_export_libraries(${PROJECT_NAME} ${LTTNG_LIBRARIES}) +else() + ament_export_libraries(${PROJECT_NAME}) +endif() + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + ament_lint_auto_find_test_dependencies() +endif() + +ament_package() + +add_definitions("-D${PROJECT_NAME}_VERSION=\"${${PROJECT_NAME}_VERSION}\"") diff --git a/tracetools/README.md b/tracetools/README.md new file mode 100644 index 0000000..5d7516e --- /dev/null +++ b/tracetools/README.md @@ -0,0 +1,26 @@ +# tracetools + +## Building + +If tracing is not enabled when building, or if LTTng is not found, then this package will not do anything. + +To enable tracing: + +1. Install [LTTng](https://lttng.org/docs/v2.10/#doc-ubuntu): + ``` + $ sudo apt-get install lttng-tools lttng-modules-dkms liblttng-ust-dev + ``` +2. Build with the `WITH_LTTNG` flag: + ``` + $ colcon build --cmake-args " -DWITH_LTTNG=ON" + ``` +3. Check if tracing is enabled (after sourcing): + ``` + $ ros2 run tracetools tracetools_status + ``` + +## Tracing + +By default, the steps above will not lead to trace data being generated, and thus they will have no impact on execution. + +LTTng has to be enabled: TODO mention scripts diff --git a/tracetools/include/tracetools/tp_call.h b/tracetools/include/tracetools/tp_call.h new file mode 100644 index 0000000..a1aaa13 --- /dev/null +++ b/tracetools/include/tracetools/tp_call.h @@ -0,0 +1,228 @@ +// Copyright 2019 Robert Bosch GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Provide fake header guard for cpplint +#undef TRACETOOLS__TP_CALL_H_ +#ifndef TRACETOOLS__TP_CALL_H_ +#define TRACETOOLS__TP_CALL_H_ + +#undef TRACEPOINT_PROVIDER +#define TRACEPOINT_PROVIDER ros2 + +#undef TRACEPOINT_INCLUDE +#define TRACEPOINT_INCLUDE "tracetools/tp_call.h" + +#if !defined(_TRACETOOLS__TP_CALL_H_) || defined(TRACEPOINT_HEADER_MULTI_READ) +#define _TRACETOOLS__TP_CALL_H_ + +#include + +#include + +TRACEPOINT_EVENT( + TRACEPOINT_PROVIDER, + rcl_init, + TP_ARGS( + const void *, context_handle_arg, + const char *, version_arg + ), + TP_FIELDS( + ctf_integer_hex(const void *, context_handle, context_handle_arg) + ctf_string(version, version_arg) + ) +) + +TRACEPOINT_EVENT( + TRACEPOINT_PROVIDER, + rcl_node_init, + TP_ARGS( + const void *, node_handle_arg, + const void *, rmw_handle_arg, + const char *, node_name_arg, + const char *, namespace_arg + ), + TP_FIELDS( + ctf_integer_hex(const void *, node_handle, node_handle_arg) + ctf_integer_hex(const void *, rmw_handle, rmw_handle_arg) + ctf_string(node_name, node_name_arg) + ctf_string(namespace, namespace_arg) + ) +) + +TRACEPOINT_EVENT( + TRACEPOINT_PROVIDER, + rcl_publisher_init, + TP_ARGS( + const void *, publisher_handle_arg, + const void *, node_handle_arg, + const void *, rmw_publisher_handle_arg, + const char *, topic_name_arg, + const size_t, queue_depth_arg + ), + TP_FIELDS( + ctf_integer_hex(const void *, publisher_handle, publisher_handle_arg) + ctf_integer_hex(const void *, node_handle, node_handle_arg) + ctf_integer_hex(const void *, rmw_publisher_handle, rmw_publisher_handle_arg) + ctf_string(topic_name, topic_name_arg) + ctf_integer(const size_t, queue_depth, queue_depth_arg) + ) +) + +TRACEPOINT_EVENT( + TRACEPOINT_PROVIDER, + rcl_subscription_init, + TP_ARGS( + const void *, subscription_handle_arg, + const void *, node_handle_arg, + const void *, rmw_subscription_handle_arg, + const char *, topic_name_arg, + const size_t, queue_depth_arg + ), + TP_FIELDS( + ctf_integer_hex(const void *, subscription_handle, subscription_handle_arg) + ctf_integer_hex(const void *, node_handle, node_handle_arg) + ctf_integer_hex(const void *, rmw_subscription_handle, rmw_subscription_handle_arg) + ctf_string(topic_name, topic_name_arg) + ctf_integer(const size_t, queue_depth, queue_depth_arg) + ) +) + +TRACEPOINT_EVENT( + TRACEPOINT_PROVIDER, + rclcpp_subscription_callback_added, + TP_ARGS( + const void *, subscription_handle_arg, + const void *, callback_arg + ), + TP_FIELDS( + ctf_integer_hex(const void *, subscription_handle, subscription_handle_arg) + ctf_integer_hex(const void *, callback, callback_arg) + ) +) + +TRACEPOINT_EVENT( + TRACEPOINT_PROVIDER, + rcl_service_init, + TP_ARGS( + const void *, service_handle_arg, + const void *, node_handle_arg, + const void *, rmw_service_handle_arg, + const char *, service_name_arg + ), + TP_FIELDS( + ctf_integer_hex(const void *, service_handle, service_handle_arg) + ctf_integer_hex(const void *, node_handle, node_handle_arg) + ctf_integer_hex(const void *, rmw_service_handle, rmw_service_handle_arg) + ctf_string(service_name, service_name_arg) + ) +) + +TRACEPOINT_EVENT( + TRACEPOINT_PROVIDER, + rclcpp_service_callback_added, + TP_ARGS( + const void *, service_handle_arg, + const void *, callback_arg + ), + TP_FIELDS( + ctf_integer_hex(const void *, service_handle, service_handle_arg) + ctf_integer_hex(const void *, callback, callback_arg) + ) +) + +TRACEPOINT_EVENT( + TRACEPOINT_PROVIDER, + rcl_client_init, + TP_ARGS( + const void *, client_handle_arg, + const void *, node_handle_arg, + const void *, rmw_client_handle_arg, + const char *, service_name_arg + ), + TP_FIELDS( + ctf_integer_hex(const void *, client_handle, client_handle_arg) + ctf_integer_hex(const void *, node_handle, node_handle_arg) + ctf_integer_hex(const void *, rmw_client_handle, rmw_client_handle_arg) + ctf_string(service_name, service_name_arg) + ) +) + +TRACEPOINT_EVENT( + TRACEPOINT_PROVIDER, + rcl_timer_init, + TP_ARGS( + const void *, timer_handle_arg, + int64_t, period_arg + ), + TP_FIELDS( + ctf_integer_hex(const void *, timer_handle, timer_handle_arg) + ctf_integer(int64_t, period, period_arg) + ) +) + +TRACEPOINT_EVENT( + TRACEPOINT_PROVIDER, + rclcpp_timer_callback_added, + TP_ARGS( + const void *, timer_handle_arg, + const void *, callback_arg + ), + TP_FIELDS( + ctf_integer_hex(const void *, timer_handle, timer_handle_arg) + ctf_integer_hex(const void *, callback, callback_arg) + ) +) + +TRACEPOINT_EVENT( + TRACEPOINT_PROVIDER, + rclcpp_callback_register, + TP_ARGS( + const void *, callback_arg, + const char *, symbol_arg + ), + TP_FIELDS( + ctf_integer_hex(const void *, callback, callback_arg) + ctf_string(symbol, symbol_arg) + ) +) + +TRACEPOINT_EVENT( + TRACEPOINT_PROVIDER, + callback_start, + TP_ARGS( + const void *, callback_arg, + int, is_intra_process_arg + ), + TP_FIELDS( + ctf_integer_hex(const void *, callback, callback_arg) + ctf_integer(int, is_intra_process, is_intra_process_arg) + ) +) + +TRACEPOINT_EVENT( + TRACEPOINT_PROVIDER, + callback_end, + TP_ARGS( + const void *, callback_arg + ), + TP_FIELDS( + ctf_integer_hex(const void *, callback, callback_arg) + ) +) + +#endif // _TRACETOOLS__TP_CALL_H_ + +#include + +#endif // TRACETOOLS__TP_CALL_H_ diff --git a/tracetools/include/tracetools/tracetools.h b/tracetools/include/tracetools/tracetools.h new file mode 100644 index 0000000..2bf082d --- /dev/null +++ b/tracetools/include/tracetools/tracetools.h @@ -0,0 +1,153 @@ +// Copyright 2019 Robert Bosch GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef TRACETOOLS__TRACETOOLS_H_ +#define TRACETOOLS__TRACETOOLS_H_ + +#include +#include +#include + +#define TRACEPOINT(event_name, ...) \ + (ros_trace_ ## event_name)(__VA_ARGS__) + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** + * Report whether tracing is compiled in + */ +bool ros_trace_compile_status(); + +/** + * tp: rcl_init + */ +void TRACEPOINT( + rcl_init, + const void * context_handle); + +/** + * tp: rcl_node_init + */ +void TRACEPOINT( + rcl_node_init, + const void * node_handle, + const void * rmw_handle, + const char * node_name, + const char * node_namespace); + +/** + * tp: rcl_publisher_init + */ +void TRACEPOINT( + rcl_publisher_init, + const void * publisher_handle, + const void * node_handle, + const void * rmw_publisher_handle, + const char * topic_name, + const size_t queue_depth); + +/** + * tp: rcl_subscription_init + */ +void TRACEPOINT( + rcl_subscription_init, + const void * subscription_handle, + const void * node_handle, + const void * rmw_subscription_handle, + const char * topic_name, + const size_t queue_depth); + +/** + * tp: rclcpp_subscription_callback_added + */ +void TRACEPOINT( + rclcpp_subscription_callback_added, + const void * subscription_handle, + const void * callback); + +/** + * tp: rcl_service_init + */ +void TRACEPOINT( + rcl_service_init, + const void * service_handle, + const void * node_handle, + const void * rmw_service_handle, + const char * service_name); + +/** + * tp: rclcpp_service_callback_added + */ +void TRACEPOINT( + rclcpp_service_callback_added, + const void * service_handle, + const void * callback); + +/** + * tp: rcl_client_init + */ +void TRACEPOINT( + rcl_client_init, + const void * client_handle, + const void * node_handle, + const void * rmw_client_handle, + const char * service_name); + +/** + * tp: rcl_timer_init + */ +void TRACEPOINT( + rcl_timer_init, + const void * timer_handle, + int64_t period); + +/** + * tp: rclcpp_timer_callback_added + */ +void TRACEPOINT( + rclcpp_timer_callback_added, + const void * timer_handle, + const void * callback); + +/** + * tp: rclcpp_callback_register + */ +void TRACEPOINT( + rclcpp_callback_register, + const void * callback, + const char * function_symbol); + +/** + * tp: callback_start + */ +void TRACEPOINT( + callback_start, + const void * callback, + const bool is_intra_process); + +/** + * tp: callback_end + */ +void TRACEPOINT( + callback_end, + const void * callback); + +#ifdef __cplusplus +} +#endif + +#endif // TRACETOOLS__TRACETOOLS_H_ diff --git a/tracetools/include/tracetools/utils.hpp b/tracetools/include/tracetools/utils.hpp new file mode 100644 index 0000000..e73030b --- /dev/null +++ b/tracetools/include/tracetools/utils.hpp @@ -0,0 +1,35 @@ +// Copyright 2019 Robert Bosch GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef TRACETOOLS__UTILS_HPP_ +#define TRACETOOLS__UTILS_HPP_ + +#include +#include + +template +void * get_address(std::function f) +{ + typedef T (fnType)(U...); + fnType ** fnPointer = f.template target(); + // Might be a lambda + if (fnPointer == nullptr) { + return 0; + } + return reinterpret_cast(*fnPointer); +} + +const char * get_symbol(void * funptr); + +#endif // TRACETOOLS__UTILS_HPP_ diff --git a/tracetools/package.xml b/tracetools/package.xml new file mode 100644 index 0000000..6bb4256 --- /dev/null +++ b/tracetools/package.xml @@ -0,0 +1,24 @@ + + + + tracetools + 0.0.1 + ROS 2 wrapper for instrumentation + Christophe Bedard + Ingo Luetkebohle + Apache Software License 2.0 + Ingo Luetkebohle + Christophe Bedard + + ament_cmake + pkg-config + + liblttng-ust-dev + + ament_lint_auto + ament_lint_common + + + ament_cmake + + diff --git a/tracetools/scripts/setup-lttng.sh b/tracetools/scripts/setup-lttng.sh new file mode 100755 index 0000000..84dbaf2 --- /dev/null +++ b/tracetools/scripts/setup-lttng.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Copyright 2019 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# set up ust ros2 events +for event in \ + ros2:rcl_init \ + ros2:rcl_node_init \ + ros2:rcl_publisher_init \ + ros2:rcl_subscription_init \ + ros2:rclcpp_subscription_callback_added \ + ros2:rcl_service_init \ + ros2:rclcpp_service_callback_added \ + ros2:rcl_client_init \ + ros2:rcl_timer_init \ + ros2:rclcpp_timer_callback_added \ + ros2:rclcpp_register_callback \ + ros2:callback_start \ + ros2:callback_end +do + lttng enable-event -c ros2 -u $event +done + +# process context +lttng add-context -c ros2 -u \ + -t vpid -t procname \ + -t vtid -t perf:thread:instructions \ + -t perf:thread:cycles -t perf:thread:cpu-cycles + +#kernel +lttng enable-channel --kernel kchan --subbuf-size=8M + +# # network +# for event in net_dev_queue netif_receive_skb net_if_receive_skb +# do +# lttng enable-event --kernel --channel=kchan $event +# done + +# other kernel stuff +for event in sched_switch sched_waking sched_pi_setprio sched_process_fork sched_process_exit sched_process_free sched_wakeup\ + irq_softirq_entry irq_softirq_raise irq_softirq_exit irq_handler_entry irq_handler_exit\ + lttng_statedump_process_state lttng_statedump_start lttng_statedump_end lttng_statedump_network_interface lttng_statedump_block_device\ + block_rq_complete block_rq_insert block_rq_issue\ + block_bio_frontmerge sched_migrate sched_migrate_task power_cpu_frequency\ + net_dev_queue netif_receive_skb net_if_receive_skb\ + timer_hrtimer_start timer_hrtimer_cancel timer_hrtimer_expire_entry timer_hrtimer_expire_exit +do + lttng enable-event --kernel --channel=kchan $event +done + +# lttng enable-event -k --syscall --all diff --git a/tracetools/scripts/trace.sh b/tracetools/scripts/trace.sh new file mode 100755 index 0000000..e29a01c --- /dev/null +++ b/tracetools/scripts/trace.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# Copyright 2019 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## Helper script for ROS tracing +## +## Call this by providing these arguments: +## 1. a session name [optional; 'ros2' will be used] +## 2. a wait time before killing and stopping (in seconds) +## 3. a roslaunch/rosrun command +## ex: ./trace.sh 3 roslaunch tracecompass_ros_testcases pub_sub.launch + +source ./${BASH_SOURCE%/*}/../../../../install/local_setup.bash + +## Parameters + +# if no parameters were given, exit with error +if [ -z "$1" ] ; then + echo "Error: no parameters were given!" + exit 1 +elif [ "$1" == "-h" ] ; then + echo -e "ROS tracing helper script.\n" \ + "Provide 3 arguments:\n" \ + "1. the lttng session name [optional; 'ros2' will be used]\n" \ + "2. the wait time before killing and stopping (in seconds)\n" \ + "3. the ros2 run command\n" \ + "Example: ./trace.sh ros-trace 3 ros2 run tracetools tracetools_status" + exit 0 +fi + +# session name +session_name="$1" +case "$session_name" in + ''|*[!0-9]*) # not a number: good + shift + ;; + *) # number: so use a default session name + session_name="ros2" + ;; +esac + +# wait time (seconds) before killing and stopping +sleep_time="$1" +case "$sleep_time" in + ''|*[!0-9]*) # not a number: error! + echo "Error: not a valid sleep time!" + exit 1 + ;; + *) # number: good + shift + ;; +esac + +# command from remaining arguments +if [ -z "$1" ] ; then + echo "Error: no command was given!" + exit 1 +fi +launch_cmd="$@" +launch_cmd+=" &" + +## Trace + +# create lttng session (and set output to traces/ directory) +lttng create $session_name --output=./${BASH_SOURCE%/*}/../traces/$session_name/ + +# enable events +./${BASH_SOURCE%/*}/setup-lttng.sh + +# start +lttng start + +# preload UST library +export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/liblttng-ust-cyg-profile.so + +# launch +eval "$launch_cmd" + +# wait a bit and kill +echo "waiting $sleep_time..." +sleep $sleep_time +echo "killing" +kill $! + +# wait again for everything to shutdown +echo "waiting for shutdown..." +sleep 2 +echo "stopping" + +# stop & destroy +lttng stop +lttng destroy diff --git a/tracetools/src/status.c b/tracetools/src/status.c new file mode 100644 index 0000000..006ad72 --- /dev/null +++ b/tracetools/src/status.c @@ -0,0 +1,28 @@ +// Copyright 2019 Robert Bosch GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include "tracetools/tracetools.h" + +int main() +{ + printf("Tracing "); + if (ros_trace_compile_status()) { + printf("enabled\n"); + return 0; + } else { + printf("disabled\n"); + return 1; + } +} diff --git a/tracetools/src/tp_call.c b/tracetools/src/tp_call.c new file mode 100644 index 0000000..6a24fba --- /dev/null +++ b/tracetools/src/tp_call.c @@ -0,0 +1,18 @@ +// Copyright 2019 Robert Bosch GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#define TRACEPOINT_CREATE_PROBES + +#define TRACEPOINT_DEFINE +#include "tracetools/tp_call.h" diff --git a/tracetools/src/tracetools.c b/tracetools/src/tracetools.c new file mode 100644 index 0000000..fc2b996 --- /dev/null +++ b/tracetools/src/tracetools.c @@ -0,0 +1,218 @@ +// Copyright 2019 Robert Bosch GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "tracetools/tracetools.h" + +#if defined(TRACETOOLS_LTTNG_ENABLED) && !defined(_WIN32) +# include "tracetools/tp_call.h" +# define CONDITIONAL_TP(...) \ + tracepoint(__VA_ARGS__) +#else +# define CONDITIONAL_TP(...) +#endif + +bool ros_trace_compile_status() +{ +#if defined(TRACETOOLS_LTTNG_ENABLED) && !defined(_WIN32) + return true; +#else + return false; +#endif +} + +#ifndef _WIN32 +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunused-parameter" +#endif + +void TRACEPOINT( + rcl_init, + const void * context_handle) +{ + CONDITIONAL_TP( + ros2, + rcl_init, + context_handle, + tracetools_VERSION); +} + +void TRACEPOINT( + rcl_node_init, + const void * node_handle, + const void * rmw_handle, + const char * node_name, + const char * node_namespace) +{ + CONDITIONAL_TP( + ros2, + rcl_node_init, + node_handle, + rmw_handle, + node_name, + node_namespace); +} + +void TRACEPOINT( + rcl_publisher_init, + const void * publisher_handle, + const void * node_handle, + const void * rmw_publisher_handle, + const char * topic_name, + const size_t queue_depth) +{ + CONDITIONAL_TP( + ros2, + rcl_publisher_init, + publisher_handle, + node_handle, + rmw_publisher_handle, + topic_name, + queue_depth); +} + +void TRACEPOINT( + rcl_subscription_init, + const void * subscription_handle, + const void * node_handle, + const void * rmw_subscription_handle, + const char * topic_name, + const size_t queue_depth) +{ + CONDITIONAL_TP( + ros2, + rcl_subscription_init, + subscription_handle, + node_handle, + rmw_subscription_handle, + topic_name, + queue_depth); +} + +void TRACEPOINT( + rclcpp_subscription_callback_added, + const void * subscription_handle, + const void * callback) +{ + CONDITIONAL_TP( + ros2, + rclcpp_subscription_callback_added, + subscription_handle, + callback); +} + +void TRACEPOINT( + rcl_service_init, + const void * service_handle, + const void * node_handle, + const void * rmw_service_handle, + const char * service_name) +{ + CONDITIONAL_TP( + ros2, + rcl_service_init, + service_handle, + node_handle, + rmw_service_handle, + service_name); +} + +void TRACEPOINT( + rclcpp_service_callback_added, + const void * service_handle, + const void * callback) +{ + CONDITIONAL_TP( + ros2, + rclcpp_service_callback_added, + service_handle, + callback); +} + +void TRACEPOINT( + rcl_client_init, + const void * client_handle, + const void * node_handle, + const void * rmw_client_handle, + const char * service_name) +{ + CONDITIONAL_TP( + ros2, + rcl_client_init, + client_handle, + node_handle, + rmw_client_handle, + service_name); +} + +void TRACEPOINT( + rcl_timer_init, + const void * timer_handle, + int64_t period) +{ + CONDITIONAL_TP( + ros2, + rcl_timer_init, + timer_handle, + period); +} + +void TRACEPOINT( + rclcpp_timer_callback_added, + const void * timer_handle, + const void * callback) +{ + CONDITIONAL_TP( + ros2, + rclcpp_timer_callback_added, + timer_handle, + callback); +} + +void TRACEPOINT( + rclcpp_callback_register, + const void * callback, + const char * function_symbol) +{ + CONDITIONAL_TP( + ros2, + rclcpp_callback_register, + callback, + function_symbol); +} + +void TRACEPOINT( + callback_start, + const void * callback, + const bool is_intra_process) +{ + CONDITIONAL_TP( + ros2, + callback_start, + callback, + (is_intra_process ? 1 : 0)); +} + +void TRACEPOINT( + callback_end, + const void * callback) +{ + CONDITIONAL_TP( + ros2, + callback_end, + callback); +} + +#ifndef _WIN32 +# pragma GCC diagnostic pop +#endif diff --git a/tracetools/src/utils.cpp b/tracetools/src/utils.cpp new file mode 100644 index 0000000..acb49cb --- /dev/null +++ b/tracetools/src/utils.cpp @@ -0,0 +1,45 @@ +// Copyright 2019 Robert Bosch GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if defined(TRACETOOLS_LTTNG_ENABLED) && !defined(_WIN32) +#include +#include +#endif +#include "tracetools/utils.hpp" + +const char * get_symbol(void * funptr) +{ +#define SYMBOL_UNKNOWN "UNKNOWN" +#if defined(TRACETOOLS_LTTNG_ENABLED) && !defined(_WIN32) +#define SYMBOL_LAMBDA "[lambda]" + if (funptr == 0) { + return SYMBOL_LAMBDA; + } + + Dl_info info; + if (dladdr(funptr, &info) == 0) { + return SYMBOL_UNKNOWN; + } + + char * demangled = nullptr; + int status; + demangled = abi::__cxa_demangle(info.dli_sname, NULL, 0, &status); + // Use demangled symbol if possible + const char * demangled_val = (status == 0 ? demangled : info.dli_sname); + return demangled_val != 0 ? demangled_val : SYMBOL_UNKNOWN; +#else + (void)funptr; + return SYMBOL_UNKNOWN; +#endif +} diff --git a/tracetools_test/.gitignore b/tracetools_test/.gitignore new file mode 100644 index 0000000..2f836aa --- /dev/null +++ b/tracetools_test/.gitignore @@ -0,0 +1,2 @@ +*~ +*.pyc diff --git a/tracetools_test/CMakeLists.txt b/tracetools_test/CMakeLists.txt new file mode 100644 index 0000000..a7d339e --- /dev/null +++ b/tracetools_test/CMakeLists.txt @@ -0,0 +1,117 @@ +cmake_minimum_required(VERSION 3.5) +project(tracetools_test) + +# Default to C++14 +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 14) +endif() + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic -fPIC) +endif() + +find_package(ament_cmake REQUIRED) + +# Tests +if(BUILD_TESTING) + find_package(rclcpp REQUIRED) + find_package(std_msgs REQUIRED) + find_package(std_srvs REQUIRED) + find_package(tracetools REQUIRED) + + add_executable(test_publisher + src/test_publisher.cpp + ) + ament_target_dependencies(test_publisher + rclcpp + std_msgs + ) + add_executable(test_subscription + src/test_subscription.cpp + ) + ament_target_dependencies(test_subscription + rclcpp + std_msgs + ) + add_executable(test_ping + src/test_ping.cpp + ) + ament_target_dependencies(test_ping + rclcpp + std_msgs + ) + add_executable(test_pong + src/test_pong.cpp + ) + ament_target_dependencies(test_pong + rclcpp + std_msgs + ) + add_executable(test_timer + src/test_timer.cpp + ) + ament_target_dependencies(test_timer + rclcpp + ) + add_executable(test_service + src/test_service.cpp + ) + ament_target_dependencies(test_service + rclcpp + std_srvs + ) + add_executable(test_service_ping + src/test_service_ping.cpp + ) + ament_target_dependencies(test_service_ping + rclcpp + std_srvs + ) + add_executable(test_service_pong + src/test_service_pong.cpp + ) + ament_target_dependencies(test_service_pong + rclcpp + std_srvs + ) + + install(TARGETS + test_publisher + test_subscription + test_ping + test_pong + test_timer + test_service + test_service_ping + test_service_pong + DESTINATION lib/${PROJECT_NAME} + ) + + find_package(ament_lint_auto REQUIRED) + ament_lint_auto_find_test_dependencies() + + find_package(ament_cmake_pytest REQUIRED) + + # Run each test in its own pytest invocation + set(_tracetools_test_pytest_tests + test/test_node.py + test/test_publisher.py + test/test_subscription.py + test/test_subscription_callback.py + test/test_timer.py + test/test_service.py + test/test_service_callback.py + ) + + foreach(_test_path ${_tracetools_test_pytest_tests}) + get_filename_component(_test_name ${_test_path} NAME_WE) + ament_add_pytest_test(${_test_name} ${_test_path} + PYTHON_EXECUTABLE "${PYTHON_EXECUTABLE}" + APPEND_ENV AMENT_PREFIX_PATH=${ament_index_build_path} + PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR} + TIMEOUT 60 + ) + endforeach() +endif() + +ament_package() diff --git a/tracetools_test/package.xml b/tracetools_test/package.xml new file mode 100644 index 0000000..2a62e8d --- /dev/null +++ b/tracetools_test/package.xml @@ -0,0 +1,33 @@ + + + + tracetools_test + 0.0.1 + Separate test package for tracetools + Christophe Bedard + Ingo Luetkebohle + Apache Software License 2.0 + Christophe Bedard + + ament_cmake + pkg-config + + rclcpp + std_msgs + std_srvs + + rclcpp + std_msgs + std_srvs + + ament_cmake_pytest + ament_lint_auto + ament_lint_common + launch_ros + python3-pytest + tracetools + + + ament_cmake + + diff --git a/tracetools_test/src/test_ping.cpp b/tracetools_test/src/test_ping.cpp new file mode 100644 index 0000000..237e074 --- /dev/null +++ b/tracetools_test/src/test_ping.cpp @@ -0,0 +1,74 @@ +// Copyright 2019 Robert Bosch GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include "rclcpp/rclcpp.hpp" +#include "std_msgs/msg/string.hpp" + +using namespace std::chrono_literals; + +class PingNode : public rclcpp::Node +{ +public: + explicit PingNode(rclcpp::NodeOptions options) + : Node("ping_node", options) + { + sub_ = this->create_subscription( + "pong", + rclcpp::QoS(10), + std::bind(&PingNode::callback, this, std::placeholders::_1)); + pub_ = this->create_publisher( + "ping", + rclcpp::QoS(10)); + timer_ = this->create_wall_timer( + 500ms, + std::bind(&PingNode::timer_callback, this)); + } + +private: + void callback(const std_msgs::msg::String::SharedPtr msg) + { + RCLCPP_INFO(this->get_logger(), "[output] %s", msg->data.c_str()); + rclcpp::shutdown(); + } + + void timer_callback() + { + auto msg = std::make_shared(); + msg->data = "some random ping string"; + pub_->publish(*msg); + } + + rclcpp::Subscription::SharedPtr sub_; + rclcpp::Publisher::SharedPtr pub_; + rclcpp::TimerBase::SharedPtr timer_; +}; + +int main(int argc, char * argv[]) +{ + rclcpp::init(argc, argv); + + rclcpp::executors::SingleThreadedExecutor exec; + auto ping_node = std::make_shared(rclcpp::NodeOptions()); + exec.add_node(ping_node); + + printf("spinning\n"); + exec.spin(); + + // Will actually be called inside the node's callback + rclcpp::shutdown(); + return 0; +} diff --git a/tracetools_test/src/test_pong.cpp b/tracetools_test/src/test_pong.cpp new file mode 100644 index 0000000..6d640b3 --- /dev/null +++ b/tracetools_test/src/test_pong.cpp @@ -0,0 +1,63 @@ +// Copyright 2019 Robert Bosch GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "rclcpp/rclcpp.hpp" +#include "std_msgs/msg/string.hpp" + +class PongNode : public rclcpp::Node +{ +public: + explicit PongNode(rclcpp::NodeOptions options) + : Node("pong_node", options) + { + sub_ = this->create_subscription( + "ping", + rclcpp::QoS(10), + std::bind(&PongNode::callback, this, std::placeholders::_1)); + pub_ = this->create_publisher( + "pong", + rclcpp::QoS(10)); + } + +private: + void callback(const std_msgs::msg::String::SharedPtr msg) + { + RCLCPP_INFO(this->get_logger(), "[output] %s", msg->data.c_str()); + auto next_msg = std::make_shared(); + next_msg->data = "some random pong string"; + pub_->publish(*next_msg); + rclcpp::shutdown(); + } + + rclcpp::Subscription::SharedPtr sub_; + rclcpp::Publisher::SharedPtr pub_; +}; + +int main(int argc, char * argv[]) +{ + rclcpp::init(argc, argv); + + rclcpp::executors::SingleThreadedExecutor exec; + auto pong_node = std::make_shared(rclcpp::NodeOptions()); + exec.add_node(pong_node); + + printf("spinning\n"); + exec.spin(); + + // Will actually be called inside the node's callback + rclcpp::shutdown(); + return 0; +} diff --git a/tracetools_test/src/test_publisher.cpp b/tracetools_test/src/test_publisher.cpp new file mode 100644 index 0000000..cb0068d --- /dev/null +++ b/tracetools_test/src/test_publisher.cpp @@ -0,0 +1,48 @@ +// Copyright 2019 Robert Bosch GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "rclcpp/rclcpp.hpp" +#include "std_msgs/msg/string.hpp" + +class PubNode : public rclcpp::Node +{ +public: + explicit PubNode(rclcpp::NodeOptions options) + : Node("pub_node", options) + { + pub_ = this->create_publisher( + "the_topic", + rclcpp::QoS(10)); + } + +private: + rclcpp::Publisher::SharedPtr pub_; +}; + +int main(int argc, char * argv[]) +{ + rclcpp::init(argc, argv); + + rclcpp::executors::SingleThreadedExecutor exec; + auto pub_node = std::make_shared(rclcpp::NodeOptions()); + exec.add_node(pub_node); + + printf("spinning once\n"); + exec.spin_once(); + + rclcpp::shutdown(); + return 0; +} diff --git a/tracetools_test/src/test_service.cpp b/tracetools_test/src/test_service.cpp new file mode 100644 index 0000000..091bb4e --- /dev/null +++ b/tracetools_test/src/test_service.cpp @@ -0,0 +1,64 @@ +// Copyright 2019 Robert Bosch GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "rclcpp/rclcpp.hpp" +#include "std_srvs/srv/empty.hpp" + +class ServiceNode : public rclcpp::Node +{ +public: + explicit ServiceNode(rclcpp::NodeOptions options) + : Node("service_node", options) + { + srv_ = this->create_service( + "service", + std::bind( + &ServiceNode::service_callback, + this, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3)); + } + +private: + void service_callback( + const std::shared_ptr request_header, + const std::shared_ptr request, + const std::shared_ptr response) + { + // Nothing + (void)request_header; + (void)request; + (void)response; + } + + rclcpp::Service::SharedPtr srv_; +}; + +int main(int argc, char * argv[]) +{ + rclcpp::init(argc, argv); + + rclcpp::executors::SingleThreadedExecutor exec; + auto service_node = std::make_shared(rclcpp::NodeOptions()); + exec.add_node(service_node); + + printf("spinning once\n"); + exec.spin_once(); + + rclcpp::shutdown(); + return 0; +} diff --git a/tracetools_test/src/test_service_ping.cpp b/tracetools_test/src/test_service_ping.cpp new file mode 100644 index 0000000..5e097eb --- /dev/null +++ b/tracetools_test/src/test_service_ping.cpp @@ -0,0 +1,82 @@ +// Copyright 2019 Robert Bosch GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include "rclcpp/rclcpp.hpp" +#include "std_srvs/srv/empty.hpp" + +using namespace std::chrono_literals; + +class PingNode : public rclcpp::Node +{ +public: + explicit PingNode(rclcpp::NodeOptions options) + : Node("ping_node", options) + { + srv_ = this->create_service( + "pong", + std::bind( + &PingNode::service_callback, + this, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3)); + client_ = this->create_client( + "ping"); + timer_ = this->create_wall_timer( + 500ms, + std::bind(&PingNode::timer_callback, this)); + } + +private: + void service_callback( + const std::shared_ptr request_header, + const std::shared_ptr request, + const std::shared_ptr response) + { + (void)request_header; + (void)request; + (void)response; + RCLCPP_INFO(this->get_logger(), "got request"); + rclcpp::shutdown(); + } + + void timer_callback() + { + auto req = std::make_shared(); + client_->async_send_request(req); + } + + rclcpp::Service::SharedPtr srv_; + rclcpp::Client::SharedPtr client_; + rclcpp::TimerBase::SharedPtr timer_; +}; + +int main(int argc, char * argv[]) +{ + rclcpp::init(argc, argv); + + rclcpp::executors::SingleThreadedExecutor exec; + auto ping_node = std::make_shared(rclcpp::NodeOptions()); + exec.add_node(ping_node); + + printf("spinning\n"); + exec.spin(); + + // Will actually be called inside the node's service callback + rclcpp::shutdown(); + return 0; +} diff --git a/tracetools_test/src/test_service_pong.cpp b/tracetools_test/src/test_service_pong.cpp new file mode 100644 index 0000000..79b098b --- /dev/null +++ b/tracetools_test/src/test_service_pong.cpp @@ -0,0 +1,70 @@ +// Copyright 2019 Robert Bosch GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "rclcpp/rclcpp.hpp" +#include "std_srvs/srv/empty.hpp" + +class PongNode : public rclcpp::Node +{ +public: + explicit PongNode(rclcpp::NodeOptions options) + : Node("pong_node", options) + { + srv_ = this->create_service( + "ping", + std::bind( + &PongNode::service_callback, + this, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3)); + client_ = this->create_client("pong"); + } + +private: + void service_callback( + const std::shared_ptr request_header, + const std::shared_ptr request, + const std::shared_ptr response) + { + (void)request_header; + (void)request; + (void)response; + RCLCPP_INFO(this->get_logger(), "got request"); + auto req = std::make_shared(); + client_->async_send_request(req); + rclcpp::shutdown(); + } + + rclcpp::Service::SharedPtr srv_; + rclcpp::Client::SharedPtr client_; +}; + +int main(int argc, char * argv[]) +{ + rclcpp::init(argc, argv); + + rclcpp::executors::SingleThreadedExecutor exec; + auto pong_node = std::make_shared(rclcpp::NodeOptions()); + exec.add_node(pong_node); + + printf("spinning\n"); + exec.spin(); + + // Will actually be called inside the node's service callback + rclcpp::shutdown(); + return 0; +} diff --git a/tracetools_test/src/test_subscription.cpp b/tracetools_test/src/test_subscription.cpp new file mode 100644 index 0000000..48593cc --- /dev/null +++ b/tracetools_test/src/test_subscription.cpp @@ -0,0 +1,55 @@ +// Copyright 2019 Robert Bosch GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "rclcpp/rclcpp.hpp" +#include "std_msgs/msg/string.hpp" + +class SubNode : public rclcpp::Node +{ +public: + explicit SubNode(rclcpp::NodeOptions options) + : Node("sub_node", options) + { + sub_ = this->create_subscription( + "the_topic", + rclcpp::QoS(10), + std::bind(&SubNode::callback, this, std::placeholders::_1)); + } + +private: + void callback(const std_msgs::msg::String::SharedPtr msg) + { + // Nothing + (void)msg; + } + + rclcpp::Subscription::SharedPtr sub_; +}; + +int main(int argc, char * argv[]) +{ + rclcpp::init(argc, argv); + + rclcpp::executors::SingleThreadedExecutor exec; + auto sub_node = std::make_shared(rclcpp::NodeOptions()); + exec.add_node(sub_node); + + printf("spinning once\n"); + exec.spin_once(); + + rclcpp::shutdown(); + return 0; +} diff --git a/tracetools_test/src/test_timer.cpp b/tracetools_test/src/test_timer.cpp new file mode 100644 index 0000000..3ad04d7 --- /dev/null +++ b/tracetools_test/src/test_timer.cpp @@ -0,0 +1,62 @@ +// Copyright 2019 Robert Bosch GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include "rclcpp/rclcpp.hpp" + +using namespace std::chrono_literals; + +class TimerNode : public rclcpp::Node +{ +public: + explicit TimerNode(rclcpp::NodeOptions options) + : Node("timer_node", options) + { + is_done_ = false; + timer_ = this->create_wall_timer( + 1ms, + std::bind(&TimerNode::timer_callback, this)); + } + +private: + void timer_callback() + { + if (is_done_) { + rclcpp::shutdown(); + } else { + is_done_ = true; + } + } + + rclcpp::TimerBase::SharedPtr timer_; + bool is_done_; +}; + +int main(int argc, char * argv[]) +{ + rclcpp::init(argc, argv); + + rclcpp::executors::SingleThreadedExecutor exec; + auto timer_node = std::make_shared(rclcpp::NodeOptions()); + exec.add_node(timer_node); + + printf("spinning\n"); + exec.spin(); + + // Will actually be called inside the timer's callback + rclcpp::shutdown(); + return 0; +} diff --git a/tracetools_test/test/test_node.py b/tracetools_test/test/test_node.py new file mode 100644 index 0000000..a3d0c9f --- /dev/null +++ b/tracetools_test/test/test_node.py @@ -0,0 +1,54 @@ +# Copyright 2019 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from tracetools_test.utils import ( + cleanup_trace, + get_trace_event_names, + run_and_trace, +) + +BASE_PATH = '/tmp' +PKG = 'tracetools_test' +node_creation_events = [ + 'ros2:rcl_init', + 'ros2:rcl_node_init', +] + + +class TestNode(unittest.TestCase): + + def test_creation(self): + session_name_prefix = 'session-test-node-creation' + test_node = ['test_publisher'] + + exit_code, full_path = run_and_trace( + BASE_PATH, + session_name_prefix, + node_creation_events, + None, + PKG, + test_node) + self.assertEqual(exit_code, 0) + + trace_events = get_trace_event_names(full_path) + print(f'trace_events: {trace_events}') + self.assertSetEqual(set(node_creation_events), trace_events) + + cleanup_trace(full_path) + + +if __name__ == '__main__': + unittest.main() diff --git a/tracetools_test/test/test_publisher.py b/tracetools_test/test/test_publisher.py new file mode 100644 index 0000000..decbbd2 --- /dev/null +++ b/tracetools_test/test/test_publisher.py @@ -0,0 +1,53 @@ +# Copyright 2019 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from tracetools_test.utils import ( + cleanup_trace, + get_trace_event_names, + run_and_trace, +) + +BASE_PATH = '/tmp' +PKG = 'tracetools_test' +publisher_creation_events = [ + 'ros2:rcl_publisher_init', +] + + +class TestPublisher(unittest.TestCase): + + def test_creation(self): + session_name_prefix = 'session-test-publisher-creation' + test_node = ['test_publisher'] + + exit_code, full_path = run_and_trace( + BASE_PATH, + session_name_prefix, + publisher_creation_events, + None, + PKG, + test_node) + self.assertEqual(exit_code, 0) + + trace_events = get_trace_event_names(full_path) + print(f'trace_events: {trace_events}') + self.assertSetEqual(set(publisher_creation_events), trace_events) + + cleanup_trace(full_path) + + +if __name__ == '__main__': + unittest.main() diff --git a/tracetools_test/test/test_service.py b/tracetools_test/test/test_service.py new file mode 100644 index 0000000..7bcf697 --- /dev/null +++ b/tracetools_test/test/test_service.py @@ -0,0 +1,54 @@ +# Copyright 2019 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from tracetools_test.utils import ( + cleanup_trace, + get_trace_event_names, + run_and_trace, +) + +BASE_PATH = '/tmp' +PKG = 'tracetools_test' +service_creation_events = [ + 'ros2:rcl_service_init', + 'ros2:rclcpp_service_callback_added', +] + + +class TestService(unittest.TestCase): + + def test_creation(self): + session_name_prefix = 'session-test-service-creation' + test_nodes = ['test_service'] + + exit_code, full_path = run_and_trace( + BASE_PATH, + session_name_prefix, + service_creation_events, + None, + PKG, + test_nodes) + self.assertEqual(exit_code, 0) + + trace_events = get_trace_event_names(full_path) + print(f'trace_events: {trace_events}') + self.assertSetEqual(set(service_creation_events), trace_events) + + cleanup_trace(full_path) + + +if __name__ == '__main__': + unittest.main() diff --git a/tracetools_test/test/test_service_callback.py b/tracetools_test/test/test_service_callback.py new file mode 100644 index 0000000..8b6a1e0 --- /dev/null +++ b/tracetools_test/test/test_service_callback.py @@ -0,0 +1,54 @@ +# Copyright 2019 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from tracetools_test.utils import ( + cleanup_trace, + get_trace_event_names, + run_and_trace, +) + +BASE_PATH = '/tmp' +PKG = 'tracetools_test' +service_callback_events = [ + 'ros2:callback_start', + 'ros2:callback_end', +] + + +class TestServiceCallback(unittest.TestCase): + + def test_callback(self): + session_name_prefix = 'session-test-service-callback' + test_nodes = ['test_service_ping', 'test_service_pong'] + + exit_code, full_path = run_and_trace( + BASE_PATH, + session_name_prefix, + service_callback_events, + None, + PKG, + test_nodes) + self.assertEqual(exit_code, 0) + + trace_events = get_trace_event_names(full_path) + print(f'trace_events: {trace_events}') + self.assertSetEqual(set(service_callback_events), trace_events) + + cleanup_trace(full_path) + + +if __name__ == '__main__': + unittest.main() diff --git a/tracetools_test/test/test_subscription.py b/tracetools_test/test/test_subscription.py new file mode 100644 index 0000000..1562934 --- /dev/null +++ b/tracetools_test/test/test_subscription.py @@ -0,0 +1,54 @@ +# Copyright 2019 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from tracetools_test.utils import ( + cleanup_trace, + get_trace_event_names, + run_and_trace, +) + +BASE_PATH = '/tmp' +PKG = 'tracetools_test' +subscription_creation_events = [ + 'ros2:rcl_subscription_init', + 'ros2:rclcpp_subscription_callback_added', +] + + +class TestSubscription(unittest.TestCase): + + def test_creation(self): + session_name_prefix = 'session-test-subscription-creation' + test_node = ['test_subscription'] + + exit_code, full_path = run_and_trace( + BASE_PATH, + session_name_prefix, + subscription_creation_events, + None, + PKG, + test_node) + self.assertEqual(exit_code, 0) + + trace_events = get_trace_event_names(full_path) + print(f'trace_events: {trace_events}') + self.assertSetEqual(set(subscription_creation_events), trace_events) + + cleanup_trace(full_path) + + +if __name__ == '__main__': + unittest.main() diff --git a/tracetools_test/test/test_subscription_callback.py b/tracetools_test/test/test_subscription_callback.py new file mode 100644 index 0000000..251eb4f --- /dev/null +++ b/tracetools_test/test/test_subscription_callback.py @@ -0,0 +1,54 @@ +# Copyright 2019 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from tracetools_test.utils import ( + cleanup_trace, + get_trace_event_names, + run_and_trace, +) + +BASE_PATH = '/tmp' +PKG = 'tracetools_test' +subscription_callback_events = [ + 'ros2:callback_start', + 'ros2:callback_end', +] + + +class TestSubscriptionCallback(unittest.TestCase): + + def test_callback(self): + session_name_prefix = 'session-test-subscription-callback' + test_nodes = ['test_ping', 'test_pong'] + + exit_code, full_path = run_and_trace( + BASE_PATH, + session_name_prefix, + subscription_callback_events, + None, + PKG, + test_nodes) + self.assertEqual(exit_code, 0) + + trace_events = get_trace_event_names(full_path) + print(f'trace_events: {trace_events}') + self.assertSetEqual(set(subscription_callback_events), trace_events) + + cleanup_trace(full_path) + + +if __name__ == '__main__': + unittest.main() diff --git a/tracetools_test/test/test_timer.py b/tracetools_test/test/test_timer.py new file mode 100644 index 0000000..9b1b3e8 --- /dev/null +++ b/tracetools_test/test/test_timer.py @@ -0,0 +1,56 @@ +# Copyright 2019 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from tracetools_test.utils import ( + cleanup_trace, + get_trace_event_names, + run_and_trace, +) + +BASE_PATH = '/tmp' +PKG = 'tracetools_test' +timer_events = [ + 'ros2:rcl_timer_init', + 'ros2:rclcpp_timer_callback_added', + 'ros2:callback_start', + 'ros2:callback_end', +] + + +class TestTimer(unittest.TestCase): + + def test_all(self): + session_name_prefix = 'session-test-timer-all' + test_nodes = ['test_timer'] + + exit_code, full_path = run_and_trace( + BASE_PATH, + session_name_prefix, + timer_events, + None, + PKG, + test_nodes) + self.assertEqual(exit_code, 0) + + trace_events = get_trace_event_names(full_path) + print(f'trace_events: {trace_events}') + self.assertSetEqual(set(timer_events), trace_events) + + cleanup_trace(full_path) + + +if __name__ == '__main__': + unittest.main() diff --git a/tracetools_test/tracetools_test/__init__.py b/tracetools_test/tracetools_test/__init__.py new file mode 100644 index 0000000..4b18865 --- /dev/null +++ b/tracetools_test/tracetools_test/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tracetools_test/tracetools_test/utils.py b/tracetools_test/tracetools_test/utils.py new file mode 100644 index 0000000..94f8e31 --- /dev/null +++ b/tracetools_test/tracetools_test/utils.py @@ -0,0 +1,107 @@ +# Copyright 2019 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utils for tracetools_test.""" + +import os +import shutil +import time +from typing import List +from typing import Set +from typing import Tuple + +import babeltrace +from launch import LaunchDescription +from launch import LaunchService +from launch_ros import get_default_launch_description +import launch_ros.actions +from tracetools_trace.tools.lttng import ( + lttng_destroy, + lttng_setup, + lttng_start, + lttng_stop, +) + + +def run_and_trace( + base_path: str, + session_name_prefix: str, + ros_events: List[str], + kernel_events: List[str], + package_name: str, + node_names: List[str] + ) -> Tuple[int, str]: + """ + Run a node while tracing. + + :param base_path: the base path where to put the trace directory + :param session_name_prefix: the session name prefix for the trace directory + :param ros_events: the list of ROS UST events to enable + :param kernel_events: the list of kernel events to enable + :param package_name: the name of the package to use + :param node_names: the names of the nodes to execute + :return: exit code, full generated path + """ + session_name = f'{session_name_prefix}-{time.strftime("%Y%m%d%H%M%S")}' + full_path = os.path.join(base_path, session_name) + print(f'trace directory: {full_path}') + + lttng_setup(session_name, full_path, ros_events=ros_events, kernel_events=kernel_events) + lttng_start(session_name) + + nodes = [] + for node_name in node_names: + n = launch_ros.actions.Node( + package=package_name, + node_executable=node_name, + output='screen') + nodes.append(n) + ld = LaunchDescription(nodes) + ls = LaunchService() + ls.include_launch_description(get_default_launch_description()) + ls.include_launch_description(ld) + + exit_code = ls.run() + + lttng_stop(session_name) + lttng_destroy(session_name) + + return exit_code, full_path + + +def cleanup_trace(full_path: str) -> None: + """ + Cleanup trace data. + + :param full_path: the full path to the main trace directory + """ + shutil.rmtree(full_path) + + +def get_trace_event_names(trace_directory: str) -> Set[str]: + """ + Get a set of event names in a trace. + + :param trace_directory: the path to the main/top trace directory + :return: event names + """ + tc = babeltrace.TraceCollection() + tc.add_traces_recursive(trace_directory, 'ctf') + + event_names = set() + + for event in tc.events: + event_names.add(event.name) + + return event_names