diff --git a/tracetools_trace/.gitignore b/tracetools_trace/.gitignore new file mode 100644 index 0000000..eef29c1 --- /dev/null +++ b/tracetools_trace/.gitignore @@ -0,0 +1,3 @@ +*~ +*.pyc + diff --git a/tracetools_trace/package.xml b/tracetools_trace/package.xml new file mode 100644 index 0000000..c3b7ffd --- /dev/null +++ b/tracetools_trace/package.xml @@ -0,0 +1,23 @@ + + + + tracetools_trace + 0.0.1 + Tools for setting up tracing sessions + Christophe Bedard + TODO + Christophe Bedard + + + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + diff --git a/tracetools_trace/setup.cfg b/tracetools_trace/setup.cfg new file mode 100644 index 0000000..6bbabc6 --- /dev/null +++ b/tracetools_trace/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script-dir=$base/lib/tracetools_trace +[install] +install-scripts=$base/lib/tracetools_trace diff --git a/tracetools_trace/setup.py b/tracetools_trace/setup.py new file mode 100644 index 0000000..e6c3e67 --- /dev/null +++ b/tracetools_trace/setup.py @@ -0,0 +1,28 @@ +from setuptools import find_packages +from setuptools import setup + +package_name = 'tracetools_trace' + +setup( + name=package_name, + version='0.0.1', + packages=find_packages(exclude=['test']), + data_files=[ + ('share/' + package_name, ['package.xml']), + ], + install_requires=['setuptools'], + maintainer='Christophe Bedard', + maintainer_email='fixed-term.christophe.bourquebedard@de.bosch.com', + author='Christophe Bedard', + author_email='fixed-term.christophe.bourquebedard@de.bosch.com', + # url='', + keywords=['ROS'], + description='Tools for setting up tracing sessions', + entry_points={ + 'console_scripts': [ + f'trace = {package_name}.trace:main', + ], + }, + license='TODO', + tests_require=['pytest'], +) diff --git a/tracetools_trace/test/test_copyright.py b/tracetools_trace/test/test_copyright.py new file mode 100644 index 0000000..cf0fae3 --- /dev/null +++ b/tracetools_trace/test/test_copyright.py @@ -0,0 +1,23 @@ +# Copyright 2017 Open Source Robotics Foundation, Inc. +# +# 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. + +from ament_copyright.main import main +import pytest + + +@pytest.mark.copyright +@pytest.mark.linter +def test_copyright(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found errors' diff --git a/tracetools_trace/test/test_flake8.py b/tracetools_trace/test/test_flake8.py new file mode 100644 index 0000000..eff8299 --- /dev/null +++ b/tracetools_trace/test/test_flake8.py @@ -0,0 +1,23 @@ +# Copyright 2017 Open Source Robotics Foundation, Inc. +# +# 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. + +from ament_flake8.main import main +import pytest + + +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8(): + rc = main(argv=[]) + assert rc == 0, 'Found errors' diff --git a/tracetools_trace/test/test_pep257.py b/tracetools_trace/test/test_pep257.py new file mode 100644 index 0000000..3aeb4d3 --- /dev/null +++ b/tracetools_trace/test/test_pep257.py @@ -0,0 +1,23 @@ +# Copyright 2015 Open Source Robotics Foundation, Inc. +# +# 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. + +from ament_pep257.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.pep257 +def test_pep257(): + rc = main(argv=[]) + assert rc == 0, 'Found code style errors / warnings' diff --git a/tracetools_trace/tracetools_trace/__init__.py b/tracetools_trace/tracetools_trace/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tracetools_trace/tracetools_trace/tools/__init__.py b/tracetools_trace/tracetools_trace/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tracetools_trace/tracetools_trace/tools/args.py b/tracetools_trace/tracetools_trace/tools/args.py new file mode 100644 index 0000000..5959f8c --- /dev/null +++ b/tracetools_trace/tracetools_trace/tools/args.py @@ -0,0 +1,51 @@ +import argparse +import time + +from . import names + + +class DefaultArgValueCompleter: + """Callable returning an arg's default value.""" + + def __init__(self, arg): + default = arg.default + self.list = default if isinstance(default, list) else [default] + + def __call__(self, **kwargs): + return self.list + + +def parse_args(): + """ + Parse args for tracing. + """ + parser = argparse.ArgumentParser(description='Setup and launch an LTTng tracing session.') + add_arguments(parser) + return parser.parse_args() + + +def add_arguments(parser): + parser.add_argument( + '--session-name', '-s', dest='session_name', + default=f'session-{time.strftime("%Y%m%d%H%M%S")}', + help='the name of the tracing session (default: session-YYYYMMDDHHMMSS)') + parser.add_argument( + '--path', '-p', dest='path', + default='/tmp', + help='path of the base directory for trace data (default: %(default)s)') + arg = parser.add_argument( + '--ust', '-u', nargs='*', dest='events_ust', default=names.DEFAULT_EVENTS_ROS, + help='the ROS UST events to enable (default: all events) ' + '[to disable all UST events, ' + 'provide this flag without any event name]') + arg.completer = DefaultArgValueCompleter(arg) + arg = parser.add_argument( + '--kernel', '-k', nargs='*', dest='events_kernel', + default=names.DEFAULT_EVENTS_KERNEL, + help='the kernel events to enable (default: all events) ' + '[to disable all UST events, ' + 'provide this flag without any event name]') + arg.completer = DefaultArgValueCompleter(arg) + parser.add_argument( + '--list', '-l', dest='list', action='store_true', + help='display lists of enabled events (default: %(default)s)') diff --git a/tracetools_trace/tracetools_trace/tools/lttng.py b/tracetools_trace/tracetools_trace/tools/lttng.py new file mode 100644 index 0000000..6c86b37 --- /dev/null +++ b/tracetools_trace/tracetools_trace/tools/lttng.py @@ -0,0 +1,278 @@ +# LTTng tracing interface + +import sys + +# Temporary workaround +sys.path = ['/usr/local/lib/python3.6/site-packages'] + sys.path +from typing import List + +import lttng # noqa: E402 + +from .names import ( # noqa: E402 + DEFAULT_CONTEXT, + DEFAULT_EVENTS_KERNEL, + DEFAULT_EVENTS_ROS, +) + + +def lttng_init( + session_name: str, + full_path: str, + ros_events: List[str] = DEFAULT_EVENTS_ROS, + kernel_events: List[str] = DEFAULT_EVENTS_KERNEL, + context_names: List[str] = DEFAULT_CONTEXT +) -> None: + """ + Set up and start LTTng session. + + :param session_name: the name of the session + :param full_path: the full path to the main directory to write trace data to + :param ros_events: list of ROS events to enable + :param kernel_events: list of kernel events to enable + :param context_names: list of context elements to enable + """ + _lttng_setup(session_name, full_path, ros_events, kernel_events, context_names) + _lttng_start(session_name) + + +def lttng_fini(session_name: str) -> None: + """ + Stop and destroy LTTng session. + + :param session_name: the name of the session + """ + _lttng_stop(session_name) + _lttng_destroy(session_name) + + +def _lttng_setup( + session_name: str, + full_path: str, + ros_events: List[str] = DEFAULT_EVENTS_ROS, + kernel_events: List[str] = DEFAULT_EVENTS_KERNEL, + context_names: List[str] = DEFAULT_CONTEXT + ) -> None: + """ + Set up LTTng session, with events and context. + + :param session_name: the name of the session + :param full_path: the full path to the main directory to write trace data to + :param ros_events: list of ROS events to enable + :param kernel_events: list of kernel events to enable + :param context_names: list of context elements to enable + """ + ust_enabled = ros_events is not None and len(ros_events) > 0 + kernel_enabled = kernel_events is not None and len(kernel_events) > 0 + + # Domains + if ust_enabled: + domain_ust = lttng.Domain() + domain_ust.type = lttng.DOMAIN_UST + domain_ust.buf_type = lttng.BUFFER_PER_UID + channel_ust = lttng.Channel() + channel_ust.name = 'ros2' + channel_ust.attr.overwrite = 0 + channel_ust.attr.subbuf_size = 2 * 4096 + channel_ust.attr.num_subbuf = 8 + channel_ust.attr.switch_timer_interval = 0 + channel_ust.attr.read_timer_interval = 200 + channel_ust.attr.output = lttng.EVENT_MMAP + events_list_ust = _create_events(ros_events) + if kernel_enabled: + domain_kernel = lttng.Domain() + domain_kernel.type = lttng.DOMAIN_KERNEL + domain_kernel.buf_type = lttng.BUFFER_GLOBAL + channel_kernel = lttng.Channel() + channel_kernel.name = 'kchan' + channel_kernel.attr.overwrite = 0 + channel_kernel.attr.subbuf_size = 8 * 4096 + channel_kernel.attr.num_subbuf = 8 + channel_kernel.attr.switch_timer_interval = 0 + channel_kernel.attr.read_timer_interval = 200 + channel_kernel.attr.output = lttng.EVENT_MMAP + events_list_kernel = _create_events(kernel_events) + + # Session + _create_session(session_name, full_path) + + # Handles, channels, events + handle_ust = None + if ust_enabled: + handle_ust = _create_handle(session_name, domain_ust) + _enable_channel(handle_ust, channel_ust) + _enable_events(handle_ust, events_list_ust, channel_ust.name) + handle_kernel = None + if kernel_enabled: + handle_kernel = _create_handle(session_name, domain_kernel) + _enable_channel(handle_kernel, channel_kernel) + _enable_events(handle_kernel, events_list_kernel, channel_kernel.name) + + # Context + context_list = _create_context_list(context_names) + enabled_handles = [h for h in [handle_ust, handle_kernel] if h is not None] + _add_context(enabled_handles, context_list) + + +def _lttng_start(session_name: str) -> None: + """ + Start LTTng session, and check for errors. + + :param session_name: the name of the session + """ + result = lttng.start(session_name) + if result < 0: + raise RuntimeError(f'failed to start tracing: {lttng.strerror(result)}') + + +def _lttng_stop(session_name: str) -> None: + """ + Stop LTTng session, and check for errors. + + :param session_name: the name of the session + """ + result = lttng.stop(session_name) + if result < 0: + raise RuntimeError(f'failed to stop tracing: {lttng.strerror(result)}') + + +def _lttng_destroy(session_name: str) -> None: + """ + Destroy LTTng session, and check for errors. + + :param session_name: the name of the session + """ + result = lttng.destroy(session_name) + if result < 0: + raise RuntimeError(f'failed to destroy tracing session: {lttng.strerror(result)}') + + +def _create_events(event_names_list: List[str]) -> List[lttng.Event]: + """ + Create events list from names. + + :param event_names_list: a list of names to create events for + :return: the list of events + """ + events_list = [] + for event_name in event_names_list: + e = lttng.Event() + e.name = event_name + e.type = lttng.EVENT_TRACEPOINT + e.loglevel_type = lttng.EVENT_LOGLEVEL_ALL + events_list.append(e) + return events_list + + +def _create_session(session_name: str, full_path: str) -> None: + """ + Create session from name and full directory path, and check for errors. + + :param session_name: the name of the session + :param full_path: the full path to the main directory to write trace data to + """ + result = lttng.create(session_name, full_path) + LTTNG_ERR_EXIST_SESS = 28 + if result == -LTTNG_ERR_EXIST_SESS: + # Sessions seem to persist, so if it already exists, + # just destroy it and try again + lttng_destroy(session_name) + result = lttng.create(session_name, full_path) + if result < 0: + raise RuntimeError(f'session creation failed: {lttng.strerror(result)}') + + +def _create_handle(session_name: str, domain: lttng.Domain) -> lttng.Handle: + """ + Create a handle for a given session name and a domain, and check for errors. + + :param session_name: the name of the session + :param domain: the domain to be used + :return: the handle + """ + handle = None + handle = lttng.Handle(session_name, domain) + if handle is None: + raise RuntimeError('handle creation failed') + return handle + + +def _enable_channel(handle: lttng.Handle, channel: lttng.Channel) -> None: + """ + Enable channel for a handle, and check for errors. + + :param handle: the handle to be used + :param channel: the channel to enable + """ + result = lttng.enable_channel(handle, channel) + if result < 0: + raise RuntimeError(f'channel enabling failed: {lttng.strerror(result)}') + + +def _enable_events( + handle: lttng.Handle, + events_list: List[lttng.Event], + channel_name: str, +) -> None: + """ + Enable events list for a given handle and channel name, and check for errors. + + :param handle: the handle to be used + :param events_list: the list of events to enable + :param channel_name: the name of the channel to associate + """ + for event in events_list: + result = lttng.enable_event(handle, event, channel_name) + if result < 0: + raise RuntimeError(f'event enabling failed: {lttng.strerror(result)}') + + +context_map = { + 'procname': lttng.EVENT_CONTEXT_PROCNAME, + 'pid': lttng.EVENT_CONTEXT_PID, + 'vpid': lttng.EVENT_CONTEXT_VPID, + 'vtid': lttng.EVENT_CONTEXT_VTID, +} + + +def _context_name_to_type(context_name: str) -> int: + """ + Convert from context name to LTTng enum/constant type. + + :param context_name: the generic name for the context + :return: the associated type + """ + return context_map.get(context_name) + + +def _create_context_list(context_names_list: List[str]) -> List[lttng.EventContext]: + """ + Create context list from names, and check for errors. + + :param context_names_list: the list of context names + :return: the event context list + """ + context_list = [] + for c in context_names_list: + ec = lttng.EventContext() + context_type = _context_name_to_type(c) + if context_type is not None: + ec.ctx = context_type + context_list.append(ec) + return context_list + + +def _add_context( + handles: List[lttng.Handle], + context_list: List[lttng.EventContext], +) -> None: + """ + Add context list to given handles, and check for errors. + + :param handles: the list of handles for which to add context + :param context_list: the list of event contexts to add to the handles + """ + for handle in handles: + for contex in context_list: + result = lttng.add_context(handle, contex, None, None) + if result < 0: + raise RuntimeError(f'failed to add context: {lttng.strerror(result)}') diff --git a/tracetools_trace/tracetools_trace/tools/names.py b/tracetools_trace/tracetools_trace/tools/names.py new file mode 100644 index 0000000..bc7de37 --- /dev/null +++ b/tracetools_trace/tracetools_trace/tools/names.py @@ -0,0 +1,60 @@ +# Lists of names (events, context) + +DEFAULT_EVENTS_KERNEL = [ + 'block_rq_complete', + 'block_rq_insert', + 'block_rq_issue', + 'block_bio_frontmerge', + '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', + 'net_dev_queue', + 'netif_receive_skb', + 'net_if_receive_skb', + 'power_cpu_frequency', + 'sched_switch', + 'sched_waking', + 'sched_pi_setprio', + 'sched_process_fork', + 'sched_process_exit', + 'sched_process_free', + 'sched_wakeup', + 'sched_migrate', + 'sched_migrate_task', + 'timer_hrtimer_start', + 'timer_hrtimer_cancel', + 'timer_hrtimer_expire_entry', + 'timer_hrtimer_expire_exit', +] + +DEFAULT_EVENTS_ROS = [ + '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_callback_register', + 'ros2:callback_start', + 'ros2:callback_end', +] + +DEFAULT_CONTEXT = [ + 'procname', + 'perf:thread:instructions', + 'perf:thread:cycles', + 'perf:thread:cpu-cycles', + 'vpid', + 'vtid', +] diff --git a/tracetools_trace/tracetools_trace/trace.py b/tracetools_trace/tracetools_trace/trace.py new file mode 100644 index 0000000..16639dc --- /dev/null +++ b/tracetools_trace/tracetools_trace/trace.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# Entrypoint/script to setup and start an LTTng tracing session + +import os + +from tracetools_trace.tools import args +from tracetools_trace.tools import lttng +from tracetools_trace.tools import names + + +def main(): + params = args.parse_args() + + session_name = params.session_name + base_path = params.path + full_path = os.path.join(base_path, session_name) + ros_events = params.events_ust + kernel_events = params.events_kernel + + ust_enabled = len(ros_events) > 0 + kernel_enabled = len(kernel_events) > 0 + if ust_enabled: + print(f'UST tracing enabled ({len(ros_events)} events)') + if params.list: + print(f'\tevents: {ros_events}') + else: + print('UST tracing disabled') + if kernel_enabled: + print(f'kernel tracing enabled ({len(kernel_events)} events)') + if params.list: + print(f'\tevents: {kernel_events}') + else: + print('kernel tracing disabled') + + print(f'writting tracing session to: {full_path}') + input('press enter to start...') + lttng.lttng_init(session_name, full_path, ros_events=ros_events, kernel_events=kernel_events) + input('press enter to stop...') + + print('stopping & destroying tracing session') + lttng.lttng_fini(session_name)