From 015896bf18d741dee15423796a95918279915f88 Mon Sep 17 00:00:00 2001 From: Michael Carroll Date: Mon, 27 Feb 2023 08:37:59 -0600 Subject: [PATCH] Add a launch description wrapper (#11) Signed-off-by: Michael Carroll --- README.md | 4 +- .../config/reference_system.yaml | 97 ++----------- ros2_profiling_demo/test/test_graph.py | 6 +- ros2_profiling_demo/test/test_profile.py | 84 ++++++++++-- ros2profile/config/reference_system.yaml | 90 ------------- ros2profile/ros2profile/api/process.py | 20 ++- ros2profile/ros2profile/data/convert/ctf.py | 4 + ros2profile/ros2profile/verb/launch.py | 127 +++++++++++++++++- 8 files changed, 223 insertions(+), 209 deletions(-) delete mode 100644 ros2profile/config/reference_system.yaml diff --git a/README.md b/README.md index 99fd9afe..188c197d 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,9 @@ Then launch the demonstration system with the specified configuration: ``` -ros2 profile launch ~/safe_ros/src/ros2_profiling/ros2_profiling_demo/config/reference_system.yaml +ros2 profile launch \ + --launch-file ./src/reference_system/launch/reference_system.launch.py \ + --config-file ./src/ros2_profiling/ros2_profiling_demo/config/reference_system.yaml ``` The `topnode` uses ROS 2 lifecycle states to determine when to start/stop recording, so we can begin the recording via: diff --git a/ros2_profiling_demo/config/reference_system.yaml b/ros2_profiling_demo/config/reference_system.yaml index 33027ff6..4e27b0c4 100644 --- a/ros2_profiling_demo/config/reference_system.yaml +++ b/ros2_profiling_demo/config/reference_system.yaml @@ -1,90 +1,11 @@ -nodes: - - name: cordoba - package: reference_system - plugin: reference_system::Cordoba - - name: freeport - package: reference_system - plugin: reference_system::Freeport - - name: medellin - package: reference_system - plugin: reference_system::Medellin - - name: portsmouth - package: reference_system - plugin: reference_system::Portsmouth - - name: delhi - package: reference_system - plugin: reference_system::Delhi - - name: taipei - package: reference_system - plugin: reference_system::Taipei - - name: lyon - package: reference_system - plugin: reference_system::Lyon - - name: hebron - package: reference_system - plugin: reference_system::Hebron - - name: kingston - package: reference_system - plugin: reference_system::Kingston - - name: hamburg - package: reference_system - plugin: reference_system::Hamburg - - name: osaka - package: reference_system - plugin: reference_system::Osaka - - name: tripoli - package: reference_system - plugin: reference_system::Tripoli - - name: mandalay - package: reference_system - plugin: reference_system::Mandalay - - name: ponce - package: reference_system - plugin: reference_system::Ponce - - name: geneva - package: reference_system - plugin: reference_system::Geneva - - name: monaco - package: reference_system - plugin: reference_system::Monaco - - name: rotterdam - package: reference_system - plugin: reference_system::Rotterdam - - name: barcelona - package: reference_system - plugin: reference_system::Barcelona - - name: arequipa - package: reference_system - plugin: reference_system::Arequipa - - name: georgetown - package: reference_system - plugin: reference_system::Georgetown +record_path: ~/.ros/profile -containers: - - name: reference_system_robot - type: component_container_isolated - nodes: - - cordoba - - freeport - - medellin - - portsmouth - - delhi - - taipei - - lyon - - hebron - - kingston - - hamburg - - osaka - - tripoli - - mandalay - - ponce +tracing: + session_name: ros2profile-tracing-session + kernel: + events: [] - - name: reference_system_control - type: component_container_isolated - nodes: - - geneva - - monaco - - rotterdam - - barcelona - - arequipa - - georgetown +topnode: + nodes: + - "/reference_system_robot" + - "/reference_system_control" diff --git a/ros2_profiling_demo/test/test_graph.py b/ros2_profiling_demo/test/test_graph.py index 1c40e03b..f722bc6d 100644 --- a/ros2_profiling_demo/test/test_graph.py +++ b/ros2_profiling_demo/test/test_graph.py @@ -2,9 +2,9 @@ def test_graph(profile_event_graph): graph = profile_event_graph - assert len(graph.nodes()) == 24 - assert len(graph.topics()) == 25 - assert len(graph.publishers()) == 25 + assert len(graph.nodes()) == 31 + assert len(graph.topics()) == 29 + assert len(graph.publishers()) == 29 assert len(graph.subscriptions()) == 36 diff --git a/ros2_profiling_demo/test/test_profile.py b/ros2_profiling_demo/test/test_profile.py index c666e010..789ddfb2 100644 --- a/ros2_profiling_demo/test/test_profile.py +++ b/ros2_profiling_demo/test/test_profile.py @@ -1,14 +1,70 @@ -def test_nodes(profile_data): - assert len(profile_data.nodes()) == 20 - - # Assert that all of the nodes under test are available - # in the tracing data - for node in profile_data.nodes(): - handle = profile_data.node_handle(node['name']) - assert handle - -def test_cpu(profile_data): - containers = profile_data.containers() - for container in containers: - usage = profile_data.cpu_memory_usage(container) - assert (usage['cpu_percent'] < 50).all() +import numpy as np + +def check_diff(field, tol=1e-6): + check = np.abs(field.max() - field.min()) + + if check >= tol: + print(field.max(), field.min()) + return check < tol + +def test_reference_system_control(profile_data): + ''' + Demonstrate asserting based on topnode-collected profiling data + ''' + data = None + for key in profile_data.keys(): + if key.find('reference_system_control') >= 0: + data = profile_data[key] + assert data is not None + + # By default: + # Topnode data should have 4 message types: + assert '~/cpu_memory_usage' in data + assert '~/memory_state' in data + assert '~/io_stats' in data + assert '~/stat' in data + + cpu_memory_usage = data['~/cpu_memory_usage'] + memory_state = data['~/memory_state'] + io_stats = data['~/io_stats'] + stat = data['~/stat'] + + # CPU should never exceed 5% + assert np.all(cpu_memory_usage['cpu_percent'] < 5) + + # Memory usage should stay constant + assert check_diff(cpu_memory_usage.max_resident_set_size) + assert check_diff(cpu_memory_usage.shared_size) + assert check_diff(cpu_memory_usage.virtual_size) + assert check_diff(cpu_memory_usage.memory_percent) + assert check_diff(memory_state.resident_size, 100) + + +def test_reference_system_robot(profile_data): + data = None + for key in profile_data.keys(): + if key.find('reference_system_control') >= 0: + data = profile_data[key] + assert data is not None + + # By default: + # Topnode data should have 4 message types: + assert '~/cpu_memory_usage' in data + assert '~/memory_state' in data + assert '~/io_stats' in data + assert '~/stat' in data + + cpu_memory_usage = data['~/cpu_memory_usage'] + memory_state = data['~/memory_state'] + io_stats = data['~/io_stats'] + stat = data['~/stat'] + + # CPU should never exceed 5% + assert np.all(cpu_memory_usage['cpu_percent'] < 5) + + # Memory usage should stay constant + assert check_diff(cpu_memory_usage.max_resident_set_size) + assert check_diff(cpu_memory_usage.shared_size) + assert check_diff(cpu_memory_usage.virtual_size) + assert check_diff(cpu_memory_usage.memory_percent) + assert check_diff(memory_state.resident_size, 100) diff --git a/ros2profile/config/reference_system.yaml b/ros2profile/config/reference_system.yaml deleted file mode 100644 index 33027ff6..00000000 --- a/ros2profile/config/reference_system.yaml +++ /dev/null @@ -1,90 +0,0 @@ -nodes: - - name: cordoba - package: reference_system - plugin: reference_system::Cordoba - - name: freeport - package: reference_system - plugin: reference_system::Freeport - - name: medellin - package: reference_system - plugin: reference_system::Medellin - - name: portsmouth - package: reference_system - plugin: reference_system::Portsmouth - - name: delhi - package: reference_system - plugin: reference_system::Delhi - - name: taipei - package: reference_system - plugin: reference_system::Taipei - - name: lyon - package: reference_system - plugin: reference_system::Lyon - - name: hebron - package: reference_system - plugin: reference_system::Hebron - - name: kingston - package: reference_system - plugin: reference_system::Kingston - - name: hamburg - package: reference_system - plugin: reference_system::Hamburg - - name: osaka - package: reference_system - plugin: reference_system::Osaka - - name: tripoli - package: reference_system - plugin: reference_system::Tripoli - - name: mandalay - package: reference_system - plugin: reference_system::Mandalay - - name: ponce - package: reference_system - plugin: reference_system::Ponce - - name: geneva - package: reference_system - plugin: reference_system::Geneva - - name: monaco - package: reference_system - plugin: reference_system::Monaco - - name: rotterdam - package: reference_system - plugin: reference_system::Rotterdam - - name: barcelona - package: reference_system - plugin: reference_system::Barcelona - - name: arequipa - package: reference_system - plugin: reference_system::Arequipa - - name: georgetown - package: reference_system - plugin: reference_system::Georgetown - -containers: - - name: reference_system_robot - type: component_container_isolated - nodes: - - cordoba - - freeport - - medellin - - portsmouth - - delhi - - taipei - - lyon - - hebron - - kingston - - hamburg - - osaka - - tripoli - - mandalay - - ponce - - - name: reference_system_control - type: component_container_isolated - nodes: - - geneva - - monaco - - rotterdam - - barcelona - - arequipa - - georgetown diff --git a/ros2profile/ros2profile/api/process.py b/ros2profile/ros2profile/api/process.py index f5a6311c..a4d67b45 100644 --- a/ros2profile/ros2profile/api/process.py +++ b/ros2profile/ros2profile/api/process.py @@ -105,9 +105,15 @@ def process_one(input_file): def process(input_path): mcap_files = glob.glob(input_path + '*.mcap') - print(f'Processing {len(mcap_files)} topnode files') + to_process = [] for mcap_file in mcap_files: + base = os.path.splitext(mcap_file)[0] + if not os.path.exists(base + '.converted'): + to_process.append(base) + print(f'Processing {len(to_process)} topnode files ({len(mcap_files)} cached)') + + for mcap_file in to_process: base = os.path.splitext(mcap_file)[0] data = process_one(mcap_file) @@ -115,12 +121,12 @@ def process(input_path): p = pickle.Pickler(f, protocol=4) p.dump(data) - events = load_ctf(input_path) - graph = build_graph(events) - - with open(os.path.join(input_path, 'event_graph'), 'wb') as f: - p = pickle.Pickler(f, protocol=4) - p.dump(graph) + if not os.path.exists(os.path.join(input_path, 'event_graph')): + events = load_ctf(input_path) + graph = build_graph(events) + with open(os.path.join(input_path, 'event_graph'), 'wb') as f: + p = pickle.Pickler(f, protocol=4) + p.dump(graph) def load_mcap_data(input_path): diff --git a/ros2profile/ros2profile/data/convert/ctf.py b/ros2profile/ros2profile/data/convert/ctf.py index 18dc255b..2cb0167a 100644 --- a/ros2profile/ros2profile/data/convert/ctf.py +++ b/ros2profile/ros2profile/data/convert/ctf.py @@ -25,12 +25,16 @@ 'rmw_publisher_handle': int, 'rmw_subscription_handle': int, 'rmw_service_handle': int, + 'rmw_client_handle': int, 'subscription_handle': int, 'publisher_handle': int, 'service_handle': int, + 'client_handle': int, 'timer_handle': int, 'rmw_handle': int, 'node_name': str, + 'start_label': str, + 'goal_label': str, 'namespace': str, 'version': str, 'vpid': int, diff --git a/ros2profile/ros2profile/verb/launch.py b/ros2profile/ros2profile/verb/launch.py index d19f7d49..68b01670 100644 --- a/ros2profile/ros2profile/verb/launch.py +++ b/ros2profile/ros2profile/verb/launch.py @@ -1,19 +1,134 @@ -from ros2profile.verb import VerbExtension -from ros2profile.api.launch import expand_configuration +import os +import shutil +import yaml + +from typing import Optional + +from ros2profile.verb import VerbExtension import launch +import launch.event_handlers +from launch.some_actions_type import SomeActionsType +import launch_ros.actions + +from tracetools_launch.action import Trace +from tracetools_trace.tools import path + class LaunchVerb(VerbExtension): def add_arguments(self, parser, cli_name): #noqa: D102 parser.add_argument( - 'profile_config', help='Configuration to profile' + '--launch-file', help='Launch file containing description of system under test' + ) + parser.add_argument( + '--config-file', help='Profiling configuration file' ) def main(self, *, args): - launch_description = expand_configuration(args.profile_config) + if not os.path.exists(args.config_file): + print(f"Config file does not exist: {args.config_file}") + return + + config = None + with open(args.config_file, 'r', encoding='utf8') as f: + config = yaml.load(f, Loader=yaml.SafeLoader) + + if not config: + print(f"Failed to load config: {args.config_file}") + return + + launch_description = launch.LaunchDescription([ + launch.actions.IncludeLaunchDescription( + launch.launch_description_sources.AnyLaunchDescriptionSource( + args.launch_file + ), + ), + ]) + + session_name = 'ros2profile-tracing-session' + base_path = '~/.ros/profile' + append_timestamp = False + + context_fields = { + 'kernel': ['vpid', 'vtid', 'procname'], + 'userspace': ['vpid', 'vtid', 'procname'], + } + + events_kernel = [] + events_ust = ['dds:*', 'ros2:*'] + + if 'record_path' in config: + base_path = config['record_path'] + + if 'tracing' in config: + trace_config = config['tracing'] + if 'session_name' in trace_config: + session_name = trace_config['session_name'] + + if 'kernel' in trace_config: + if 'events' in trace_config['kernel']: + events_kernel = trace_config['kernel']['events'] + if 'context_field' in trace_config['kernel']: + context_fields['kernel'] = trace_config['kernel']['context_fields'] + + if 'ust' in trace_config: + if 'events' in trace_config['ust']: + events_ust = trace_config['ust']['events'] + if 'context_field' in trace_config['ust']: + context_fields['userspace'] = trace_config['ust']['context_fields'] + + if len(events_kernel) == 0: + del context_fields['kernel'] + if len(events_ust) == 0: + del context_fields['userspace'] + + basename = os.path.basename(args.config_file) + basename = basename.split('.')[0] + + basename = path.append_timestamp(basename) + + output_dir = os.path.normpath(os.path.expanduser(os.path.join(base_path, basename))) + + os.makedirs(output_dir, exist_ok=True) + shutil.copyfile(args.config_file, os.path.join(output_dir, 'config.yaml')) + + launch_description.add_action(action=Trace( + session_name=session_name, + base_path=output_dir, + append_timestamp=append_timestamp, + events_kernel=events_kernel, + events_ust=events_ust, + context_fields=context_fields + )) + + def on_start(event: launch.events.process.ProcessStarted, + context: launch.launch_context.LaunchContext) -> Optional[SomeActionsType]: + if event.action.node_name in nodes: + pid = event.pid + + return launch_ros.actions.Node( + name=f'topnode_{pid}', + namespace='', + package='topnode', + executable='resource_monitor', + output='screen', + parameters=[{ + "publish_period_ms": 500, + "record_cpu_memory_usage": True, + "record_memory_state": True, + "record_io_stats": True, + "record_stat": True, + "record_file": f'{output_dir}{event.action.node_name}_{pid}.mcap', + "pid": pid + }]) + + if 'topnode' in config and 'nodes' in config['topnode']: + nodes = config['topnode']['nodes'] + launch_description.add_action(launch.actions.RegisterEventHandler( + launch.event_handlers.OnProcessStart(on_start=on_start) + )) - print(launch.LaunchIntrospector().format_launch_description(launch_description)) launch_service = launch.LaunchService() launch_service.include_launch_description(launch_description) - ret = launch_service.run() + return launch_service.run()