diff --git a/tracetools_launch/test/test_copyright.py b/tracetools_launch/test/test_copyright.py new file mode 100644 index 0000000..cf0fae3 --- /dev/null +++ b/tracetools_launch/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_launch/test/test_flake8.py b/tracetools_launch/test/test_flake8.py new file mode 100644 index 0000000..eff8299 --- /dev/null +++ b/tracetools_launch/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_launch/test/test_pep257.py b/tracetools_launch/test/test_pep257.py new file mode 100644 index 0000000..0e38a6c --- /dev/null +++ b/tracetools_launch/test/test_pep257.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_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_launch/test/test_xmllint.py b/tracetools_launch/test/test_xmllint.py new file mode 100644 index 0000000..f46285e --- /dev/null +++ b/tracetools_launch/test/test_xmllint.py @@ -0,0 +1,23 @@ +# Copyright 2019 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_xmllint.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.xmllint +def test_xmllint(): + rc = main(argv=[]) + assert rc == 0, 'Found errors' diff --git a/tracetools_launch/test/tracetools_launch/test_trace_action.py b/tracetools_launch/test/tracetools_launch/test_trace_action.py new file mode 100644 index 0000000..9f27407 --- /dev/null +++ b/tracetools_launch/test/tracetools_launch/test_trace_action.py @@ -0,0 +1,57 @@ +# 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_launch.action import Trace + + +class TestTraceAction(unittest.TestCase): + + def __init__(self, *args) -> None: + super().__init__( + *args, + ) + + def test_has_profiling_events(self) -> None: + events_lists_match = [ + [ + 'lttng_ust_cyg_profile_fast:func_entry', + 'hashtag:yopo', + ], + [ + 'lttng_ust_cyg_profile:func_entry', + 'some_other_event', + 'lttng_ust_cyg_profile:func_exit', + ], + ] + events_lists_no_match = [ + [ + 'lttng_ust_statedump:bin_info', + 'ros2:event', + ], + [] + ] + for events in events_lists_match: + self.assertTrue(Trace.has_profiling_events(events)) + for events in events_lists_no_match: + self.assertFalse(Trace.has_profiling_events(events)) + + def test_get_shared_lib_path(self) -> None: + # Only test not finding a lib for now + self.assertIsNone(Trace.get_shared_lib_path('random_lib_that_does_not_exist_I_hope.so')) + + +if __name__ == '__main__': + unittest.main() diff --git a/tracetools_launch/tracetools_launch/action.py b/tracetools_launch/tracetools_launch/action.py index 8ec3419..ce46a80 100644 --- a/tracetools_launch/tracetools_launch/action.py +++ b/tracetools_launch/tracetools_launch/action.py @@ -14,11 +14,14 @@ """Module for the Trace action.""" -import os +import re +import subprocess from typing import List from typing import Optional +from typing import Union from launch.action import Action +from launch.actions import SetEnvironmentVariable from launch.event import Event from launch.event_handlers import OnShutdown from launch.launch_context import LaunchContext @@ -34,6 +37,10 @@ class Trace(Action): Sets up and enables tracing through a launch file description. """ + PROFILE_LIB_NORMAL = 'liblttng-ust-cyg-profile.so' + PROFILE_LIB_FAST = 'liblttng-ust-cyg-profile-fast.so' + PROFILE_EVENT_PATTERN = '^lttng_ust_cyg_profile.*:func_.*' + def __init__( self, *, @@ -42,6 +49,7 @@ class Trace(Action): base_path: str = path.DEFAULT_BASE_PATH, events_ust: List[str] = names.DEFAULT_EVENTS_ROS, events_kernel: List[str] = names.DEFAULT_EVENTS_KERNEL, + profile_fast: bool = True, **kwargs, ) -> None: """ @@ -49,9 +57,10 @@ class Trace(Action): :param session_name: the name of the tracing session :param append_timestamp: whether to append timestamp to the session name - :param base_path: the path to the base directory in which to create the tracing session directory + :param base_path: the path to the base directory in which to create the session directory :param events_ust: the list of ROS UST events to enable :param events_kernel: the list of kernel events to enable + :param profile_fast: `True` to use fast profiling, `False` for normal (only if necessary) """ super().__init__(**kwargs) if append_timestamp: @@ -60,12 +69,47 @@ class Trace(Action): self.__base_path = base_path self.__events_ust = events_ust self.__events_kernel = events_kernel + self.__profile_fast = profile_fast + self.__ld_preload_action = None + if self.has_profiling_events(events_ust): + profile_lib_name = self.PROFILE_LIB_FAST if profile_fast else self.PROFILE_LIB_NORMAL + self.__ld_preload_action = SetEnvironmentVariable( + 'LD_PRELOAD', + self.get_shared_lib_path(profile_lib_name), + ) + + @classmethod + def has_profiling_events(cls, events_ust: List[str]) -> bool: + """Check if the UST events list contains at least one profiling event.""" + return any(re.match(cls.PROFILE_EVENT_PATTERN, event_name) for event_name in events_ust) + + @staticmethod + def get_shared_lib_path(lib_name: str) -> Union[str, None]: + """ + Get the full path to a given shared lib, if possible. + + :param lib_name: the name of the shared library + :return: the full path if found, `None` otherwise + """ + (exit_code, output) = subprocess.getstatusoutput(f'whereis -b {lib_name}') + if exit_code != 0: + return None + # Output of whereis is + # : + # Filter out empty strings, in case lib is not found + output_split = [split_part for split_part in output.split(':') if len(split_part) > 0] + if len(output_split) != 2: + return None + return output_split[1].strip() def execute(self, context: LaunchContext) -> Optional[List[Action]]: # TODO make sure this is done as late as possible context.register_event_handler(OnShutdown(on_shutdown=self._destroy)) # TODO make sure this is done as early as possible self._setup() + if self.__ld_preload_action is not None: + return [self.__ld_preload_action] + return None def _setup(self) -> None: lttng.lttng_init( @@ -79,9 +123,11 @@ class Trace(Action): def __repr__(self): return ( - "Trace(" - f"session_name='{self.__session_name}', " - f"base_path='{self.__base_path}', " - f"num_events_ust={len(self.__events_ust)}, " - f"num_events_kernel={len(self.__events_kernel)})" + 'Trace(' + f'session_name={self.__session_name}, ' + f'base_path={self.__base_path}, ' + f'num_events_ust={len(self.__events_ust)}, ' + f'num_events_kernel={len(self.__events_kernel)}), ' + f'profiling={self.__ld_preload_action is not None}, ' + f'profile_fast={self.__profile_fast}' )