Commit 514ca46d authored by Ingo Lütkebohle's avatar Ingo Lütkebohle

Merge branch 'analysis' into 'master'

Add basic analysis tools

See merge request ros_tracing/tracetools_analysis!1
parents e5c64669 d9be91dc
Pipeline #69839341 failed with stage
in 16 seconds
image: registry.gitlab.com/ros_tracing/ros2_tracing/ci-base:latest
variables:
DOCKER_DRIVER: overlay2
PACKAGES_LIST: tracetools_analysis
before_script:
- git clone https://gitlab.com/ros_tracing/ros2_tracing.git
build:
script:
- colcon build --symlink-install --packages-up-to $PACKAGES_LIST
- colcon test --packages-select $PACKAGES_LIST
- colcon test-result
artifacts:
paths:
- install
- build/*/test_results/*/*.xunit.xml
- build/*/pytest.xml
reports:
junit:
- build/*/test_results/*/*.xunit.xml
- build/*/pytest.xml
This diff is collapsed.
# tracetools_analysis
Analysis tools for [ROS 2 tracing](https://gitlab.com/ros_tracing/ros2_tracing).
# Setup
To display results, install:
* [Jupyter](https://jupyter.org/install)
* [Bokeh](https://bokeh.pydata.org/en/latest/docs/user_guide/quickstart.html#userguide-quickstart-install)
# Use
Start Jupyter Notebook:
```
$ jupyter notebook
```
Then navigate to the [`analysis/`](./tracetools_analysis/analysis/) directory, and select one of the provided notebooks, or create your own!
*.svg
*.png
*.pdf
.ipynb_checkpoints
This diff is collapsed.
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="2">
<name>tracetools_analysis</name>
<version>0.0.1</version>
<description>Tools for analysing trace data</description>
<maintainer email="fixed-term.christophe.bourquebedard@de.bosch.com">Christophe Bedard</maintainer>
<maintainer email="ingo.luetkebohle@de.bosch.com">Ingo Lütkebohle</maintainer>
<license>Apache 2.0</license>
<author email="ingo.luetkebohle@de.bosch.com">Ingo Luetkebohle</author>
<author email="fixed-term.christophe.bourquebedard@de.bosch.com">Christophe Bedard</author>
<exec_depend>tracetools_read</exec_depend>
<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>
[develop]
script-dir=$base/lib/tracetools_analysis
[install]
install-scripts=$base/lib/tracetools_analysis
from setuptools import find_packages
from setuptools import setup
package_name = 'tracetools_analysis'
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, '
'Ingo Lütkebohle'
),
maintainer_email=(
'fixed-term.christophe.bourquebedard@de.bosch.com, '
'ingo.luetkebohle@de.bosch.com'
),
author=(
'Christophe Bedard, '
'Ingo Lütkebohle'
),
author_email=(
'fixed-term.christophe.bourquebedard@de.bosch.com, '
'ingo.luetkebohle@de.bosch.com'
),
# url='',
keywords=['ROS'],
description='Tools for analysing trace data',
entry_points={
'console_scripts': [
f'convert = {package_name}.convert:main',
f'process = {package_name}.process:main',
],
},
license='Apache 2.0',
tests_require=['pytest'],
)
# 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'
# 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'
# 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'
# 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.
"""Reading and interpreting of LTTng trace data."""
__author__ = 'Luetkebohle Ingo (CR/AEX3)'
# 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.
# 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.
"""Module for data model."""
import pandas as pd
class DataModel():
"""
Container to model pre-processed data for analysis.
Contains data for an analysis to use. This is a middleground between trace events data and the
output data of an analysis. This aims to represent the data in a ROS-aware way.
It uses pandas DataFrames directly.
"""
def __init__(self) -> None:
# Objects (one-time events, usually when something is created)
self.contexts = pd.DataFrame(columns=['context_handle',
'timestamp',
'pid',
'version'])
self.contexts.set_index(['context_handle'], inplace=True, drop=True)
self.nodes = pd.DataFrame(columns=['node_handle',
'timestamp',
'tid',
'rmw_handle',
'name',
'namespace'])
self.nodes.set_index(['node_handle'], inplace=True, drop=True)
self.publishers = pd.DataFrame(columns=['publisher_handle',
'timestamp',
'node_handle',
'rmw_handle',
'topic_name',
'depth'])
self.publishers.set_index(['publisher_handle'], inplace=True, drop=True)
self.subscriptions = pd.DataFrame(columns=['subscription_handle',
'timestamp',
'node_handle',
'rmw_handle',
'topic_name',
'depth'])
self.subscriptions.set_index(['subscription_handle'], inplace=True, drop=True)
self.services = pd.DataFrame(columns=['service_handle',
'timestamp',
'node_handle',
'rmw_handle',
'service_name'])
self.services.set_index(['service_handle'], inplace=True, drop=True)
self.clients = pd.DataFrame(columns=['client_handle',
'timestamp',
'node_handle',
'rmw_handle',
'service_name'])
self.clients.set_index(['client_handle'], inplace=True, drop=True)
self.timers = pd.DataFrame(columns=['timer_handle',
'timestamp',
'period',
'tid'])
self.timers.set_index(['timer_handle'], inplace=True, drop=True)
self.callback_objects = pd.DataFrame(columns=['handle',
'timestamp',
'callback_object'])
self.callback_objects.set_index(['handle'], inplace=True, drop=True)
self.callback_symbols = pd.DataFrame(columns=['callback_object',
'timestamp',
'symbol'])
self.callback_symbols.set_index(['callback_object'], inplace=True, drop=True)
# Events (multiple instances, may not have a meaningful index)
self.callback_instances = pd.DataFrame(columns=['callback_object',
'timestamp',
'duration',
'intra_process'])
def add_context(
self, context_handle, timestamp, pid, version
) -> None:
self.contexts.loc[context_handle] = [timestamp, pid, version]
def add_node(
self, node_handle, timestamp, tid, rmw_handle, name, namespace
) -> None:
self.nodes.loc[node_handle] = [timestamp, tid, rmw_handle, name, namespace]
def add_publisher(
self, handle, timestamp, node_handle, rmw_handle, topic_name, depth
) -> None:
self.publishers.loc[handle] = [timestamp, node_handle, rmw_handle, topic_name, depth]
def add_subscription(
self, handle, timestamp, node_handle, rmw_handle, topic_name, depth
) -> None:
self.subscriptions.loc[handle] = [timestamp, node_handle, rmw_handle, topic_name, depth]
def add_service(
self, handle, timestamp, node_handle, rmw_handle, service_name
) -> None:
self.services.loc[handle] = [timestamp, node_handle, rmw_handle, service_name]
def add_client(
self, handle, timestamp, node_handle, rmw_handle, service_name
) -> None:
self.clients.loc[handle] = [timestamp, node_handle, rmw_handle, service_name]
def add_timer(
self, handle, timestamp, period, tid
) -> None:
self.timers.loc[handle] = [timestamp, period, tid]
def add_callback_object(
self, handle, timestamp, callback_object
) -> None:
self.callback_objects.loc[handle] = [timestamp, callback_object]
def add_callback_symbol(
self, callback_object, timestamp, symbol
) -> None:
self.callback_symbols.loc[callback_object] = [timestamp, symbol]
def add_callback_instance(
self, callback_object, timestamp, duration, intra_process
) -> None:
data = {
'callback_object': callback_object,
'timestamp': timestamp,
'duration': duration,
'intra_process': intra_process,
}
self.callback_instances = self.callback_instances.append(data, ignore_index=True)
def print_model(self) -> None:
"""Debug method to print every contained df."""
print('====================DATA MODEL====================')
print(f'Contexts:\n{self.contexts.to_string()}')
print()
print(f'Nodes:\n{self.nodes.to_string()}')
print()
print(f'Publishers:\n{self.publishers.to_string()}')
print()
print(f'Subscriptions:\n{self.subscriptions.to_string()}')
print()
print(f'Services:\n{self.services.to_string()}')
print()
print(f'Clients:\n{self.clients.to_string()}')
print()
print(f'Timers:\n{self.timers.to_string()}')
print()
print(f'Callback objects:\n{self.callback_objects.to_string()}')
print()
print(f'Callback symbols:\n{self.callback_symbols.to_string()}')
print()
print(f'Callback instances:\n{self.callback_instances.to_string()}')
print('==================================================')
# 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.
"""Module for event handler."""
import sys
from typing import Callable
from typing import Dict
from typing import List
from tracetools_read.utils import get_event_name
from tracetools_read.utils import get_field
from .lttng_models import EventMetadata
class EventHandler():
"""Base event handling class."""
def __init__(self, handler_map: Dict[str, Callable[[Dict, EventMetadata], None]]) -> None:
"""
Constructor.
:param handler_map: the mapping from event name to handling method
"""
self._handler_map = handler_map
def handle_events(self, events: List[Dict[str, str]]) -> None:
"""
Handle events by calling their handlers.
:param events: the events to process
"""
for event in events:
self._handle(event)
def _handle(self, event: Dict[str, str]) -> None:
event_name = get_event_name(event)
handler_function = self._handler_map.get(event_name, None)
if handler_function is not None:
pid = get_field(
event,
'vpid',
default=get_field(
event,
'pid',
raise_if_not_found=False))
tid = get_field(
event,
'vtid',
default=get_field(
event,
'tid',
raise_if_not_found=False))
timestamp = get_field(event, '_timestamp')
procname = get_field(event, 'procname')
metadata = EventMetadata(event_name, pid, tid, timestamp, procname)
handler_function(event, metadata)
else:
print(f'unhandled event name: {event_name}', file=sys.stderr)
# 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.
"""Module for pickle loading."""
import pickle
from typing import Dict
from typing import List
def load_pickle(pickle_file_path: str) -> List[Dict]:
"""
Load pickle file containing converted trace events.
:param pickle_file_path: the path to the pickle file to load
:return: the list of events read from the file
"""
events = []
with open(pickle_file_path, 'rb') as f:
p = pickle.Unpickler(f)
while True:
try:
events.append(p.load())
except EOFError:
break # we're done
return events
# 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.
"""Module LTTng traces/events models."""
class EventMetadata():
"""Container for event metadata."""
def __init__(self, event_name, pid, tid, timestamp, procname) -> None:
self._event_name = event_name
self._pid = pid
self._tid = tid
self._timestamp = timestamp
self._procname = procname
@property
def event_name(self):
return self._event_name
@property
def pid(self):
return self._pid
@property
def tid(self):
return self._tid
@property
def timestamp(self):
return self._timestamp
@property
def procname(self):
return self._procname
# 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.
"""Module for trace events processor and ROS model creation."""
from typing import Dict
from typing import List
from tracetools_read.utils import get_field
from .data_model import DataModel
from .handler import EventHandler
from .lttng_models import EventMetadata
class Ros2Processor(EventHandler):
"""
ROS 2-aware event processing/handling class implementation.
Handles a trace's events and builds a model with the data.
"""
def __init__(self) -> None:
# Link a ROS trace event to its corresponding handling method
handler_map = {
'ros2:rcl_init':
self._handle_rcl_init,
'ros2:rcl_node_init':
self._handle_rcl_node_init,
'ros2:rcl_publisher_init':
self._handle_rcl_publisher_init,
'ros2:rcl_subscription_init':
self._handle_subscription_init,
'ros2:rclcpp_subscription_callback_added':
self._handle_rclcpp_subscription_callback_added,
'ros2:rcl_service_init':
self._handle_rcl_service_init,
'ros2:rclcpp_service_callback_added':
self._handle_rclcpp_service_callback_added,
'ros2:rcl_client_init':
self._handle_rcl_client_init,
'ros2:rcl_timer_init':
self._handle_rcl_timer_init,
'ros2:rclcpp_timer_callback_added':
self._handle_rclcpp_timer_callback_added,
'ros2:rclcpp_callback_register':
self._handle_rclcpp_callback_register,
'ros2:callback_start':
self._handle_callback_start,
'ros2:callback_end':
self._handle_callback_end,
}
super().__init__(handler_map)
self._data = DataModel()
# Temporary buffers
self._callback_instances = {}
def get_data_model(self) -> DataModel:
return self._data
def _handle_rcl_init(
self, event: Dict, metadata: EventMetadata
) -> None:
context_handle = get_field(event, 'context_handle')
timestamp = metadata.timestamp
pid = metadata.pid
version = get_field(event, 'version')
self._data.add_context(context_handle, timestamp, pid, version)
def _handle_rcl_node_init(
self, event: Dict, metadata: EventMetadata
) -> None:
handle = get_field(event, 'node_handle')
timestamp = metadata.timestamp
tid = metadata.tid
rmw_handle = get_field(event, 'rmw_handle')
name = get_field(event, 'node_name')
namespace = get_field(event, 'namespace')
self._data.add_node(handle, timestamp, tid, rmw_handle, name, namespace)
def _handle_rcl_publisher_init(
self, event: Dict, metadata: EventMetadata
) -> None:
handle = get_field(event, 'publisher_handle')
timestamp = metadata.timestamp
node_handle = get_field(event, 'node_handle')
rmw_handle = get_field(event, 'rmw_publisher_handle')
topic_name = get_field(event, 'topic_name')
depth = get_field(event, 'queue_depth')
self._data.add_publisher(handle, timestamp, node_handle, rmw_handle, topic_name, depth)
def _handle_subscription_init(
self, event: Dict, metadata: EventMetadata
) -> None:
handle = get_field(event, 'subscription_handle')
timestamp = metadata.timestamp
node_handle = get_field(event, 'node_handle')
rmw_handle = get_field(event, 'rmw_subscription_handle')
topic_name = get_field(event, 'topic_name')
depth = get_field(event, 'queue_depth')
self._data.add_subscription(handle, timestamp, node_handle, rmw_handle, topic_name, depth)
def _handle_rclcpp_subscription_callback_added(
self, event: Dict, metadata: EventMetadata
) -> None:
handle = get_field(event, 'subscription_handle')
timestamp = metadata.timestamp
callback_object = get_field(event, 'callback')
self._data.add_callback_object(handle, timestamp, callback_object)
def _handle_rcl_service_init(
self, event: Dict, metadata: EventMetadata
) -> None:
handle = get_field(event, 'service_handle')
timestamp = metadata.timestamp
node_handle = get_field(event, 'node_handle')