diff --git a/test_ros2trace/package.xml b/test_ros2trace/package.xml index 73125ed2..7757b617 100644 --- a/test_ros2trace/package.xml +++ b/test_ros2trace/package.xml @@ -18,6 +18,7 @@ ament_xmllint launch launch_ros + lttngpy python3-pytest ros2run ros2trace diff --git a/test_ros2trace/test/test_ros2trace/test_trace.py b/test_ros2trace/test/test_ros2trace/test_trace.py index 43a3e9a4..d2e3f71c 100644 --- a/test_ros2trace/test/test_ros2trace/test_trace.py +++ b/test_ros2trace/test/test_ros2trace/test_trace.py @@ -25,6 +25,7 @@ from launch import LaunchDescription from launch import LaunchService from launch_ros.actions import Node +from lttngpy import impl as lttngpy from tracetools_trace.tools import tracepoints from tracetools_trace.tools.lttng import is_lttng_installed @@ -63,30 +64,26 @@ def tearDown(self) -> None: # Even if running 'ros2 trace' fails, we do not want any lingering tracing session self.assertNoTracingSession() - def get_tracing_sessions(self) -> Tuple[bool, str]: - output = self.run_lttng_list() - # If there is no session daemon, then there are no tracing sessions - no_session_daemon_available = 'No session daemon is available' in output - if no_session_daemon_available: - return False, output - # Starting from LTTng 2.13, 'tracing session' was replaced with 'recording session' - # (see lttng-tools e971184) - has_tracing_sessions = not any( - f'Currently no available {name} session' in output for name in ('tracing', 'recording') - ) - return has_tracing_sessions, output - def assertTracingSession(self) -> None: - has_tracing_sessions, output = self.get_tracing_sessions() - self.assertTrue(has_tracing_sessions, 'no tracing sessions exist:\n' + output) + self.assertTrue( + lttngpy.is_lttng_session_daemon_alive(), + 'no tracing sessions exist because there is no daemon', + ) + session_names = lttngpy.get_session_names() + has_tracing_sessions = session_names is not None and 0 < len(session_names) + self.assertTrue(has_tracing_sessions, 'no tracing sessions exist') def assertNoTracingSession(self) -> None: - has_tracing_sessions, output = self.get_tracing_sessions() - if has_tracing_sessions: + # If there is no session daemon, then there are no tracing sessions + if not lttngpy.is_lttng_session_daemon_alive(): + return + session_names = lttngpy.get_session_names() + no_tracing_sessions = 0 == len(session_names) + if not no_tracing_sessions: # Destroy tracing sessions if there are any, this way we can continue running tests and # avoid possible interference between them - self.run_lttng_destroy_all() - self.assertFalse(has_tracing_sessions, 'tracing session(s) exist:\n' + output) + self.assertEqual(0, lttngpy.destroy_all_sessions()) + self.assertTrue(no_tracing_sessions, f'tracing session(s) exist: {session_names}') def assertTraceExist(self, trace_dir: str) -> None: self.assertTrue(os.path.isdir(trace_dir), f'trace directory does not exist: {trace_dir}') @@ -116,25 +113,6 @@ def create_test_tmpdir(self, test_name: str) -> str: def get_subdirectories(self, directory: str) -> List[str]: return [f.name for f in os.scandir(directory) if f.is_dir()] - def run_lttng_list(self) -> str: - process = subprocess.run( - ['lttng', 'list'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - encoding='utf-8', - ) - return process.stdout + process.stderr - - def run_lttng_destroy_all(self): - process = subprocess.run( - ['lttng', 'destroy', '--all'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - encoding='utf-8', - ) - output = process.stdout + process.stderr - self.assertEqual(0, process.returncode, f"'lttng destroy' command failed: {output}") - def run_command( self, args: List[str], @@ -370,6 +348,17 @@ def test_base_path_not_exist(self) -> None: shutil.rmtree(tmpdir) + def test_no_events(self) -> None: + tmpdir = self.create_test_tmpdir('test_no_events') + + # Enabling no events should result in an error + ret = self.run_trace_command( + ['--path', tmpdir, '--ust', '--kernel'], + ) + self.assertEqual(1, ret) + + shutil.rmtree(tmpdir) + def test_unknown_context_field(self) -> None: tmpdir = self.create_test_tmpdir('test_unknown_context_field') diff --git a/tracetools_trace/package.xml b/tracetools_trace/package.xml index c7e02d62..53b55e59 100644 --- a/tracetools_trace/package.xml +++ b/tracetools_trace/package.xml @@ -12,8 +12,7 @@ https://github.com/ros2/ros2_tracing/issues Christophe Bedard - lttng-tools - python3-lttng + lttngpy ament_copyright ament_flake8 diff --git a/tracetools_trace/test/tracetools_trace/test_lttng_tracing.py b/tracetools_trace/test/tracetools_trace/test_lttng_tracing.py index 63992e3e..312fd793 100644 --- a/tracetools_trace/test/tracetools_trace/test_lttng_tracing.py +++ b/tracetools_trace/test/tracetools_trace/test_lttng_tracing.py @@ -13,7 +13,6 @@ # limitations under the License. import os -import subprocess import tempfile import unittest from unittest import mock @@ -35,32 +34,7 @@ def test_is_lttng_installed(self): with mock.patch('platform.system', return_value='Windows'): self.assertFalse(is_lttng_installed()) - # LTTng command not found - class PopenFileNotFound: - - def __init__(self, *args, **kwargs): - raise FileNotFoundError('file not found') - - with mock.patch.object(subprocess, 'Popen', PopenFileNotFound): - self.assertFalse(is_lttng_installed()) - - # Other error when running LTTng command - class PopenReturnCodeError: - - def __init__(self, *args, **kwargs): - pass - - def communicate(self): - return 'stdout'.encode(), 'stderr'.encode() - - @property - def returncode(self): - return 1 - - with mock.patch.object(subprocess, 'Popen', PopenReturnCodeError): - self.assertFalse(is_lttng_installed()) - - # lttng Python package or version not found + # lttng-ctl or version not found with mock.patch('tracetools_trace.tools.lttng.get_lttng_version', return_value=None): self.assertFalse(is_lttng_installed()) @@ -81,11 +55,18 @@ def test_lttng_not_installed(self): def test_no_kernel_tracer(self): from tracetools_trace.tools.lttng_impl import setup with ( + mock.patch( + 'tracetools_trace.tools.lttng_impl.is_session_daemon_not_alive', + return_value=False, + ), + mock.patch( + 'tracetools_trace.tools.lttng_impl.is_session_daemon_unreachable', + return_value=False, + ), mock.patch( 'tracetools_trace.tools.lttng_impl.is_kernel_tracer_available', - return_value=(False, 'some error message'), + return_value=False, ), - mock.patch('lttng.session_daemon_alive', return_value=1), ): with self.assertRaises(RuntimeError): setup( @@ -166,9 +147,15 @@ def test_is_session_daemon_unreachable(self): def test_unreachable_session_daemon(self): from tracetools_trace.tools.lttng_impl import setup - with mock.patch( - 'tracetools_trace.tools.lttng_impl.is_session_daemon_unreachable', - return_value=True, + with ( + mock.patch( + 'tracetools_trace.tools.lttng_impl.is_session_daemon_not_alive', + return_value=False, + ), + mock.patch( + 'tracetools_trace.tools.lttng_impl.is_session_daemon_unreachable', + return_value=True, + ), ): with self.assertRaises(RuntimeError): setup(session_name='test-session', base_path='/tmp') diff --git a/tracetools_trace/tracetools_trace/tools/lttng.py b/tracetools_trace/tracetools_trace/tools/lttng.py index 8e109ce8..cd67aff1 100644 --- a/tracetools_trace/tracetools_trace/tools/lttng.py +++ b/tracetools_trace/tracetools_trace/tools/lttng.py @@ -16,42 +16,30 @@ """Interface for tracing with LTTng.""" import platform -import subprocess import sys from typing import Optional from packaging.version import Version try: - from . import lttng_impl - _lttng = lttng_impl # type: ignore + from . import lttng_impl as _lttng except ImportError: # Fall back on stub functions so that this still passes linter checks - from . import lttng_stub - _lttng = lttng_stub # type: ignore + # This will happen if lttngpy isn't found, in which case importing lttng_impl will fail + from . import lttng_stub as _lttng # type: ignore def get_lttng_version() -> Optional[Version]: """ - Get version of lttng Python package. + Get version of lttng-ctl. - :return: the version of the lttng Python package, or `None` if it is not available + :return: the version of lttng-ctl, or `None` if it is not available """ if not hasattr(_lttng, 'get_version') or not callable(_lttng.get_version): return None return _lttng.get_version() -# Check lttng module version -current_version = get_lttng_version() -LTTNG_MIN_VERSION = '2.10.7' -if current_version is None or current_version < Version(LTTNG_MIN_VERSION): - print( - f'lttng module version >={LTTNG_MIN_VERSION} required, found {str(current_version)}', - file=sys.stderr, - ) - - def lttng_init(**kwargs) -> Optional[str]: """ Set up and start LTTng session. @@ -114,14 +102,13 @@ def is_lttng_installed( Check if LTTng is installed. It first checks if the OS can support LTTng. - If so, it then simply checks if LTTng is installed using the 'lttng' command, and checks if the - lttng Python package is installed (python3-lttng). + If so, it then checks if lttng-ctl is installed. - Optionally, a minimum version can also be specified for the lttng Python package. + Optionally, a minimum version can also be specified for lttng-ctl. - :param minimum_version: the minimum required lttng Python package version - :return: True if LTTng and the lttng Python package are installed, and optionally if the - version of lttng Python package is sufficient, False otherwise + :param minimum_version: the minimum required lttng-ctl version + :return: True if lttng-ctl is installed, and optionally if the version of lttng-ctl is + sufficient, False otherwise """ # Check system message_doc = ( @@ -132,32 +119,19 @@ def is_lttng_installed( if 'Linux' != system: print(f"System '{system}' does not support LTTng.\n{message_doc}", file=sys.stderr) return False - # Check if LTTng (CLI) is installed - try: - process = subprocess.Popen( - ['lttng', '--version'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - _, stderr = process.communicate() - if 0 != process.returncode: - raise RuntimeError(stderr.decode()) - except (RuntimeError, FileNotFoundError) as e: - print(f'LTTng not found: {e}\n{message_doc}', file=sys.stderr) - return False - # Check if lttng Python package is installed + # Check if lttng-ctl is installed lttng_version = get_lttng_version() if not lttng_version: print( - f'lttng Python package (python3-lttng) not installed\n{message_doc}', + f'lttng-ctl (liblttng-ctl-dev) not installed\n{message_doc}', file=sys.stderr, ) return False - # Check if lttng Python package version is sufficient + # Check if lttng-ctl version is sufficient if minimum_version and lttng_version < Version(minimum_version): print( ( - f'lttng Python package (python3-lttng) version >={minimum_version} required, ' + f'lttng-ctl (liblttng-ctl-dev) version >={minimum_version} required, ' f'found {str(lttng_version)}' ), file=sys.stderr, diff --git a/tracetools_trace/tracetools_trace/tools/lttng_impl.py b/tracetools_trace/tracetools_trace/tools/lttng_impl.py index 6b8377b7..9d5e38b6 100644 --- a/tracetools_trace/tracetools_trace/tools/lttng_impl.py +++ b/tracetools_trace/tracetools_trace/tools/lttng_impl.py @@ -16,60 +16,43 @@ """Implementation of the interface for tracing with LTTng.""" import os -import re import shlex import subprocess from typing import Dict from typing import List from typing import Optional from typing import Set -from typing import Tuple from typing import Union -import lttng +from lttngpy import impl as lttngpy from packaging.version import Version -from .names import CONTEXT_TYPE_CONSTANTS_MAP from .names import DEFAULT_CONTEXT from .names import DEFAULT_EVENTS_ROS +from .names import DOMAIN_TYPE_KERNEL +from .names import DOMAIN_TYPE_USERSPACE def get_version() -> Optional[Version]: """ - Get the version of the lttng module. - - The module does not have a __version__ attribute, but the version is mentioned in its __doc__, - and seems to be written in a consistent way across versions. + Get version of lttng-ctl. :return: the version as a Version object, or `None` if it cannot be extracted """ - doc_lines = str(lttng.__doc__).split('\n') - filtered_doc_lines: List[str] = list(filter(None, doc_lines)) - if len(filtered_doc_lines) == 0: - return None - first_line = filtered_doc_lines[0] - version_string = first_line.split(' ')[1] - if not re.compile(r'^[0-9]+\.[0-9]+\.[0-9]+$').match(version_string): + if not lttngpy.is_available(): return None - return Version(version_string) + return Version(lttngpy.LTTNG_CTL_VERSION) -def is_kernel_tracer_available() -> Tuple[bool, Optional[str]]: +def is_kernel_tracer_available() -> bool: """ Check if the kernel tracer is available. - Runs 'lttng list -k', which gives an error if the kernel tracer is not available. + This must not be called if `lttngpy.is_available()` is `False`. - :return: (`True` if available or `False` if not, stderr output if unavailable) + :return: `True` if available or `False` if not """ - process = subprocess.run( - ['lttng', 'list', '-k'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - if 0 != process.returncode: - return False, process.stderr.decode().strip('\n') - return True, None + return not isinstance(lttngpy.get_tracepoints(domain_type=lttngpy.LTTNG_DOMAIN_KERNEL), int) def get_lttng_home() -> Optional[str]: @@ -137,6 +120,17 @@ def is_session_daemon_unreachable() -> bool: return 1 == process.returncode or 'lttng-sessiond' != process.stdout.strip() +def is_session_daemon_not_alive() -> bool: + """ + Check if the session daemon isn't alive. + + This must not be called if `lttngpy.is_available()` is `False`. + + :return: `True` if the session daemon is not alive, or `False` if it is alive + """ + return not lttngpy.is_lttng_session_daemon_alive() + + def setup( *, session_name: str, @@ -158,6 +152,7 @@ def setup( Initialization will fail if the list of kernel events to be enabled is not empty and if the kernel tracer is not installed. + This must not be called if `lttngpy.is_available()` is `False`. Raises RuntimeError on failure, in which case the tracing session might still exist. :param session_name: the name of the session @@ -189,7 +184,7 @@ def setup( f'trace directory already exists, use the append option to append to it: {full_path}') # If there is no session daemon running, try to spawn one - if lttng.session_daemon_alive() == 0: + if is_session_daemon_not_alive(): subprocess.run( ['lttng-sessiond', '--daemonize'], ) @@ -202,22 +197,20 @@ def setup( 'directory exists. See: https://bugs.lttng.org/issues/1371' ) # Error out if there is still no session daemon - if lttng.session_daemon_alive() == 0: + if is_session_daemon_not_alive(): raise RuntimeError('failed to start lttng session daemon') # Make sure the kernel tracer is available if there are kernel events # Do this after spawning a session daemon, otherwise we can't detect the kernel tracer - if 0 < len(kernel_events): - kernel_tracer_available, message = is_kernel_tracer_available() - if not kernel_tracer_available: - raise RuntimeError( - f'kernel tracer is not available: {message}\n' - ' cannot use kernel events:\n' - " 'ros2 trace' command: cannot use '-k' option\n" - " 'Trace' action: cannot set 'events_kernel'/'events-kernel' list\n" - ' install the kernel tracer, e.g., on Ubuntu, install lttng-modules-dkms\n' - ' see: https://github.com/ros2/ros2_tracing#building' - ) + if 0 < len(kernel_events) and not is_kernel_tracer_available(): + raise RuntimeError( + 'kernel tracer is not available:\n' + ' cannot use kernel events:\n' + " 'ros2 trace' command: cannot use '-k' option\n" + " 'Trace' action: cannot set 'events_kernel'/'events-kernel' list\n" + ' install the kernel tracer, e.g., on Ubuntu, install lttng-modules-dkms\n' + ' see: https://github.com/ros2/ros2_tracing#building' + ) # Convert lists to sets if not isinstance(ros_events, set): @@ -229,65 +222,86 @@ def setup( 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 - # Per-user buffer - domain_ust.buf_type = lttng.BUFFER_PER_UID - channel_ust = lttng.Channel() - channel_ust.name = channel_name_ust - # Discard, do not overwrite - channel_ust.attr.overwrite = 0 - # We use 2 sub-buffers because the number of sub-buffers is pointless in discard mode, - # and switching between sub-buffers introduces noticeable CPU overhead - channel_ust.attr.subbuf_size = subbuffer_size_ust - channel_ust.attr.num_subbuf = 2 - # Ignore switch timer interval and use read timer instead - channel_ust.attr.switch_timer_interval = 0 - channel_ust.attr.read_timer_interval = 200 - # mmap channel output (only option for UST) - 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 - # Global buffer (only option for kernel domain) - domain_kernel.buf_type = lttng.BUFFER_GLOBAL - channel_kernel = lttng.Channel() - channel_kernel.name = channel_name_kernel - # Discard, do not overwrite - channel_kernel.attr.overwrite = 0 - channel_kernel.attr.subbuf_size = subbuffer_size_kernel - channel_kernel.attr.num_subbuf = 2 - # Ignore switch timer interval and use read timer instead - channel_kernel.attr.switch_timer_interval = 0 - channel_kernel.attr.read_timer_interval = 200 - # mmap channel output instead of splice - channel_kernel.attr.output = lttng.EVENT_MMAP - events_list_kernel = _create_events(kernel_events) + if not(ust_enabled or kernel_enabled): + raise RuntimeError('no events enabled') # Create session # LTTng will create the parent directories if needed - _create_session(session_name, full_path) + _create_session( + session_name=session_name, + full_path=full_path, + ) - # Handles, channels, events - handle_ust = None + # Enable channel, events, and contexts for each domain + contexts_dict = _normalize_contexts_dict(context_fields) 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 + domain = DOMAIN_TYPE_USERSPACE + domain_type = lttngpy.LTTNG_DOMAIN_UST + channel_name = channel_name_ust + _enable_channel( + session_name=session_name, + domain_type=domain_type, + # Per-user buffer + buffer_type=lttngpy.LTTNG_BUFFER_PER_UID, + channel_name=channel_name, + # Discard, do not overwrite + overwrite=0, + # We use 2 sub-buffers because the number of sub-buffers is pointless in discard mode, + # and switching between sub-buffers introduces noticeable CPU overhead + subbuf_size=subbuffer_size_ust, + num_subbuf=2, + # Ignore switch timer interval and use read timer instead + switch_timer_interval=0, + read_timer_interval=200, + # mmap channel output (only option for UST) + output=lttngpy.LTTNG_EVENT_MMAP, + ) + _enable_events( + session_name=session_name, + domain_type=domain_type, + channel_name=channel_name, + events=ros_events, + ) + _add_contexts( + session_name=session_name, + domain_type=domain_type, + channel_name=channel_name, + context_fields=contexts_dict.get(domain), + ) 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 - contexts_dict = _normalize_contexts_dict( - {'kernel': handle_kernel, 'userspace': handle_ust}, context_fields) - _add_context(contexts_dict) + domain = DOMAIN_TYPE_KERNEL + domain_type = lttngpy.LTTNG_DOMAIN_KERNEL + channel_name = channel_name_kernel + _enable_channel( + session_name=session_name, + domain_type=domain_type, + # Global buffer (only option for kernel domain) + buffer_type=lttngpy.LTTNG_BUFFER_GLOBAL, + channel_name=channel_name, + # Discard, do not overwrite + overwrite=0, + # We use 2 sub-buffers because the number of sub-buffers is pointless in discard mode, + # and switching between sub-buffers introduces noticeable CPU overhead + subbuf_size=subbuffer_size_kernel, + num_subbuf=2, + # Ignore switch timer interval and use read timer instead + switch_timer_interval=0, + read_timer_interval=200, + # mmap channel output instead of splice + output=lttngpy.LTTNG_EVENT_MMAP, + ) + _enable_events( + session_name=session_name, + domain_type=domain_type, + channel_name=channel_name, + events=kernel_events, + ) + _add_contexts( + session_name=session_name, + domain_type=domain_type, + channel_name=channel_name, + context_fields=contexts_dict.get(domain), + ) return full_path @@ -300,13 +314,14 @@ def start( """ Start LTTng session, and check for errors. + This must not be called if `lttngpy.is_available()` is `False`. Raises RuntimeError on failure to start. :param session_name: the name of the session """ - result = lttng.start(session_name) + result = lttngpy.lttng_start_tracing(session_name=session_name) if result < 0: - raise RuntimeError(f'failed to start tracing: {lttng.strerror(result)}') + raise RuntimeError(f'failed to start tracing: {lttngpy.lttng_strerror(result)}') def stop( @@ -318,14 +333,15 @@ def stop( """ Stop LTTng session, and check for errors. + This must not be called if `lttngpy.is_available()` is `False`. Raises RuntimeError on failure to stop, unless ignored. :param session_name: the name of the session :param ignore_error: whether to ignore any error when stopping """ - result = lttng.stop(session_name) + result = lttngpy.lttng_stop_tracing(session_name=session_name) if result < 0 and not ignore_error: - raise RuntimeError(f'failed to stop tracing: {lttng.strerror(result)}') + raise RuntimeError(f'failed to stop tracing: {lttngpy.lttng_strerror(result)}') def destroy( @@ -337,181 +353,106 @@ def destroy( """ Destroy LTTng session, and check for errors. - Raises RuntimeError on failure to stop, unless ignored. + This must not be called if `lttngpy.is_available()` is `False`. + Raises RuntimeError on failure to destroy, unless ignored. :param session_name: the name of the session :param ignore_error: whether to ignore any error when destroying """ - result = lttng.destroy(session_name) + result = lttngpy.lttng_destroy_session(session_name=session_name) if result < 0 and not ignore_error: - raise RuntimeError(f'failed to destroy tracing session: {lttng.strerror(result)}') - - -def _create_events( - event_names: Set[str], -) -> List[lttng.Event]: - """ - Create events list from names. - - :param event_names: a set of names to create events for - :return: the list of events - """ - events_list = [] - for event_name in event_names: - 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 + raise RuntimeError(f'failed to destroy tracing session: {lttngpy.lttng_strerror(result)}') def _create_session( + *, session_name: str, full_path: str, ) -> None: """ Create session from name and full directory path, and check for errors. + This must not be called if `lttngpy.is_available()` is `False`. + Raises RuntimeError on failure. + :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) - # See lttng-tools/include/lttng/lttng-error.h - if -28 == result: - # Sessions seem to persist, so if it already exists, - # just destroy it and try again + result = lttngpy.lttng_create_session( + session_name=session_name, + url=full_path, + ) + if -lttngpy.LTTNG_ERR_EXIST_SESS.value == result: + # Sessions may persist if there was an error previously, so if it already exists, just + # destroy it and try again destroy(session_name=session_name) - result = lttng.create(session_name, full_path) + result = lttngpy.lttng_create_session( + session_name=session_name, + url=full_path, + ) if result < 0: - raise RuntimeError(f'session creation failed: {lttng.strerror(result)}') - + raise RuntimeError(f'session creation failed: {lttngpy.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 +def _enable_channel(**kwargs) -> None: """ - handle = None - handle = lttng.Handle(session_name, domain) - if handle is None: - raise RuntimeError('handle creation failed') - return handle - + Enable channel, and check for errors. -def _enable_channel( - handle: lttng.Handle, - channel: lttng.Channel, -) -> None: - """ - Enable channel for a handle, and check for errors. + This must not be called if `lttngpy.is_available()` is `False`. + Raises RuntimeError on failure. - :param handle: the handle to be used - :param channel: the channel to enable + See `lttngpy.enable_channel` for kwargs. """ - result = lttng.enable_channel(handle, channel) + result = lttngpy.enable_channel(**kwargs) 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)}') + raise RuntimeError(f'channel enabling failed: {lttngpy.lttng_strerror(result)}') -context_map = { - name: getattr(lttng, name_constant, None) if name_constant is not None else None - for name, name_constant in CONTEXT_TYPE_CONSTANTS_MAP.items() -} - - -def _context_field_name_to_type( - context_field_name: str, -) -> Optional[int]: - """ - Convert from context name to LTTng enum/constant type. - - :param context_field_name: the generic name for the context field - :return: the associated type, or `None` if it cannot be found +def _enable_events(**kwargs) -> None: """ - return context_map.get(context_field_name, None) + Enable events for a given channel name, and check for errors. + This must not be called if `lttngpy.is_available()` is `False`. + Raises RuntimeError on failure. -def _create_context_list( - context_fields: Set[str], -) -> List[lttng.EventContext]: + See `lttngpy.enable_events` for kwargs. """ - Create context list from field names, and check for errors. - - :param context_fields: the set of context fields - :return: the event context list - """ - context_list = [] - for context_field_name in context_fields: - ec = lttng.EventContext() - context_type = _context_field_name_to_type(context_field_name) - if context_type is not None: - ec.ctx = context_type - context_list.append(ec) - else: - raise RuntimeError(f'failed to find context type: {context_field_name}') - return context_list + result = lttngpy.enable_events(**kwargs) + if result < 0: + raise RuntimeError(f'event enabling failed: {lttngpy.lttng_strerror(result)}') def _normalize_contexts_dict( - handles: Dict[str, Optional[lttng.Handle]], context_fields: Union[Set[str], Dict[str, List[str]]], -) -> Dict[lttng.Handle, List[lttng.EventContext]]: +) -> Dict[str, Set[str]]: """ - Normalize context list/set/dict to dict. + Normalize context set/dict to dict. - :param handles: the mapping from domain type name to handle - :param context_fields: the set of context field names, - or mapping from domain type name to list of context field names - :return: a dictionary of handle to list of event contexts + :param context_fields: the names of context fields to enable + if it's a set, the context fields are enabled for both kernel and userspace; + if it's a dictionary: { domain type string -> context fields list } + with the domain type string being either 'kernel' or 'userspace' + :return: a dictionary of domain type name to list of context field name """ - handles = {domain: handle for domain, handle in handles.items() if handle is not None} - contexts_dict = {} - if isinstance(context_fields, set): - contexts_dict = {h: _create_context_list(context_fields) for _, h in handles.items()} - elif isinstance(context_fields, dict): - contexts_dict = \ - {h: _create_context_list(set(context_fields[d])) for d, h in handles.items()} - else: - assert False - return contexts_dict - - -def _add_context( - contexts: Dict[lttng.Handle, List[lttng.EventContext]], -) -> None: + if isinstance(context_fields, dict): + return {domain: set(field_names) for domain, field_names in context_fields.items()} + assert isinstance(context_fields, set) + return { + 'userspace': context_fields, + 'kernel': context_fields, + } + + +def _add_contexts(**kwargs) -> None: """ Add context lists to given handles, and check for errors. - :param contexts: the dictionay of context handles -> event contexts + This must not be called if `lttngpy.is_available()` is `False`. + Raises RuntimeError on failure. + + See `lttngpy.add_contexts` for kwargs. """ - for handle, contexts_list in contexts.items(): - for context in contexts_list: - result = lttng.add_context(handle, context, None, None) - if result < 0: - raise RuntimeError( - f'failed to add context field {str(context)}: {lttng.strerror(result)}') + result = lttngpy.add_contexts(**kwargs) + if result < 0: + raise RuntimeError( + f'failed to add context field: {lttngpy.lttng_strerror(result)}') diff --git a/tracetools_trace/tracetools_trace/tools/lttng_stub.py b/tracetools_trace/tracetools_trace/tools/lttng_stub.py index ce7ecc75..2a6b5c05 100644 --- a/tracetools_trace/tracetools_trace/tools/lttng_stub.py +++ b/tracetools_trace/tracetools_trace/tools/lttng_stub.py @@ -15,7 +15,7 @@ """Stub version of the interface for tracing with LTTng.""" -ERROR_MESSAGE = 'lttng module not found, but still tried to use it' +ERROR_MESSAGE = 'LTTng Python bindings not available, but still tried to use them' def setup(*args, **kwargs) -> None: diff --git a/tracetools_trace/tracetools_trace/tools/names.py b/tracetools_trace/tracetools_trace/tools/names.py index 6b3effa8..ab5d37af 100644 --- a/tracetools_trace/tracetools_trace/tools/names.py +++ b/tracetools_trace/tracetools_trace/tools/names.py @@ -101,29 +101,8 @@ DEFAULT_EVENTS_UST = DEFAULT_EVENTS_ROS -CONTEXT_TYPE_CONSTANTS_MAP = { - 'pid': 'EVENT_CONTEXT_PID', - 'procname': 'EVENT_CONTEXT_PROCNAME', - 'prio': 'EVENT_CONTEXT_PRIO', - 'nice': 'EVENT_CONTEXT_NICE', - 'vpid': 'EVENT_CONTEXT_VPID', - 'tid': 'EVENT_CONTEXT_TID', - 'vtid': 'EVENT_CONTEXT_VTID', - 'ppid': 'EVENT_CONTEXT_PPID', - 'vppid': 'EVENT_CONTEXT_VPPID', - 'pthread_id': 'EVENT_CONTEXT_PTHREAD_ID', - 'hostname': 'EVENT_CONTEXT_HOSTNAME', - 'ip': 'EVENT_CONTEXT_IP', - 'interruptible': 'EVENT_CONTEXT_INTERRUPTIBLE', - 'preemptible': 'EVENT_CONTEXT_PREEMPTIBLE', - 'need_reschedule': 'EVENT_CONTEXT_NEED_RESCHEDULE', - 'migratable': 'EVENT_CONTEXT_MIGRATABLE', - 'perf:thread:instructions': None, - 'perf:thread:cycles': None, - 'perf:thread:cpu-cycles': None, -} - -CONTEXT = list(CONTEXT_TYPE_CONSTANTS_MAP.keys()) +DOMAIN_TYPE_KERNEL = 'kernel' +DOMAIN_TYPE_USERSPACE = 'userspace' # These apply to both kernel & userspace domains DEFAULT_CONTEXT = [