diff --git a/system/autoware_component_monitor/CMakeLists.txt b/system/autoware_component_monitor/CMakeLists.txt new file mode 100644 index 0000000000000..674b079a90563 --- /dev/null +++ b/system/autoware_component_monitor/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.8) +project(autoware_component_monitor) + +find_package(autoware_cmake REQUIRED) +autoware_package() + +find_package(Boost REQUIRED COMPONENTS + filesystem +) + +ament_auto_add_library(${PROJECT_NAME} SHARED + src/component_monitor_node.cpp +) +target_link_libraries(${PROJECT_NAME} ${Boost_LIBRARIES}) + +rclcpp_components_register_node(${PROJECT_NAME} + PLUGIN "autoware::component_monitor::ComponentMonitor" + EXECUTABLE ${PROJECT_NAME}_node +) + +if(BUILD_TESTING) + ament_add_ros_isolated_gtest(test_unit_conversions test/test_unit_conversions.cpp) + target_link_libraries(test_unit_conversions ${PROJECT_NAME}) + target_include_directories(test_unit_conversions PRIVATE src) +endif() + +ament_auto_package( + INSTALL_TO_SHARE + config + launch +) diff --git a/system/autoware_component_monitor/README.md b/system/autoware_component_monitor/README.md new file mode 100644 index 0000000000000..c255c420c048e --- /dev/null +++ b/system/autoware_component_monitor/README.md @@ -0,0 +1,84 @@ +# autoware_component_monitor + +The `autoware_component_monitor` package allows monitoring system usage of component containers. +The composable node inside the package is attached to a component container, and it publishes CPU and memory usage of +the container. + +## Inputs / Outputs + +### Input + +None. + +### Output + +| Name | Type | Description | +| -------------------------- | -------------------------------------------------- | ---------------------- | +| `~/component_system_usage` | `autoware_internal_msgs::msg::ResourceUsageReport` | CPU, Memory usage etc. | + +## Parameters + +### Core Parameters + +{{ json_to_markdown("system/autoware_component_monitor/schema/component_monitor.schema.json") }} + +## How to use + +Add it as a composable node in your launch file: + +```xml + + + + + ... + + + + + + + + ... + + +``` + +### Quick testing + +You can test the package by running the following command: + +```bash +ros2 component load autoware_component_monitor autoware::component_monitor::ComponentMonitor -p publish_rate:=10.0 --node-namespace + +# Example usage +ros2 component load /pointcloud_container autoware_component_monitor autoware::component_monitor::ComponentMonitor -p publish_rate:=10.0 --node-namespace /pointcloud_container +``` + +## How it works + +The package uses the `top` command under the hood. +`top -b -n 1 -E k -p PID` command is run at 10 Hz to get the system usage of the process. + +- `-b` activates the batch mode. By default, `top` doesn't exit and prints to stdout periodically. Batch mode allows + exiting the program. +- `-n` number of times should `top` prints the system usage in batch mode. +- `-p` specifies the PID of the process to monitor. +- `-E k` changes the memory unit in the summary section to KiB. + +Here is a sample output: + +```text +top - 13:57:26 up 3:14, 1 user, load average: 1,09, 1,10, 1,04 +Tasks: 1 total, 0 running, 1 sleeping, 0 stopped, 0 zombie +%Cpu(s): 0,0 us, 0,8 sy, 0,0 ni, 99,2 id, 0,0 wa, 0,0 hi, 0,0 si, 0,0 st +KiB Mem : 65532208 total, 35117428 free, 17669824 used, 12744956 buff/cache +KiB Swap: 39062524 total, 39062524 free, 0 used. 45520816 avail Mem + + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND + 3352 meb 20 0 2905940 1,2g 39292 S 0,0 2,0 23:24.01 awesome +``` + +We get 5th, 8th fields from the last line, which are RES, %CPU respectively. diff --git a/system/autoware_component_monitor/config/component_monitor.param.yaml b/system/autoware_component_monitor/config/component_monitor.param.yaml new file mode 100644 index 0000000000000..62cf278921460 --- /dev/null +++ b/system/autoware_component_monitor/config/component_monitor.param.yaml @@ -0,0 +1,3 @@ +/**: + ros__parameters: + publish_rate: 5.0 # Hz diff --git a/system/autoware_component_monitor/launch/component_monitor.launch.xml b/system/autoware_component_monitor/launch/component_monitor.launch.xml new file mode 100644 index 0000000000000..1b2a77eddab93 --- /dev/null +++ b/system/autoware_component_monitor/launch/component_monitor.launch.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/system/autoware_component_monitor/package.xml b/system/autoware_component_monitor/package.xml new file mode 100644 index 0000000000000..640e1f2dc2517 --- /dev/null +++ b/system/autoware_component_monitor/package.xml @@ -0,0 +1,25 @@ + + + + autoware_component_monitor + 0.0.0 + A ROS 2 package to monitor system usage of component containers. + Mehmet Emin Başoğlu + Apache-2.0 + + ament_cmake_auto + autoware_cmake + + autoware_internal_msgs + libboost-filesystem-dev + rclcpp + rclcpp_components + + ament_cmake_ros + ament_lint_auto + autoware_lint_common + + + ament_cmake + + diff --git a/system/autoware_component_monitor/schema/component_monitor.schema.json b/system/autoware_component_monitor/schema/component_monitor.schema.json new file mode 100644 index 0000000000000..f0edf5add718e --- /dev/null +++ b/system/autoware_component_monitor/schema/component_monitor.schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Parameters for the Component Monitor node", + "type": "object", + "definitions": { + "component_monitor": { + "type": "object", + "properties": { + "publish_rate": { + "type": "number", + "default": "5.0", + "description": "Publish rate in Hz" + } + }, + "required": ["publish_rate"] + } + }, + "properties": { + "/**": { + "type": "object", + "properties": { + "ros__parameters": { + "$ref": "#/definitions/component_monitor" + } + }, + "required": ["ros__parameters"] + } + }, + "required": ["/**"] +} diff --git a/system/autoware_component_monitor/src/component_monitor_node.cpp b/system/autoware_component_monitor/src/component_monitor_node.cpp new file mode 100644 index 0000000000000..3c5d6b6667725 --- /dev/null +++ b/system/autoware_component_monitor/src/component_monitor_node.cpp @@ -0,0 +1,177 @@ +// Copyright 2024 The Autoware Foundation +// +// 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 "component_monitor_node.hpp" + +#include "unit_conversions.hpp" + +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace autoware::component_monitor +{ +ComponentMonitor::ComponentMonitor(const rclcpp::NodeOptions & node_options) +: Node("component_monitor", node_options), publish_rate_(declare_parameter("publish_rate")) +{ + usage_pub_ = + create_publisher("~/component_system_usage", rclcpp::SensorDataQoS()); + + // Make sure top ins installed and is in path + const auto path_top = boost::process::search_path("top"); + if (path_top.empty()) { + RCLCPP_ERROR_STREAM(get_logger(), "Couldn't find 'top' in path."); + rclcpp::shutdown(); + } + + // Get the PID of the current process + int pid = getpid(); + + environment_ = boost::this_process::environment(); + environment_["LC_NUMERIC"] = "en_US.UTF-8"; + + on_timer_tick_wrapped_ = std::bind(&ComponentMonitor::on_timer_tick, this, pid); + + timer_ = rclcpp::create_timer( + this, get_clock(), rclcpp::Rate(publish_rate_).period(), on_timer_tick_wrapped_); +} + +void ComponentMonitor::on_timer_tick(const int pid) const +{ + if (usage_pub_->get_subscription_count() == 0) return; + + try { + auto usage_msg = pid_to_report(pid); + usage_msg.header.stamp = this->now(); + usage_msg.pid = pid; + usage_pub_->publish(usage_msg); + } catch (std::exception & e) { + RCLCPP_ERROR(get_logger(), "%s", e.what()); + } catch (...) { + RCLCPP_ERROR(get_logger(), "An unknown error occurred."); + } +} + +ComponentMonitor::ResourceUsageReport ComponentMonitor::pid_to_report(const pid_t & pid) const +{ + const auto std_out = run_system_command("top -b -n 1 -E k -p " + std::to_string(pid)); + + const auto fields = parse_lines_into_words(std_out); + + ResourceUsageReport report; + report.cpu_cores_utilized = std::stof(fields.back().at(8)) / 100.0f; + report.total_memory_bytes = unit_conversions::kib_to_bytes(std::stoul(fields.at(3).at(3))); + report.free_memory_bytes = unit_conversions::kib_to_bytes(std::stoul(fields.at(3).at(5))); + report.process_memory_bytes = parse_memory_res(fields.back().at(5)); + + return report; +} + +std::stringstream ComponentMonitor::run_system_command(const std::string & cmd) const +{ + int out_fd[2]; + if (pipe2(out_fd, O_CLOEXEC) != 0) { + RCLCPP_ERROR_STREAM(get_logger(), "Error setting flags on out_fd"); + } + boost::process::pipe out_pipe{out_fd[0], out_fd[1]}; + boost::process::ipstream is_out{std::move(out_pipe)}; + + int err_fd[2]; + if (pipe2(err_fd, O_CLOEXEC) != 0) { + RCLCPP_ERROR_STREAM(get_logger(), "Error setting flags on err_fd"); + } + boost::process::pipe err_pipe{err_fd[0], err_fd[1]}; + boost::process::ipstream is_err{std::move(err_pipe)}; + + boost::process::child c( + cmd, environment_, boost::process::std_out > is_out, boost::process::std_err > is_err); + c.wait(); + + if (c.exit_code() != 0) { + std::ostringstream os; + is_err >> os.rdbuf(); + RCLCPP_ERROR_STREAM(get_logger(), "Error running command: " << os.str()); + } + + std::stringstream sstream; + sstream << is_out.rdbuf(); + return sstream; +} + +ComponentMonitor::VecVecStr ComponentMonitor::parse_lines_into_words( + const std::stringstream & std_out) +{ + VecVecStr fields; + std::string line; + std::istringstream input{std_out.str()}; + + while (std::getline(input, line)) { + std::istringstream iss_line{line}; + std::string word; + std::vector words; + + while (iss_line >> word) { + words.push_back(word); + } + + fields.push_back(words); + } + + return fields; +} + +std::uint64_t ComponentMonitor::parse_memory_res(const std::string & mem_res) +{ + // example 1: 12.3g + // example 2: 123 (without suffix, just bytes) + static const std::unordered_map> unit_map{ + {'k', unit_conversions::kib_to_bytes}, {'m', unit_conversions::mib_to_bytes}, + {'g', unit_conversions::gib_to_bytes}, {'t', unit_conversions::tib_to_bytes}, + {'p', unit_conversions::pib_to_bytes}, {'e', unit_conversions::eib_to_bytes}}; + + if (std::isdigit(mem_res.back())) { + return std::stoull(mem_res); // Handle plain bytes without any suffix + } + + // Extract the numeric part and the unit suffix + double value = std::stod(mem_res.substr(0, mem_res.size() - 1)); + char suffix = mem_res.back(); + + // Find the appropriate function from the map + auto it = unit_map.find(suffix); + if (it != unit_map.end()) { + const auto & conversion_function = it->second; + return conversion_function(value); + } + + // Throw an exception or handle the error as needed if the suffix is not recognized + throw std::runtime_error("Unsupported unit suffix: " + std::string(1, suffix)); +} + +} // namespace autoware::component_monitor + +#include +RCLCPP_COMPONENTS_REGISTER_NODE(autoware::component_monitor::ComponentMonitor) diff --git a/system/autoware_component_monitor/src/component_monitor_node.hpp b/system/autoware_component_monitor/src/component_monitor_node.hpp new file mode 100644 index 0000000000000..70a486eb8209b --- /dev/null +++ b/system/autoware_component_monitor/src/component_monitor_node.hpp @@ -0,0 +1,102 @@ +// Copyright 2024 The Autoware Foundation +// +// 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 COMPONENT_MONITOR_NODE_HPP_ +#define COMPONENT_MONITOR_NODE_HPP_ + +#include + +#include + +#include + +#include +#include +#include +#include + +namespace autoware::component_monitor +{ +class ComponentMonitor : public rclcpp::Node +{ +public: + explicit ComponentMonitor(const rclcpp::NodeOptions & node_options); + +private: + using ResourceUsageReport = autoware_internal_msgs::msg::ResourceUsageReport; + using VecVecStr = std::vector>; + + const double publish_rate_; + + std::function on_timer_tick_wrapped_; + + rclcpp::Publisher::SharedPtr usage_pub_; + rclcpp::TimerBase::SharedPtr timer_; + + boost::process::native_environment environment_; + + void on_timer_tick(int pid) const; + + /** + * @brief Get system usage of the component. + * + * @details The output of top -b -n 1 -E k -p PID` is like below: + * + * top - 13:57:26 up 3:14, 1 user, load average: 1,09, 1,10, 1,04 + * Tasks: 1 total, 0 running, 1 sleeping, 0 stopped, 0 zombie + * %Cpu(s): 0,0 us, 0,8 sy, 0,0 ni, 99,2 id, 0,0 wa, 0,0 hi, 0,0 si, 0,0 st + * KiB Mem : 65532208 total, 35117428 free, 17669824 used, 12744956 buff/cache + * KiB Swap: 39062524 total, 39062524 free, 0 used. 45520816 avail Mem + * + * PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND + * 3352 meb 20 0 2905940 1,2g 39292 S 0,0 2,0 23:24.01 awesome + * + * We get 5th and 8th fields, which are RES, %CPU, respectively. + */ + ResourceUsageReport pid_to_report(const pid_t & pid) const; + + /** + * @brief Run a terminal command and return the standard output. + * + * @param cmd The terminal command to run + * @return The standard output of the command + */ + std::stringstream run_system_command(const std::string & cmd) const; + + /** + * @brief Parses text from a stringstream into separated words by line. + * + * @param std_out Reference to the input stringstream. + * @return Nested vector with each inner vector containing words from one line. + */ + static VecVecStr parse_lines_into_words(const std::stringstream & std_out); + + /** + * @brief Parses a memory resource string and converts it to bytes. + * + * This function handles memory size strings with suffixes to denote + * the units (e.g., "k" for kibibytes, "m" for mebibytes, etc.). + * If the string has no suffix, it is interpreted as plain bytes. + * + * @param mem_res A string representing the memory resource with a unit suffix or just bytes. + * @return uint64_t The memory size in bytes. + * + * @exception std::runtime_error Thrown if the suffix is not recognized. + */ + static std::uint64_t parse_memory_res(const std::string & mem_res); +}; + +} // namespace autoware::component_monitor + +#endif // COMPONENT_MONITOR_NODE_HPP_ diff --git a/system/autoware_component_monitor/src/unit_conversions.hpp b/system/autoware_component_monitor/src/unit_conversions.hpp new file mode 100644 index 0000000000000..c8f3fa02da519 --- /dev/null +++ b/system/autoware_component_monitor/src/unit_conversions.hpp @@ -0,0 +1,68 @@ +// Copyright 2024 The Autoware Foundation +// +// 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 UNIT_CONVERSIONS_HPP_ +#define UNIT_CONVERSIONS_HPP_ + +#include +#include + +namespace autoware::component_monitor::unit_conversions +{ +template +std::uint64_t kib_to_bytes(T kibibytes) +{ + static_assert(std::is_arithmetic::value, "Template parameter must be a numeric type"); + return static_cast(kibibytes * 1024); +} + +template +std::uint64_t mib_to_bytes(T mebibytes) +{ + static_assert(std::is_arithmetic::value, "Template parameter must be a numeric type"); + return static_cast(mebibytes * 1024 * 1024); +} + +template +std::uint64_t gib_to_bytes(T gibibytes) +{ + static_assert(std::is_arithmetic::value, "Template parameter must be a numeric type"); + return static_cast(gibibytes * 1024ULL * 1024ULL * 1024ULL); +} + +template +std::uint64_t tib_to_bytes(T tebibytes) +{ + static_assert(std::is_arithmetic::value, "Template parameter must be a numeric type"); + return static_cast(tebibytes * 1024ULL * 1024ULL * 1024ULL * 1024ULL); +} + +template +std::uint64_t pib_to_bytes(T pebibytes) +{ + static_assert(std::is_arithmetic::value, "Template parameter must be a numeric type"); + return static_cast(pebibytes * 1024ULL * 1024ULL * 1024ULL * 1024ULL * 1024ULL); +} + +template +std::uint64_t eib_to_bytes(T exbibytes) +{ + static_assert(std::is_arithmetic::value, "Template parameter must be a numeric type"); + return static_cast( + exbibytes * 1024ULL * 1024ULL * 1024ULL * 1024ULL * 1024ULL * 1024ULL); +} + +} // namespace autoware::component_monitor::unit_conversions + +#endif // UNIT_CONVERSIONS_HPP_ diff --git a/system/autoware_component_monitor/test/test_unit_conversions.cpp b/system/autoware_component_monitor/test/test_unit_conversions.cpp new file mode 100644 index 0000000000000..e8104ff31b80e --- /dev/null +++ b/system/autoware_component_monitor/test/test_unit_conversions.cpp @@ -0,0 +1,57 @@ +// Copyright 2024 The Autoware Foundation +// +// 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 "unit_conversions.hpp" + +#include + +namespace autoware::component_monitor::unit_conversions +{ +TEST(UnitConversions, kib_to_bytes) +{ + EXPECT_EQ(kib_to_bytes(1), 1024U); + EXPECT_EQ(kib_to_bytes(0), 0U); + EXPECT_EQ(kib_to_bytes(10), 10240U); +} +TEST(UnitConversions, mib_to_bytes) +{ + EXPECT_EQ(mib_to_bytes(1), 1048576U); + EXPECT_EQ(mib_to_bytes(0), 0U); + EXPECT_EQ(mib_to_bytes(10), 10485760U); +} +TEST(UnitConversions, gib_to_bytes) +{ + EXPECT_EQ(gib_to_bytes(1), 1073741824U); + EXPECT_EQ(gib_to_bytes(0), 0U); + EXPECT_EQ(gib_to_bytes(10), 10737418240U); +} +TEST(UnitConversions, tib_to_bytes) +{ + EXPECT_EQ(tib_to_bytes(1), 1099511627776U); + EXPECT_EQ(tib_to_bytes(0), 0U); + EXPECT_EQ(tib_to_bytes(10), 10995116277760U); +} +TEST(UnitConversions, pib_to_bytes) +{ + EXPECT_EQ(pib_to_bytes(1), 1125899906842624U); + EXPECT_EQ(pib_to_bytes(0), 0U); + EXPECT_EQ(pib_to_bytes(10), 11258999068426240U); +} +TEST(UnitConversions, eib_to_bytes) +{ + EXPECT_EQ(eib_to_bytes(1), 1152921504606846976U); + EXPECT_EQ(eib_to_bytes(0), 0U); + EXPECT_EQ(eib_to_bytes(10), 11529215046068469760U); +} +} // namespace autoware::component_monitor::unit_conversions