├── .github └── workflows │ ├── mirror-rolling-to-master.yaml │ └── test.yml ├── LICENSE ├── README.md ├── codecov.yml ├── pytest.ini ├── ros2trace_analysis ├── .coveragerc ├── .gitignore ├── CHANGELOG.rst ├── package.xml ├── resource │ └── ros2trace_analysis ├── ros2trace_analysis │ ├── __init__.py │ ├── api │ │ └── __init__.py │ ├── command │ │ ├── __init__.py │ │ └── trace_analysis.py │ └── verb │ │ ├── __init__.py │ │ ├── convert.py │ │ └── process.py ├── setup.py └── test │ ├── test_copyright.py │ ├── test_flake8.py │ ├── test_mypy.py │ ├── test_pep257.py │ └── test_xmllint.py ├── test_ros2trace_analysis ├── .coveragerc ├── .gitignore ├── CHANGELOG.rst ├── package.xml ├── resource │ └── test_ros2trace_analysis ├── setup.py ├── test │ ├── test_copyright.py │ ├── test_flake8.py │ ├── test_mypy.py │ ├── test_pep257.py │ ├── test_ros2trace_analysis │ │ └── test_process.py │ └── test_xmllint.py └── test_ros2trace_analysis │ └── __init__.py └── tracetools_analysis ├── .coveragerc ├── .gitignore ├── CHANGELOG.rst ├── analysis ├── .gitignore ├── callback_duration.ipynb ├── lifecycle_states.ipynb ├── memory_usage.ipynb └── sample_data │ └── converted_pingpong ├── docs ├── .gitignore ├── Makefile └── source │ ├── about.rst │ ├── api.rst │ ├── api │ └── tracetools_analysis.rst │ ├── conf.py │ └── index.rst ├── launch ├── lifecycle_states.launch.py ├── memory_usage.launch.py ├── pingpong.launch.py └── profile.launch.py ├── package.xml ├── resource └── tracetools_analysis ├── setup.cfg ├── setup.py ├── test ├── test_copyright.py ├── test_flake8.py ├── test_mypy.py ├── test_pep257.py ├── test_xmllint.py └── tracetools_analysis │ ├── test_autoprocessor.py │ ├── test_data_model_util.py │ ├── test_dependency_solver.py │ ├── test_loading.py │ ├── test_processor.py │ ├── test_profile_handler.py │ └── test_utils.py └── tracetools_analysis ├── __init__.py ├── conversion ├── __init__.py └── ctf.py ├── convert.py ├── data_model ├── __init__.py ├── cpu_time.py ├── memory_usage.py ├── profile.py └── ros2.py ├── loading └── __init__.py ├── process.py ├── processor ├── __init__.py ├── cpu_time.py ├── memory_usage.py ├── profile.py └── ros2.py ├── scripts ├── __init__.py ├── auto.py ├── cb_durations.py └── memory_usage.py └── utils ├── __init__.py ├── cpu_time.py ├── memory_usage.py ├── profile.py └── ros2.py /.github/workflows/mirror-rolling-to-master.yaml: -------------------------------------------------------------------------------- 1 | name: Mirror rolling to master 2 | 3 | on: 4 | push: 5 | branches: [ rolling ] 6 | 7 | jobs: 8 | mirror-to-master: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: zofrex/mirror-branch@v1 12 | with: 13 | target-branch: master 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - rolling 7 | schedule: 8 | - cron: "0 5 * * *" 9 | jobs: 10 | build-and-test: 11 | runs-on: ubuntu-latest 12 | container: 13 | image: ubuntu:24.04 14 | continue-on-error: ${{ matrix.build-type == 'binary' }} 15 | strategy: 16 | matrix: 17 | distro: 18 | - rolling 19 | build-type: 20 | - binary 21 | - source 22 | env: 23 | ROS2_REPOS_FILE_URL: 'https://raw.githubusercontent.com/ros2/ros2/${{ matrix.distro }}/ros2.repos' 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: ros-tooling/setup-ros@master 27 | with: 28 | required-ros-distributions: ${{ matrix.build-type == 'binary' && matrix.distro || '' }} 29 | use-ros2-testing: true 30 | - uses: ros-tooling/action-ros-ci@master 31 | with: 32 | package-name: ros2trace_analysis tracetools_analysis 33 | target-ros2-distro: ${{ matrix.distro }} 34 | vcs-repo-file-url: ${{ matrix.build-type == 'source' && env.ROS2_REPOS_FILE_URL || '' }} 35 | colcon-defaults: | 36 | { 37 | "build": { 38 | "mixin": [ 39 | "coverage-pytest" 40 | ] 41 | }, 42 | "test": { 43 | "mixin": [ 44 | "coverage-pytest" 45 | ], 46 | "executor": "sequential", 47 | "retest-until-pass": 2, 48 | "pytest-args": ["-m", "not xfail"] 49 | } 50 | } 51 | - uses: codecov/codecov-action@v3 52 | with: 53 | files: ros_ws/coveragepy/.coverage 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tracetools_analysis 2 | 3 | 4 | [![codecov](https://codecov.io/gh/ros-tracing/tracetools_analysis/branch/rolling/graph/badge.svg)](https://codecov.io/gh/ros-tracing/tracetools_analysis) 5 | 6 | Analysis tools for [`ros2_tracing`](https://github.com/ros2/ros2_tracing). 7 | 8 | **Note**: make sure to use the right branch, depending on the ROS 2 distro: [use `rolling` for Rolling, `humble` for Humble, etc.](https://docs.ros.org/en/rolling/The-ROS2-Project/Contributing/Developer-Guide.html) 9 | 10 | ## Trace analysis 11 | 12 | After generating a trace (see [`ros2_tracing`](https://github.com/ros2/ros2_tracing#tracing)), we can analyze it to extract useful execution data. 13 | 14 | ### Commands 15 | 16 | Then we can process a trace to create a data model which could be queried for analysis. 17 | 18 | ```shell 19 | $ ros2 trace-analysis process /path/to/trace/directory 20 | ``` 21 | 22 | Note that this simply outputs lightly-processed ROS 2 trace data which is split into a number of pandas `DataFrame`s. 23 | This can be used to quickly check the trace data. 24 | For real data processing/trace analysis, see [*Analysis*](#analysis). 25 | 26 | Since CTF traces (the output format of the [LTTng](https://lttng.org/) tracer) are very slow to read, the trace is first converted into a single file which can be read much faster and can be re-used to run many analyses. 27 | This is done automatically, but if the trace changed after the file was generated, it can be re-generated using the `--force-conversion` option. 28 | Run with `--help` to see all options. 29 | 30 | ### Analysis 31 | 32 | The command above will process and output raw data models. 33 | We need to actually analyze the data and display some results. 34 | We recommend doing this in a Jupyter Notebook, but you can do this in a normal Python file. 35 | 36 | ```shell 37 | $ jupyter notebook 38 | ``` 39 | 40 | Navigate to the [`analysis/`](./tracetools_analysis/analysis/) directory, and select one of the provided notebooks, or create your own! 41 | 42 | For example: 43 | 44 | ```python 45 | from tracetools_analysis.loading import load_file 46 | from tracetools_analysis.processor import Processor 47 | from tracetools_analysis.processor.cpu_time import CpuTimeHandler 48 | from tracetools_analysis.processor.ros2 import Ros2Handler 49 | from tracetools_analysis.utils.cpu_time import CpuTimeDataModelUtil 50 | from tracetools_analysis.utils.ros2 import Ros2DataModelUtil 51 | 52 | # Load trace directory or converted trace file 53 | events = load_file('/path/to/trace/or/converted/file') 54 | 55 | # Process 56 | ros2_handler = Ros2Handler() 57 | cpu_handler = CpuTimeHandler() 58 | 59 | Processor(ros2_handler, cpu_handler).process(events) 60 | 61 | # Use data model utils to extract information 62 | ros2_util = Ros2DataModelUtil(ros2_handler.data) 63 | cpu_util = CpuTimeDataModelUtil(cpu_handler.data) 64 | 65 | callback_symbols = ros2_util.get_callback_symbols() 66 | callback_object, callback_symbol = list(callback_symbols.items())[0] 67 | callback_durations = ros2_util.get_callback_durations(callback_object) 68 | time_per_thread = cpu_util.get_time_per_thread() 69 | # ... 70 | 71 | # Display, e.g., with bokeh, matplotlib, print, etc. 72 | print(callback_symbol) 73 | print(callback_durations) 74 | 75 | print(time_per_thread) 76 | # ... 77 | ``` 78 | 79 | Note: bokeh has to be installed manually, e.g., with `pip`: 80 | 81 | ```shell 82 | $ pip3 install bokeh 83 | ``` 84 | 85 | ## Design 86 | 87 | See the [`ros2_tracing` design document](https://github.com/ros2/ros2_tracing/blob/rolling/doc/design_ros_2.md), especially the [*Goals and requirements*](https://github.com/ros2/ros2_tracing/blob/rolling/doc/design_ros_2.md#goals-and-requirements) and [*Analysis*](https://github.com/ros2/ros2_tracing/blob/rolling/doc/design_ros_2.md#analysis) sections. 88 | 89 | ## Packages 90 | 91 | ### ros2trace_analysis 92 | 93 | Package containing a `ros2cli` extension to perform trace analysis. 94 | 95 | ### tracetools_analysis 96 | 97 | Package containing tools for analyzing trace data. 98 | 99 | See the [API documentation](https://docs.ros.org/en/rolling/p/tracetools_analysis/). 100 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | fixes: 2 | - "/builds/ros-tracing/tracetools_analysis/::" 3 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | junit_family=xunit2 3 | 4 | -------------------------------------------------------------------------------- /ros2trace_analysis/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | setup.py 4 | test/* 5 | -------------------------------------------------------------------------------- /ros2trace_analysis/.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | 4 | -------------------------------------------------------------------------------- /ros2trace_analysis/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package ros2trace_analysis 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 3.0.0 (2022-01-21) 6 | ------------------ 7 | * Add 'process --convert-only' option 8 | * Deprecate 'convert' verb since it is just an implementation detail 9 | * Contributors: Christophe Bedard 10 | 11 | 0.2.2 (2019-11-19) 12 | ------------------ 13 | * Add flag for hiding processing results with the process verb 14 | * Contributors: Christophe Bedard 15 | 16 | 0.2.0 (2019-10-14) 17 | ------------------ 18 | * Add ros2trace_analysis command and process/convert verbs 19 | * Contributors: Christophe Bedard 20 | -------------------------------------------------------------------------------- /ros2trace_analysis/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ros2trace_analysis 5 | 3.1.0 6 | The trace-analysis command for ROS 2 command line tools. 7 | Christophe Bedard 8 | Apache 2.0 9 | https://index.ros.org/p/ros2trace_analysis/ 10 | https://github.com/ros-tracing/tracetools_analysis 11 | https://github.com/ros-tracing/tracetools_analysis/issues 12 | Christophe Bedard 13 | 14 | ros2cli 15 | tracetools_analysis 16 | 17 | ament_copyright 18 | ament_flake8 19 | ament_mypy 20 | ament_pep257 21 | ament_xmllint 22 | python3-pytest 23 | 24 | 25 | ament_python 26 | 27 | 28 | -------------------------------------------------------------------------------- /ros2trace_analysis/resource/ros2trace_analysis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ros-tracing/tracetools_analysis/d2bd9bae0400b83404e22e1d5fe7ecb6982bfaa8/ros2trace_analysis/resource/ros2trace_analysis -------------------------------------------------------------------------------- /ros2trace_analysis/ros2trace_analysis/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Apex.AI, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /ros2trace_analysis/ros2trace_analysis/api/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Apex.AI, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /ros2trace_analysis/ros2trace_analysis/command/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Apex.AI, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /ros2trace_analysis/ros2trace_analysis/command/trace_analysis.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Apex.AI, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Module for trace analysis command extension implementation.""" 16 | 17 | from ros2cli.command import add_subparsers_on_demand 18 | from ros2cli.command import CommandExtension 19 | 20 | 21 | class TraceAnalysisCommand(CommandExtension): 22 | """Analyze traces to extract useful execution data.""" 23 | 24 | def add_arguments(self, parser, cli_name): 25 | self._subparser = parser 26 | # get verb extensions and let them add their arguments 27 | add_subparsers_on_demand( 28 | parser, cli_name, '_verb', 'ros2trace_analysis.verb', required=False) 29 | 30 | def main(self, *, parser, args): 31 | if not hasattr(args, '_verb'): 32 | # in case no verb was passed 33 | self._subparser.print_help() 34 | return 0 35 | 36 | extension = getattr(args, '_verb') 37 | 38 | # call the verb's main method 39 | return extension.main(args=args) 40 | -------------------------------------------------------------------------------- /ros2trace_analysis/ros2trace_analysis/verb/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Apex.AI, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /ros2trace_analysis/ros2trace_analysis/verb/convert.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Apex.AI, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ros2cli.verb import VerbExtension 16 | from tracetools_analysis.convert import add_args 17 | from tracetools_analysis.convert import convert 18 | 19 | 20 | class ConvertVerb(VerbExtension): 21 | """Convert trace data to a file. DEPRECATED: use the 'process' verb directly.""" 22 | 23 | def add_arguments(self, parser, cli_name): 24 | add_args(parser) 25 | 26 | def main(self, *, args): 27 | import warnings 28 | warnings.warn("'convert' is deprecated, use 'process' directly instead", stacklevel=2) 29 | return convert( 30 | args.trace_directory, 31 | args.output_file_name, 32 | ) 33 | -------------------------------------------------------------------------------- /ros2trace_analysis/ros2trace_analysis/verb/process.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Apex.AI, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ros2cli.verb import VerbExtension 16 | from tracetools_analysis.process import add_args 17 | from tracetools_analysis.process import process 18 | 19 | 20 | class ProcessVerb(VerbExtension): 21 | """Process ROS 2 trace data and output model data.""" 22 | 23 | def add_arguments(self, parser, cli_name): 24 | add_args(parser) 25 | 26 | def main(self, *, args): 27 | return process( 28 | args.input_path, 29 | args.force_conversion, 30 | args.hide_results, 31 | args.convert_only, 32 | ) 33 | -------------------------------------------------------------------------------- /ros2trace_analysis/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages 2 | from setuptools import setup 3 | 4 | package_name = 'ros2trace_analysis' 5 | 6 | setup( 7 | name=package_name, 8 | version='3.1.0', 9 | packages=find_packages(exclude=['test']), 10 | data_files=[ 11 | ('share/' + package_name, ['package.xml']), 12 | ('share/ament_index/resource_index/packages', 13 | ['resource/' + package_name]), 14 | ], 15 | install_requires=['ros2cli'], 16 | zip_safe=True, 17 | maintainer=( 18 | 'Christophe Bedard' 19 | ), 20 | maintainer_email=( 21 | 'bedard.christophe@gmail.com' 22 | ), 23 | author='Christophe Bedard', 24 | author_email='christophe.bedard@apex.ai', 25 | url='https://github.com/ros-tracing/tracetools_analysis', 26 | keywords=[], 27 | description='The trace-analysis command for ROS 2 command line tools.', 28 | long_description=( 29 | 'The package provides the trace-analysis ' 30 | 'command for the ROS 2 command line tools.' 31 | ), 32 | license='Apache 2.0', 33 | tests_require=['pytest'], 34 | entry_points={ 35 | 'ros2cli.command': [ 36 | f'trace-analysis = {package_name}.command.trace_analysis:TraceAnalysisCommand', 37 | ], 38 | 'ros2cli.extension_point': [ 39 | f'{package_name}.verb = {package_name}.verb:VerbExtension', 40 | ], 41 | f'{package_name}.verb': [ 42 | f'convert = {package_name}.verb.convert:ConvertVerb', 43 | f'process = {package_name}.verb.process:ProcessVerb', 44 | ], 45 | } 46 | ) 47 | -------------------------------------------------------------------------------- /ros2trace_analysis/test/test_copyright.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_copyright.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.copyright 20 | @pytest.mark.linter 21 | def test_copyright(): 22 | rc = main(argv=['.', 'test']) 23 | assert rc == 0, 'Found errors' 24 | -------------------------------------------------------------------------------- /ros2trace_analysis/test/test_flake8.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_flake8.main import main_with_errors 16 | import pytest 17 | 18 | 19 | @pytest.mark.flake8 20 | @pytest.mark.linter 21 | def test_flake8(): 22 | rc, errors = main_with_errors(argv=[]) 23 | assert rc == 0, \ 24 | 'Found %d code style errors / warnings:\n' % len(errors) + \ 25 | '\n'.join(errors) 26 | -------------------------------------------------------------------------------- /ros2trace_analysis/test/test_mypy.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Canonical Ltd 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_mypy.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.mypy 20 | @pytest.mark.linter 21 | def test_mypy(): 22 | assert main(argv=[]) == 0, 'Found errors' 23 | -------------------------------------------------------------------------------- /ros2trace_analysis/test/test_pep257.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_pep257.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.linter 20 | @pytest.mark.pep257 21 | def test_pep257(): 22 | rc = main(argv=[]) 23 | assert rc == 0, 'Found code style errors / warnings' 24 | -------------------------------------------------------------------------------- /ros2trace_analysis/test/test_xmllint.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_xmllint.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.linter 20 | @pytest.mark.xmllint 21 | def test_xmllint(): 22 | rc = main(argv=[]) 23 | assert rc == 0, 'Found errors' 24 | -------------------------------------------------------------------------------- /test_ros2trace_analysis/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | setup.py 4 | test/* 5 | -------------------------------------------------------------------------------- /test_ros2trace_analysis/.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | 4 | -------------------------------------------------------------------------------- /test_ros2trace_analysis/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package test_ros2trace_analysis 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 3.1.0 (2024-06-15) 6 | ------------------ 7 | * Fix test_ros2trace_analysis package version (`#26 `_) 8 | * Use tracepoint names from tracetools_trace and add tests (`#25 `_) 9 | * Contributors: Christophe Bedard 10 | -------------------------------------------------------------------------------- /test_ros2trace_analysis/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | test_ros2trace_analysis 5 | 3.1.0 6 | Tests for the ros2trace_analysis package. 7 | Christophe Bedard 8 | Apache 2.0 9 | https://index.ros.org/p/test_ros2trace_analysis/ 10 | https://github.com/ros-tracing/tracetools_analysis 11 | https://github.com/ros-tracing/tracetools_analysis/issues 12 | Christophe Bedard 13 | 14 | ament_copyright 15 | ament_flake8 16 | ament_mypy 17 | ament_pep257 18 | ament_xmllint 19 | launch 20 | launch_ros 21 | python3-pytest 22 | ros2run 23 | ros2trace 24 | ros2trace_analysis 25 | test_tracetools 26 | tracetools 27 | tracetools_trace 28 | 29 | 30 | ament_python 31 | 32 | 33 | -------------------------------------------------------------------------------- /test_ros2trace_analysis/resource/test_ros2trace_analysis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ros-tracing/tracetools_analysis/d2bd9bae0400b83404e22e1d5fe7ecb6982bfaa8/test_ros2trace_analysis/resource/test_ros2trace_analysis -------------------------------------------------------------------------------- /test_ros2trace_analysis/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages 2 | from setuptools import setup 3 | 4 | package_name = 'test_ros2trace_analysis' 5 | 6 | setup( 7 | name=package_name, 8 | version='3.1.0', 9 | packages=find_packages(exclude=['test']), 10 | data_files=[ 11 | ('share/' + package_name, ['package.xml']), 12 | ('share/ament_index/resource_index/packages', 13 | ['resource/' + package_name]), 14 | ], 15 | install_requires=['setuptools'], 16 | zip_safe=True, 17 | maintainer='Christophe Bedard', 18 | maintainer_email='bedard.christophe@gmail.com', 19 | author='Christophe Bedard', 20 | author_email='bedard.christophe@gmail.com', 21 | url='https://github.com/ros-tracing/tracetools_analysis', 22 | keywords=[], 23 | description='Tests for the ros2trace_analysis package.', 24 | license='Apache 2.0', 25 | tests_require=['pytest'], 26 | ) 27 | -------------------------------------------------------------------------------- /test_ros2trace_analysis/test/test_copyright.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_copyright.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.copyright 20 | @pytest.mark.linter 21 | def test_copyright(): 22 | rc = main(argv=['.', 'test']) 23 | assert rc == 0, 'Found errors' 24 | -------------------------------------------------------------------------------- /test_ros2trace_analysis/test/test_flake8.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_flake8.main import main_with_errors 16 | import pytest 17 | 18 | 19 | @pytest.mark.flake8 20 | @pytest.mark.linter 21 | def test_flake8(): 22 | rc, errors = main_with_errors(argv=[]) 23 | assert rc == 0, \ 24 | 'Found %d code style errors / warnings:\n' % len(errors) + \ 25 | '\n'.join(errors) 26 | -------------------------------------------------------------------------------- /test_ros2trace_analysis/test/test_mypy.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Canonical Ltd 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_mypy.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.mypy 20 | @pytest.mark.linter 21 | def test_mypy(): 22 | assert main(argv=[]) == 0, 'Found errors' 23 | -------------------------------------------------------------------------------- /test_ros2trace_analysis/test/test_pep257.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_pep257.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.linter 20 | @pytest.mark.pep257 21 | def test_pep257(): 22 | rc = main(argv=[]) 23 | assert rc == 0, 'Found code style errors / warnings' 24 | -------------------------------------------------------------------------------- /test_ros2trace_analysis/test/test_ros2trace_analysis/test_process.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Apex.AI, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from pathlib import Path 17 | import shutil 18 | import subprocess 19 | import tempfile 20 | from typing import Dict 21 | from typing import List 22 | from typing import Optional 23 | import unittest 24 | 25 | from launch import LaunchDescription 26 | from launch import LaunchService 27 | from launch_ros.actions import Node 28 | from tracetools_trace.tools.lttng import is_lttng_installed 29 | 30 | 31 | def are_tracepoints_included() -> bool: 32 | """ 33 | Check if tracing instrumentation is enabled and if tracepoints are included. 34 | 35 | :return: True if tracepoints are included, False otherwise 36 | """ 37 | if not is_lttng_installed(): 38 | return False 39 | process = subprocess.run( 40 | ['ros2', 'run', 'tracetools', 'status'], 41 | stdout=subprocess.PIPE, 42 | stderr=subprocess.PIPE, 43 | encoding='utf-8', 44 | ) 45 | return 0 == process.returncode 46 | 47 | 48 | @unittest.skipIf(not is_lttng_installed(minimum_version='2.9.0'), 'LTTng is required') 49 | class TestROS2TraceAnalysisCLI(unittest.TestCase): 50 | 51 | def __init__(self, *args) -> None: 52 | super().__init__( 53 | *args, 54 | ) 55 | 56 | def create_test_tmpdir(self, test_name: str) -> str: 57 | prefix = self.__class__.__name__ + '__' + test_name 58 | return tempfile.mkdtemp(prefix=prefix) 59 | 60 | def run_command( 61 | self, 62 | args: List[str], 63 | *, 64 | env: Optional[Dict[str, str]] = None, 65 | ) -> subprocess.Popen: 66 | print('=>running:', args) 67 | process_env = os.environ.copy() 68 | process_env['PYTHONUNBUFFERED'] = '1' 69 | if env: 70 | process_env.update(env) 71 | return subprocess.Popen( 72 | args, 73 | stdin=subprocess.PIPE, 74 | stdout=subprocess.PIPE, 75 | stderr=subprocess.PIPE, 76 | encoding='utf-8', 77 | env=process_env, 78 | ) 79 | 80 | def wait_and_print_command_output( 81 | self, 82 | process: subprocess.Popen, 83 | ) -> int: 84 | stdout, stderr = process.communicate() 85 | stdout = stdout.strip(' \r\n\t') 86 | stderr = stderr.strip(' \r\n\t') 87 | print('=>stdout:\n' + stdout) 88 | print('=>stderr:\n' + stderr) 89 | return process.wait() 90 | 91 | def run_command_and_wait( 92 | self, 93 | args: List[str], 94 | *, 95 | env: Optional[Dict[str, str]] = None, 96 | ) -> int: 97 | process = self.run_command(args, env=env) 98 | return self.wait_and_print_command_output(process) 99 | 100 | def run_nodes(self) -> None: 101 | nodes = [ 102 | Node( 103 | package='test_tracetools', 104 | executable='test_ping', 105 | output='screen', 106 | ), 107 | Node( 108 | package='test_tracetools', 109 | executable='test_pong', 110 | output='screen', 111 | ), 112 | ] 113 | ld = LaunchDescription(nodes) 114 | ls = LaunchService() 115 | ls.include_launch_description(ld) 116 | exit_code = ls.run() 117 | self.assertEqual(0, exit_code) 118 | 119 | def test_process_bad_input_path(self) -> None: 120 | tmpdir = self.create_test_tmpdir('test_process_bad_input_path') 121 | 122 | # No input path 123 | ret = self.run_command_and_wait(['ros2', 'trace-analysis', 'process']) 124 | self.assertEqual(2, ret) 125 | 126 | # Does not exist 127 | ret = self.run_command_and_wait(['ros2', 'trace-analysis', 'process', '']) 128 | self.assertEqual(1, ret) 129 | fake_input = os.path.join(tmpdir, 'doesnt_exist') 130 | ret = self.run_command_and_wait(['ros2', 'trace-analysis', 'process', fake_input]) 131 | self.assertEqual(1, ret) 132 | 133 | # Exists but empty 134 | empty_input = os.path.join(tmpdir, 'empty') 135 | os.mkdir(empty_input) 136 | ret = self.run_command_and_wait(['ros2', 'trace-analysis', 'process', empty_input]) 137 | self.assertEqual(1, ret) 138 | 139 | # Exists but converted file empty 140 | empty_converted_file = os.path.join(empty_input, 'converted') 141 | Path(empty_converted_file).touch() 142 | ret = self.run_command_and_wait(['ros2', 'trace-analysis', 'process', empty_input]) 143 | self.assertEqual(1, ret) 144 | 145 | shutil.rmtree(tmpdir) 146 | 147 | @unittest.skipIf(not are_tracepoints_included(), 'tracepoints are required') 148 | def test_process(self) -> None: 149 | tmpdir = self.create_test_tmpdir('test_process') 150 | session_name = 'test_process' 151 | 152 | # Run and trace nodes 153 | ret = self.run_command_and_wait( 154 | [ 155 | 'ros2', 'trace', 156 | 'start', session_name, 157 | '--path', tmpdir, 158 | ], 159 | ) 160 | self.assertEqual(0, ret) 161 | trace_dir = os.path.join(tmpdir, session_name) 162 | self.run_nodes() 163 | ret = self.run_command_and_wait(['ros2', 'trace', 'stop', session_name]) 164 | self.assertEqual(0, ret) 165 | 166 | # Process trace 167 | ret = self.run_command_and_wait(['ros2', 'trace-analysis', 'process', trace_dir]) 168 | self.assertEqual(0, ret) 169 | 170 | # Check that converted file exists and isn't empty 171 | converted_file = os.path.join(trace_dir, 'converted') 172 | self.assertTrue(os.path.isfile(converted_file)) 173 | self.assertGreater(os.path.getsize(converted_file), 0) 174 | 175 | shutil.rmtree(tmpdir) 176 | -------------------------------------------------------------------------------- /test_ros2trace_analysis/test/test_xmllint.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_xmllint.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.linter 20 | @pytest.mark.xmllint 21 | def test_xmllint(): 22 | rc = main(argv=[]) 23 | assert rc == 0, 'Found errors' 24 | -------------------------------------------------------------------------------- /test_ros2trace_analysis/test_ros2trace_analysis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ros-tracing/tracetools_analysis/d2bd9bae0400b83404e22e1d5fe7ecb6982bfaa8/test_ros2trace_analysis/test_ros2trace_analysis/__init__.py -------------------------------------------------------------------------------- /tracetools_analysis/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | setup.py 4 | test/* 5 | -------------------------------------------------------------------------------- /tracetools_analysis/.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | 4 | -------------------------------------------------------------------------------- /tracetools_analysis/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package tracetools_analysis 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 3.1.0 (2024-06-15) 6 | ------------------ 7 | * Use tracepoint names from tracetools_trace and add tests (`#25 `_) 8 | * Use underscores in setup.cfg (`#21 `_) 9 | * Skip TestDataModelUtil.test_convert_time_columns if pandas < 2.2.0 (`#20 `_) 10 | * Fix warnings when using mypy>=1.8.0 (`#16 `_) 11 | * Support traces with multiple callbacks for same pointer (`#13 `_) (`#15 `_) 12 | * Update path to ros2_tracing in notebooks (`#8 `_) 13 | * Refactored for compatibility with Bokeh 3.2.0 (`#7 `_) 14 | * Fix mypy errors (`#4 `_) 15 | * Contributors: Christophe Bedard, Oren Bell 16 | 17 | 3.0.0 (2022-01-21) 18 | ------------------ 19 | * Update context_fields option name in profile example launch file 20 | * Fix both rcl and rmw subscriptions being added to the rcl dataframe 21 | * Support rmw pub/sub init and take instrumentation 22 | * Support publishing instrumentation 23 | * Change 'input_path' arg help message wording 24 | * Add 'process --convert-only' option 25 | * Deprecate 'convert' verb since it is just an implementation detail 26 | * Simplify jupyter notebooks and add way to use Debian packages 27 | * Contributors: Christophe Bedard 28 | 29 | 2.0.0 (2021-03-31) 30 | ------------------ 31 | * Set callback_instances' timestamp & duration cols to datetime/timedelta 32 | * Improve performance by using lists of dicts as intermediate storage & converting to dataframes at the end 33 | * Update callback_duration notebook and pingpong sample data 34 | * Support instrumentation for linking a timer to a node 35 | * Disable kernel tracing for pingpong example launchfile 36 | * Support lifecycle node state transition instrumentation 37 | * Contributors: Christophe Bedard 38 | 39 | 1.0.0 (2020-06-02) 40 | ------------------ 41 | * Add sphinx documentation for tracetools_analysis 42 | * Improve RequiredEventNotFoundError message 43 | * Add 'quiet' option to loading-related functions 44 | * Declare dependencies on jupyter & bokeh, and restore pandas dependency 45 | * Fix deprecation warnings by using executable instead of node_executable 46 | * Define output metavar to simplify ros2 trace-analysis convert usage info 47 | * Validate convert/process paths 48 | * Add 'ip' context to example profiling launch file 49 | * Switch to using ping/pong nodes for profile example launch file 50 | * Add option to simply give an EventHandler when creating a DataModelUtil 51 | * Do check before calling super().__init_\_() 52 | * Add AutoProcessor and script entrypoint 53 | * Make sure Processor is given at least one EventHandler 54 | * Make do_convert_if_needed True by default 55 | * Allow EventHandlers to declare set of required events 56 | * Add cleanup method for ProcessingProgressDisplay 57 | * Add memory usage analysis and entrypoint script 58 | * Add callback-durations analysis script 59 | * Contributors: Christophe Bedard, Ingo Lütkebohle 60 | 61 | 0.2.2 (2019-11-19) 62 | ------------------ 63 | * Update ROS 2 handler and data model after new tracepoint 64 | * Fix timestamp column conversion util method 65 | * Contributors: Christophe Bedard 66 | 67 | 0.2.0 (2019-10-14) 68 | ------------------ 69 | * Improve UX 70 | * Add flag for process command to force re-conversion of trace directory 71 | * Make process command convert directory if necessary 72 | * Make output file name optional for convert command 73 | * Remove references to "pickle" file and simply use "output" file 74 | * Display Processor progress on stdout 75 | * Add sample data, notebook, and launch file 76 | * Add data model util functions 77 | * Add profiling and CPU time event handlers 78 | * Refactor and extend analysis architecture 79 | * Contributors: Christophe Bedard 80 | 81 | 0.1.1 (2019-07-16) 82 | ------------------ 83 | * Update metadata 84 | * Contributors: Christophe Bedard 85 | 86 | 0.1.0 (2019-07-11) 87 | ------------------ 88 | * Add analysis tools 89 | * Contributors: Christophe Bedard, Ingo Lütkebohle 90 | -------------------------------------------------------------------------------- /tracetools_analysis/analysis/.gitignore: -------------------------------------------------------------------------------- 1 | *.svg 2 | *.png 3 | *.pdf 4 | .ipynb_checkpoints 5 | 6 | -------------------------------------------------------------------------------- /tracetools_analysis/analysis/callback_duration.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# Callback duration\n", 10 | "#\n", 11 | "# Get trace data using the provided launch file:\n", 12 | "# $ ros2 launch tracetools_analysis pingpong.launch.py\n", 13 | "# (wait at least a few seconds, then kill with Ctrl+C)\n", 14 | "#\n", 15 | "# OR\n", 16 | "#\n", 17 | "# Use the provided sample converted trace file, changing the path below to:\n", 18 | "# 'sample_data/converted_pingpong'" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "path = '~/.ros/tracing/pingpong/ust'\n", 28 | "#path = 'sample_data/converted_pingpong'" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "import sys\n", 38 | "# Add paths to tracetools_analysis and tracetools_read.\n", 39 | "# There are two options:\n", 40 | "# 1. from source, assuming a workspace with:\n", 41 | "# src/tracetools_analysis/\n", 42 | "# src/ros2/ros2_tracing/tracetools_read/\n", 43 | "sys.path.insert(0, '../')\n", 44 | "sys.path.insert(0, '../../../ros2/ros2_tracing/tracetools_read/')\n", 45 | "# 2. from Debian packages, setting the right ROS 2 distro:\n", 46 | "#ROS_DISTRO = 'rolling'\n", 47 | "#sys.path.insert(0, f'/opt/ros/{ROS_DISTRO}/lib/python3.8/site-packages')\n", 48 | "import datetime as dt\n", 49 | "\n", 50 | "from bokeh.plotting import figure\n", 51 | "from bokeh.plotting import output_notebook\n", 52 | "from bokeh.io import show\n", 53 | "from bokeh.layouts import row\n", 54 | "from bokeh.models import ColumnDataSource\n", 55 | "from bokeh.models import DatetimeTickFormatter\n", 56 | "from bokeh.models import PrintfTickFormatter\n", 57 | "import numpy as np\n", 58 | "import pandas as pd\n", 59 | "\n", 60 | "from tracetools_analysis.loading import load_file\n", 61 | "from tracetools_analysis.processor.ros2 import Ros2Handler\n", 62 | "from tracetools_analysis.utils.ros2 import Ros2DataModelUtil" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "# Process\n", 72 | "events = load_file(path)\n", 73 | "handler = Ros2Handler.process(events)\n", 74 | "#handler.data.print_data()" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "metadata": { 81 | "scrolled": false 82 | }, 83 | "outputs": [], 84 | "source": [ 85 | "data_util = Ros2DataModelUtil(handler.data)\n", 86 | "\n", 87 | "callback_symbols = data_util.get_callback_symbols()\n", 88 | "\n", 89 | "output_notebook()\n", 90 | "psize = 450\n", 91 | "# If the trace contains more callbacks, add colours here\n", 92 | "# or use: https://docs.bokeh.org/en/3.2.2/docs/reference/palettes.html\n", 93 | "colours = ['#29788E', '#DD4968', '#410967']" 94 | ] 95 | }, 96 | { 97 | "cell_type": "code", 98 | "execution_count": null, 99 | "metadata": { 100 | "scrolled": false 101 | }, 102 | "outputs": [], 103 | "source": [ 104 | "# Plot durations separately\n", 105 | "colour_i = 0\n", 106 | "for obj, symbol in callback_symbols.items():\n", 107 | " owner_info = data_util.get_callback_owner_info(obj)\n", 108 | " if owner_info is None:\n", 109 | " owner_info = '[unknown]'\n", 110 | "\n", 111 | " # Filter out internal subscriptions\n", 112 | " if '/parameter_events' in owner_info:\n", 113 | " continue\n", 114 | "\n", 115 | " # Duration\n", 116 | " duration_df = data_util.get_callback_durations(obj)\n", 117 | " starttime = duration_df.loc[:, 'timestamp'].iloc[0].strftime('%Y-%m-%d %H:%M')\n", 118 | " source = ColumnDataSource(duration_df)\n", 119 | " duration = figure(\n", 120 | " title=owner_info,\n", 121 | " x_axis_label=f'start ({starttime})',\n", 122 | " y_axis_label='duration (ms)',\n", 123 | " width=psize, height=psize,\n", 124 | " )\n", 125 | " duration.title.align = 'center'\n", 126 | " duration.line(\n", 127 | " x='timestamp',\n", 128 | " y='duration',\n", 129 | " legend_label=str(symbol),\n", 130 | " line_width=2,\n", 131 | " source=source,\n", 132 | " line_color=colours[colour_i],\n", 133 | " )\n", 134 | " duration.legend.label_text_font_size = '11px'\n", 135 | " duration.xaxis[0].formatter = DatetimeTickFormatter(seconds='%Ss')\n", 136 | "\n", 137 | " # Histogram\n", 138 | " # (convert to milliseconds)\n", 139 | " dur_hist, edges = np.histogram(duration_df['duration'] * 1000 / np.timedelta64(1, 's'))\n", 140 | " duration_hist = pd.DataFrame({\n", 141 | " 'duration': dur_hist, \n", 142 | " 'left': edges[:-1], \n", 143 | " 'right': edges[1:],\n", 144 | " })\n", 145 | " hist = figure(\n", 146 | " title='Duration histogram',\n", 147 | " x_axis_label='duration (ms)',\n", 148 | " y_axis_label='frequency',\n", 149 | " width=psize, height=psize,\n", 150 | " )\n", 151 | " hist.title.align = 'center'\n", 152 | " hist.quad(\n", 153 | " bottom=0,\n", 154 | " top=duration_hist['duration'], \n", 155 | " left=duration_hist['left'],\n", 156 | " right=duration_hist['right'],\n", 157 | " fill_color=colours[colour_i],\n", 158 | " line_color=colours[colour_i],\n", 159 | " )\n", 160 | "\n", 161 | " colour_i += 1\n", 162 | " colour_i %= len(colours)\n", 163 | " show(row(duration, hist))" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": null, 169 | "metadata": {}, 170 | "outputs": [], 171 | "source": [ 172 | "# Plot durations in one plot\n", 173 | "earliest_date = None\n", 174 | "for obj, symbol in callback_symbols.items():\n", 175 | " duration_df = data_util.get_callback_durations(obj)\n", 176 | " thedate = duration_df.loc[:, 'timestamp'].iloc[0]\n", 177 | " if earliest_date is None or thedate <= earliest_date:\n", 178 | " earliest_date = thedate\n", 179 | "\n", 180 | "starttime = earliest_date.strftime('%Y-%m-%d %H:%M')\n", 181 | "duration = figure(\n", 182 | " title='Callback durations',\n", 183 | " x_axis_label=f'start ({starttime})',\n", 184 | " y_axis_label='duration (ms)',\n", 185 | " width=psize, height=psize,\n", 186 | ")\n", 187 | "\n", 188 | "colour_i = 0\n", 189 | "for obj, symbol in callback_symbols.items():\n", 190 | " # Filter out internal subscriptions\n", 191 | " owner_info = data_util.get_callback_owner_info(obj)\n", 192 | " if not owner_info or '/parameter_events' in owner_info:\n", 193 | " continue\n", 194 | "\n", 195 | " duration_df = data_util.get_callback_durations(obj)\n", 196 | " source = ColumnDataSource(duration_df)\n", 197 | " duration.title.align = 'center'\n", 198 | " duration.line(\n", 199 | " x='timestamp',\n", 200 | " y='duration',\n", 201 | " legend_label=str(symbol),\n", 202 | " line_width=2,\n", 203 | " source=source,\n", 204 | " line_color=colours[colour_i],\n", 205 | " )\n", 206 | " colour_i += 1\n", 207 | " colour_i %= len(colours)\n", 208 | " duration.legend.label_text_font_size = '11px'\n", 209 | " duration.xaxis[0].formatter = DatetimeTickFormatter(seconds='%Ss')\n", 210 | "\n", 211 | "show(duration)" 212 | ] 213 | }, 214 | { 215 | "cell_type": "code", 216 | "execution_count": null, 217 | "metadata": {}, 218 | "outputs": [], 219 | "source": [] 220 | } 221 | ], 222 | "metadata": { 223 | "kernelspec": { 224 | "display_name": "Python 3 (ipykernel)", 225 | "language": "python", 226 | "name": "python3" 227 | }, 228 | "language_info": { 229 | "codemirror_mode": { 230 | "name": "ipython", 231 | "version": 3 232 | }, 233 | "file_extension": ".py", 234 | "mimetype": "text/x-python", 235 | "name": "python", 236 | "nbconvert_exporter": "python", 237 | "pygments_lexer": "ipython3", 238 | "version": "3.10.12" 239 | } 240 | }, 241 | "nbformat": 4, 242 | "nbformat_minor": 2 243 | } 244 | -------------------------------------------------------------------------------- /tracetools_analysis/analysis/lifecycle_states.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# Lifecycle node states\n", 10 | "#\n", 11 | "# Get trace data using the provided launch file:\n", 12 | "# $ ros2 launch tracetools_analysis lifecycle_states.launch.py" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "path = '~/.ros/tracing/lifecycle-node-state/'" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "import sys\n", 31 | "# Add paths to tracetools_analysis and tracetools_read.\n", 32 | "# There are two options:\n", 33 | "# 1. from source, assuming a workspace with:\n", 34 | "# src/tracetools_analysis/\n", 35 | "# src/ros2/ros2_tracing/tracetools_read/\n", 36 | "sys.path.insert(0, '../')\n", 37 | "sys.path.insert(0, '../../../ros2/ros2_tracing/tracetools_read/')\n", 38 | "# 2. from Debian packages, setting the right ROS 2 distro:\n", 39 | "#ROS_DISTRO = 'rolling'\n", 40 | "#sys.path.insert(0, f'/opt/ros/{ROS_DISTRO}/lib/python3.8/site-packages')\n", 41 | "import datetime as dt\n", 42 | "\n", 43 | "from bokeh.palettes import Category10\n", 44 | "from bokeh.plotting import figure\n", 45 | "from bokeh.plotting import output_notebook\n", 46 | "from bokeh.io import show\n", 47 | "from bokeh.layouts import row\n", 48 | "from bokeh.models import ColumnDataSource\n", 49 | "from bokeh.models import DatetimeTickFormatter\n", 50 | "from bokeh.models import PrintfTickFormatter\n", 51 | "import numpy as np\n", 52 | "import pandas as pd\n", 53 | "\n", 54 | "from tracetools_analysis.loading import load_file\n", 55 | "from tracetools_analysis.processor.ros2 import Ros2Handler\n", 56 | "from tracetools_analysis.utils.ros2 import Ros2DataModelUtil" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": null, 62 | "metadata": {}, 63 | "outputs": [], 64 | "source": [ 65 | "# Process\n", 66 | "events = load_file(path)\n", 67 | "handler = Ros2Handler.process(events)\n", 68 | "#handler.data.print_data()" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": null, 74 | "metadata": {}, 75 | "outputs": [], 76 | "source": [ 77 | "data_util = Ros2DataModelUtil(handler.data)\n", 78 | "\n", 79 | "state_intervals = data_util.get_lifecycle_node_state_intervals()\n", 80 | "for handle, states in state_intervals.items():\n", 81 | " print(handle)\n", 82 | " print(states.to_string())\n", 83 | "\n", 84 | "output_notebook()\n", 85 | "psize = 450" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": null, 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "# Plot\n", 95 | "colors = Category10[10]\n", 96 | "\n", 97 | "lifecycle_node_names = {\n", 98 | " handle: data_util.get_lifecycle_node_handle_info(handle)['lifecycle node'] for handle in state_intervals.keys()\n", 99 | "}\n", 100 | "states_labels = []\n", 101 | "start_times = []\n", 102 | "\n", 103 | "fig = figure(\n", 104 | " y_range=list(lifecycle_node_names.values()),\n", 105 | " title='Lifecycle states over time',\n", 106 | " y_axis_label='node',\n", 107 | " plot_width=psize*2, plot_height=psize,\n", 108 | ")\n", 109 | "\n", 110 | "for lifecycle_node_handle, states in state_intervals.items():\n", 111 | " lifecycle_node_name = lifecycle_node_names[lifecycle_node_handle]\n", 112 | "\n", 113 | " start_times.append(states['start_timestamp'].iloc[0])\n", 114 | " for index, row in states.iterrows():\n", 115 | " # TODO fix end\n", 116 | " if index == max(states.index):\n", 117 | " continue\n", 118 | " start = row['start_timestamp']\n", 119 | " end = row['end_timestamp']\n", 120 | " state = row['state']\n", 121 | " if state not in states_labels:\n", 122 | " states_labels.append(state)\n", 123 | " state_index = states_labels.index(state)\n", 124 | " fig.line(\n", 125 | " x=[start, end],\n", 126 | " y=[lifecycle_node_name]*2,\n", 127 | " line_width=10.0,\n", 128 | " line_color=colors[state_index],\n", 129 | " legend_label=state,\n", 130 | " )\n", 131 | "\n", 132 | "fig.title.align = 'center'\n", 133 | "fig.xaxis[0].formatter = DatetimeTickFormatter(seconds=['%Ss'])\n", 134 | "fig.xaxis[0].axis_label = 'time (' + min(start_times).strftime('%Y-%m-%d %H:%M') + ')'\n", 135 | "show(fig)" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": null, 141 | "metadata": {}, 142 | "outputs": [], 143 | "source": [] 144 | } 145 | ], 146 | "metadata": { 147 | "kernelspec": { 148 | "display_name": "Python 3 (ipykernel)", 149 | "language": "python", 150 | "name": "python3" 151 | }, 152 | "language_info": { 153 | "codemirror_mode": { 154 | "name": "ipython", 155 | "version": 3 156 | }, 157 | "file_extension": ".py", 158 | "mimetype": "text/x-python", 159 | "name": "python", 160 | "nbconvert_exporter": "python", 161 | "pygments_lexer": "ipython3", 162 | "version": "3.10.6" 163 | } 164 | }, 165 | "nbformat": 4, 166 | "nbformat_minor": 2 167 | } 168 | -------------------------------------------------------------------------------- /tracetools_analysis/analysis/memory_usage.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# Memory usage\n", 10 | "#\n", 11 | "# Get trace data using the provided launch file:\n", 12 | "# $ ros2 launch tracetools_analysis memory_usage.launch.py\n", 13 | "# (wait at least a few seconds, then kill with Ctrl+C)" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": null, 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "path = '~/.ros/tracing/memory-usage'" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "import sys\n", 32 | "# Add paths to tracetools_analysis and tracetools_read.\n", 33 | "# There are two options:\n", 34 | "# 1. from source, assuming a workspace with:\n", 35 | "# src/tracetools_analysis/\n", 36 | "# src/ros2/ros2_tracing/tracetools_read/\n", 37 | "sys.path.insert(0, '../')\n", 38 | "sys.path.insert(0, '../../../ros2/ros2_tracing/tracetools_read/')\n", 39 | "# 2. from Debian packages, setting the right ROS 2 distro:\n", 40 | "#ROS_DISTRO = 'rolling'\n", 41 | "#sys.path.insert(0, f'/opt/ros/{ROS_DISTRO}/lib/python3.8/site-packages')\n", 42 | "import datetime as dt\n", 43 | "\n", 44 | "from bokeh.palettes import viridis\n", 45 | "from bokeh.plotting import figure\n", 46 | "from bokeh.plotting import output_notebook\n", 47 | "from bokeh.io import show\n", 48 | "from bokeh.models import ColumnDataSource\n", 49 | "from bokeh.models import DatetimeTickFormatter\n", 50 | "from bokeh.models import NumeralTickFormatter\n", 51 | "import numpy as np\n", 52 | "import pandas as pd\n", 53 | "\n", 54 | "from tracetools_analysis.loading import load_file\n", 55 | "from tracetools_analysis.processor import Processor\n", 56 | "from tracetools_analysis.processor.memory_usage import KernelMemoryUsageHandler\n", 57 | "from tracetools_analysis.processor.memory_usage import UserspaceMemoryUsageHandler\n", 58 | "from tracetools_analysis.processor.ros2 import Ros2Handler\n", 59 | "from tracetools_analysis.utils.memory_usage import MemoryUsageDataModelUtil\n", 60 | "from tracetools_analysis.utils.ros2 import Ros2DataModelUtil" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "# Process\n", 70 | "events = load_file(path)\n", 71 | "ust_memory_handler = UserspaceMemoryUsageHandler()\n", 72 | "kernel_memory_handler = KernelMemoryUsageHandler()\n", 73 | "ros2_handler = Ros2Handler()\n", 74 | "Processor(ust_memory_handler, kernel_memory_handler, ros2_handler).process(events)" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "memory_data_util = MemoryUsageDataModelUtil(\n", 84 | " userspace=ust_memory_handler.data,\n", 85 | " kernel=kernel_memory_handler.data,\n", 86 | ")\n", 87 | "ros2_data_util = Ros2DataModelUtil(ros2_handler.data)\n", 88 | "\n", 89 | "output_notebook()\n", 90 | "psize = 650" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": null, 96 | "metadata": {}, 97 | "outputs": [], 98 | "source": [ 99 | "# Plot memory usage\n", 100 | "ust_memory_usage_dfs = memory_data_util.get_absolute_userspace_memory_usage_by_tid()\n", 101 | "kernel_memory_usage_dfs = memory_data_util.get_absolute_kernel_memory_usage_by_tid()\n", 102 | "tids = ros2_data_util.get_tids()\n", 103 | "\n", 104 | "colours = viridis(len(tids) + 1)\n", 105 | "first_tid = min(tids)\n", 106 | "starttime = ust_memory_usage_dfs[first_tid].loc[:, 'timestamp'].iloc[0].strftime('%Y-%m-%d %H:%M')\n", 107 | "memory = figure(\n", 108 | " title='Memory usage per thread/node',\n", 109 | " x_axis_label=f'time ({starttime})',\n", 110 | " y_axis_label='memory usage',\n", 111 | " plot_width=psize, plot_height=psize,\n", 112 | ")\n", 113 | "\n", 114 | "i_colour = 0\n", 115 | "for tid in tids:\n", 116 | " legend = str(tid) + ' ' + str(ros2_data_util.get_node_names_from_tid(tid))\n", 117 | " # Userspace\n", 118 | " memory.line(\n", 119 | " x='timestamp',\n", 120 | " y='memory_usage',\n", 121 | " legend=legend + ' (ust)',\n", 122 | " line_width=2,\n", 123 | " source=ColumnDataSource(ust_memory_usage_dfs[tid]),\n", 124 | " line_color=colours[i_colour],\n", 125 | " )\n", 126 | " # Kernel\n", 127 | " memory.line(\n", 128 | " x='timestamp',\n", 129 | " y='memory_usage',\n", 130 | " legend=legend + ' (kernel)',\n", 131 | " line_width=2,\n", 132 | " source=ColumnDataSource(kernel_memory_usage_dfs[tid]),\n", 133 | " line_color=colours[i_colour],\n", 134 | " line_dash='dotted',\n", 135 | " )\n", 136 | " i_colour += 1\n", 137 | "\n", 138 | "memory.title.align = 'center'\n", 139 | "memory.legend.label_text_font_size = '11px'\n", 140 | "memory.xaxis[0].formatter = DatetimeTickFormatter(seconds=['%Ss'])\n", 141 | "memory.yaxis[0].formatter = NumeralTickFormatter(format='0.0b')\n", 142 | "\n", 143 | "show(memory)" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": null, 149 | "metadata": {}, 150 | "outputs": [], 151 | "source": [] 152 | } 153 | ], 154 | "metadata": { 155 | "kernelspec": { 156 | "display_name": "Python 3 (ipykernel)", 157 | "language": "python", 158 | "name": "python3" 159 | }, 160 | "language_info": { 161 | "codemirror_mode": { 162 | "name": "ipython", 163 | "version": 3 164 | }, 165 | "file_extension": ".py", 166 | "mimetype": "text/x-python", 167 | "name": "python", 168 | "nbconvert_exporter": "python", 169 | "pygments_lexer": "ipython3", 170 | "version": "3.10.6" 171 | } 172 | }, 173 | "nbformat": 4, 174 | "nbformat_minor": 2 175 | } 176 | -------------------------------------------------------------------------------- /tracetools_analysis/analysis/sample_data/converted_pingpong: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ros-tracing/tracetools_analysis/d2bd9bae0400b83404e22e1d5fe7ecb6982bfaa8/tracetools_analysis/analysis/sample_data/converted_pingpong -------------------------------------------------------------------------------- /tracetools_analysis/docs/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /tracetools_analysis/docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /tracetools_analysis/docs/source/about.rst: -------------------------------------------------------------------------------- 1 | About 2 | ===== 3 | 4 | Tools for analyzing trace data from ROS 2 systems generated by the `ros2_tracing packages `_. 5 | -------------------------------------------------------------------------------- /tracetools_analysis/docs/source/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | api/tracetools_analysis 9 | -------------------------------------------------------------------------------- /tracetools_analysis/docs/source/api/tracetools_analysis.rst: -------------------------------------------------------------------------------- 1 | tracetools_analysis 2 | =================== 3 | 4 | .. automodule:: tracetools_analysis 5 | 6 | loading 7 | ####### 8 | 9 | .. automodule:: tracetools_analysis.loading 10 | 11 | processor 12 | ######### 13 | 14 | .. automodule:: tracetools_analysis.processor 15 | 16 | CPU time 17 | ******** 18 | 19 | .. automodule:: tracetools_analysis.processor.cpu_time 20 | 21 | memory usage 22 | ************ 23 | 24 | .. automodule:: tracetools_analysis.processor.memory_usage 25 | 26 | profile 27 | ******* 28 | 29 | .. automodule:: tracetools_analysis.processor.profile 30 | 31 | ROS 2 32 | ***** 33 | 34 | .. automodule:: tracetools_analysis.processor.ros2 35 | 36 | data model 37 | ########## 38 | 39 | .. automodule:: tracetools_analysis.data_model 40 | 41 | CPU time 42 | ******** 43 | 44 | .. automodule:: tracetools_analysis.data_model.cpu_time 45 | 46 | memory usage 47 | ************ 48 | 49 | .. automodule:: tracetools_analysis.data_model.memory_usage 50 | 51 | profile 52 | ******* 53 | 54 | .. automodule:: tracetools_analysis.data_model.profile 55 | 56 | ROS 2 57 | ***** 58 | 59 | .. automodule:: tracetools_analysis.data_model.ros2 60 | 61 | utils 62 | ##### 63 | 64 | .. automodule:: tracetools_analysis.utils 65 | 66 | CPU time 67 | ******** 68 | 69 | .. automodule:: tracetools_analysis.utils.cpu_time 70 | 71 | memory usage 72 | ************ 73 | 74 | .. automodule:: tracetools_analysis.utils.memory_usage 75 | 76 | profile 77 | ******* 78 | 79 | .. automodule:: tracetools_analysis.utils.profile 80 | 81 | ROS 2 82 | ***** 83 | 84 | .. automodule:: tracetools_analysis.utils.ros2 85 | -------------------------------------------------------------------------------- /tracetools_analysis/docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # -*- coding: utf-8 -*- 16 | # 17 | # Configuration file for the Sphinx documentation builder. 18 | # 19 | # This file does only contain a selection of the most common options. For a 20 | # full list see the documentation: 21 | # http://www.sphinx-doc.org/en/master/config 22 | 23 | # -- Path setup -------------------------------------------------------------- 24 | 25 | # If extensions (or modules to document with autodoc) are in another directory, 26 | # add these directories to sys.path here. If the directory is relative to the 27 | # documentation root, use os.path.abspath to make it absolute, like shown here. 28 | # 29 | import os 30 | import sys 31 | # tracetools_analysis 32 | sys.path.insert(0, os.path.abspath('../../')) 33 | 34 | 35 | # -- Project information ----------------------------------------------------- 36 | 37 | project = 'tracetools_analysis' 38 | copyright = '2019-2020, Robert Bosch GmbH & Christophe Bedard' # noqa 39 | author = 'Robert Bosch GmbH, Christophe Bedard' 40 | 41 | # The short X.Y version 42 | version = '' 43 | # The full version, including alpha/beta/rc tags 44 | release = '1.0.1' 45 | 46 | 47 | # -- General configuration --------------------------------------------------- 48 | 49 | # If your documentation needs a minimal Sphinx version, state it here. 50 | # 51 | # needs_sphinx = '1.0' 52 | 53 | # Add any Sphinx extension module names here, as strings. They can be 54 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 55 | # ones. 56 | extensions = [ 57 | 'sphinx.ext.autodoc', 58 | 'sphinx_autodoc_typehints', 59 | 'sphinx.ext.autosummary', 60 | 'sphinx.ext.doctest', 61 | 'sphinx.ext.coverage', 62 | ] 63 | 64 | # Add any paths that contain templates here, relative to this directory. 65 | templates_path = ['_templates'] 66 | 67 | # The suffix(es) of source filenames. 68 | # You can specify multiple suffix as a list of string: 69 | # 70 | # source_suffix = ['.rst', '.md'] 71 | source_suffix = '.rst' 72 | 73 | # The master toctree document. 74 | master_doc = 'index' 75 | 76 | # The language for content autogenerated by Sphinx. Refer to documentation 77 | # for a list of supported languages. 78 | # 79 | # This is also used if you do content translation via gettext catalogs. 80 | # Usually you set "language" from the command line for these cases. 81 | language = None 82 | 83 | # List of patterns, relative to source directory, that match files and 84 | # directories to ignore when looking for source files. 85 | # This pattern also affects html_static_path and html_extra_path. 86 | # exclude_patterns = [] 87 | 88 | # The name of the Pygments (syntax highlighting) style to use. 89 | pygments_style = None 90 | 91 | 92 | # -- Options for HTML output ------------------------------------------------- 93 | 94 | # The theme to use for HTML and HTML Help pages. See the documentation for 95 | # a list of builtin themes. 96 | # 97 | html_theme = 'alabaster' 98 | 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | # 103 | html_theme_options = { 104 | 'sidebar_width': '260px', 105 | } 106 | 107 | # Add any paths that contain custom static files (such as style sheets) here, 108 | # relative to this directory. They are copied after the builtin static files, 109 | # so a file named "default.css" will overwrite the builtin "default.css". 110 | # html_static_path = [] 111 | 112 | # Custom sidebar templates, must be a dictionary that maps document names 113 | # to template names. 114 | # 115 | # The default sidebars (for documents that don't match any pattern) are 116 | # defined by theme itself. Builtin themes are using these templates by 117 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 118 | # 'searchbox.html']``. 119 | # 120 | # html_sidebars = {} 121 | 122 | 123 | # -- Options for HTMLHelp output --------------------------------------------- 124 | 125 | # Output file base name for HTML help builder. 126 | htmlhelp_basename = 'tracetools_analysis-doc' 127 | 128 | 129 | # -- Options for LaTeX output ------------------------------------------------ 130 | 131 | # latex_elements = { 132 | # The paper size ('letterpaper' or 'a4paper'). 133 | # 134 | # 'papersize': 'letterpaper', 135 | 136 | # The font size ('10pt', '11pt' or '12pt'). 137 | # 138 | # 'pointsize': '10pt', 139 | 140 | # Additional stuff for the LaTeX preamble. 141 | # 142 | # 'preamble': '', 143 | 144 | # Latex figure (float) alignment 145 | # 146 | # 'figure_align': 'htbp', 147 | # } 148 | 149 | # Grouping the document tree into LaTeX files. List of tuples 150 | # (source start file, target name, title, 151 | # author, documentclass [howto, manual, or own class]). 152 | # latex_documents = [ 153 | # (master_doc, 'rclpy.tex', 'rclpy Documentation', 154 | # 'Esteve Fernandez', 'manual'), 155 | # ] 156 | 157 | 158 | # -- Options for manual page output ------------------------------------------ 159 | 160 | # One entry per manual page. List of tuples 161 | # (source start file, name, description, authors, manual section). 162 | # man_pages = [ 163 | # (master_doc, 'rclpy', 'rclpy Documentation', 164 | # [author], 1) 165 | # ] 166 | 167 | 168 | # -- Options for Texinfo output ---------------------------------------------- 169 | 170 | # Grouping the document tree into Texinfo files. List of tuples 171 | # (source start file, target name, title, author, 172 | # dir menu entry, description, category) 173 | # texinfo_documents = [ 174 | # (master_doc, 'rclpy', 'rclpy Documentation', 175 | # author, 'rclpy', 'One line description of project.', 176 | # 'Miscellaneous'), 177 | # ] 178 | 179 | 180 | # -- Options for Epub output ------------------------------------------------- 181 | 182 | # Bibliographic Dublin Core info. 183 | epub_title = project 184 | 185 | # The unique identifier of the text. This can be a ISBN number 186 | # or the project homepage. 187 | # 188 | # epub_identifier = '' 189 | 190 | # A unique identification for the text. 191 | # 192 | # epub_uid = '' 193 | 194 | # A list of files that should not be packed into the epub file. 195 | # epub_exclude_files = ['search.html'] 196 | 197 | 198 | # -- Extension configuration ------------------------------------------------- 199 | 200 | # Ignore these 201 | autodoc_mock_imports = [ 202 | 'tracetools_read', 203 | ] 204 | 205 | autoclass_content = 'both' 206 | 207 | autodoc_default_options = { 208 | 'members': None, 209 | 'undoc-members': True, 210 | } 211 | -------------------------------------------------------------------------------- /tracetools_analysis/docs/source/index.rst: -------------------------------------------------------------------------------- 1 | tracetools_analysis 2 | =================== 3 | 4 | tracetools_analysis provides tools for analyzing trace data from ROS 2 systems generated by the `ros2_tracing packages `_. 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | 9 | about 10 | api 11 | 12 | Indices and tables 13 | ================== 14 | 15 | * :ref:`genindex` 16 | * :ref:`modindex` 17 | * :ref:`search` 18 | -------------------------------------------------------------------------------- /tracetools_analysis/launch/lifecycle_states.launch.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Christophe Bedard 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Example launch file for a lifecycle node state analysis.""" 16 | 17 | import launch 18 | from launch_ros.actions import Node 19 | from tracetools_launch.action import Trace 20 | 21 | 22 | def generate_launch_description(): 23 | return launch.LaunchDescription([ 24 | Trace( 25 | session_name='lifecycle-node-state', 26 | events_kernel=[], 27 | ), 28 | Node( 29 | package='test_tracetools', 30 | executable='test_lifecycle_node', 31 | output='screen', 32 | ), 33 | Node( 34 | package='test_tracetools', 35 | executable='test_lifecycle_client', 36 | output='screen', 37 | ), 38 | ]) 39 | -------------------------------------------------------------------------------- /tracetools_analysis/launch/memory_usage.launch.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Example launch file for a memory_usage analysis.""" 16 | 17 | import launch 18 | from launch_ros.actions import Node 19 | from tracetools_launch.action import Trace 20 | from tracetools_trace.tools.names import DEFAULT_EVENTS_ROS 21 | 22 | 23 | def generate_launch_description(): 24 | return launch.LaunchDescription([ 25 | Trace( 26 | session_name='memory-usage', 27 | events_ust=[ 28 | 'lttng_ust_libc:malloc', 29 | 'lttng_ust_libc:calloc', 30 | 'lttng_ust_libc:realloc', 31 | 'lttng_ust_libc:free', 32 | 'lttng_ust_libc:memalign', 33 | 'lttng_ust_libc:posix_memalign', 34 | ] + DEFAULT_EVENTS_ROS, 35 | events_kernel=[ 36 | 'kmem_mm_page_alloc', 37 | 'kmem_mm_page_free', 38 | ], 39 | ), 40 | Node( 41 | package='test_tracetools', 42 | executable='test_ping', 43 | arguments=['do_more'], 44 | output='screen', 45 | ), 46 | Node( 47 | package='test_tracetools', 48 | executable='test_pong', 49 | arguments=['do_more'], 50 | output='screen', 51 | ), 52 | ]) 53 | -------------------------------------------------------------------------------- /tracetools_analysis/launch/pingpong.launch.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Example launch file for a callback duration analysis.""" 16 | 17 | import launch 18 | from launch_ros.actions import Node 19 | from tracetools_launch.action import Trace 20 | 21 | 22 | def generate_launch_description(): 23 | return launch.LaunchDescription([ 24 | Trace( 25 | session_name='pingpong', 26 | events_kernel=[], 27 | ), 28 | Node( 29 | package='test_tracetools', 30 | executable='test_ping', 31 | arguments=['do_more'], 32 | output='screen', 33 | ), 34 | Node( 35 | package='test_tracetools', 36 | executable='test_pong', 37 | arguments=['do_more'], 38 | output='screen', 39 | ), 40 | ]) 41 | -------------------------------------------------------------------------------- /tracetools_analysis/launch/profile.launch.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Example launch file for a profiling analysis.""" 16 | 17 | import launch 18 | from launch_ros.actions import Node 19 | from tracetools_launch.action import Trace 20 | from tracetools_trace.tools.names import DEFAULT_CONTEXT 21 | from tracetools_trace.tools.names import DEFAULT_EVENTS_ROS 22 | 23 | 24 | def generate_launch_description(): 25 | return launch.LaunchDescription([ 26 | Trace( 27 | session_name='profile', 28 | events_ust=[ 29 | 'lttng_ust_cyg_profile_fast:func_entry', 30 | 'lttng_ust_cyg_profile_fast:func_exit', 31 | 'lttng_ust_statedump:start', 32 | 'lttng_ust_statedump:end', 33 | 'lttng_ust_statedump:bin_info', 34 | 'lttng_ust_statedump:build_id', 35 | ] + DEFAULT_EVENTS_ROS, 36 | events_kernel=[ 37 | 'sched_switch', 38 | ], 39 | context_fields={ 40 | 'kernel': DEFAULT_CONTEXT, 41 | 'userspace': DEFAULT_CONTEXT + ['ip'], 42 | }, 43 | ), 44 | Node( 45 | package='test_tracetools', 46 | executable='test_ping', 47 | arguments=['do_more'], 48 | output='screen', 49 | ), 50 | Node( 51 | package='test_tracetools', 52 | executable='test_pong', 53 | arguments=['do_more'], 54 | output='screen', 55 | ), 56 | ]) 57 | -------------------------------------------------------------------------------- /tracetools_analysis/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tracetools_analysis 5 | 3.1.0 6 | Tools for analysing trace data. 7 | Christophe Bedard 8 | Ingo Lütkebohle 9 | Apache 2.0 10 | https://index.ros.org/p/tracetools_analysis/ 11 | https://github.com/ros-tracing/tracetools_analysis 12 | https://github.com/ros-tracing/tracetools_analysis/issues 13 | Ingo Lütkebohle 14 | Christophe Bedard 15 | 16 | tracetools_read 17 | tracetools_trace 18 | python3-pandas 19 | 20 | jupyter-notebook 21 | 22 | ament_copyright 23 | ament_flake8 24 | ament_mypy 25 | ament_pep257 26 | ament_xmllint 27 | python3-pytest 28 | 29 | 30 | ament_python 31 | 32 | 33 | -------------------------------------------------------------------------------- /tracetools_analysis/resource/tracetools_analysis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ros-tracing/tracetools_analysis/d2bd9bae0400b83404e22e1d5fe7ecb6982bfaa8/tracetools_analysis/resource/tracetools_analysis -------------------------------------------------------------------------------- /tracetools_analysis/setup.cfg: -------------------------------------------------------------------------------- 1 | [develop] 2 | script_dir=$base/lib/tracetools_analysis 3 | [install] 4 | install_scripts=$base/lib/tracetools_analysis 5 | -------------------------------------------------------------------------------- /tracetools_analysis/setup.py: -------------------------------------------------------------------------------- 1 | import glob 2 | 3 | from setuptools import find_packages 4 | from setuptools import setup 5 | 6 | package_name = 'tracetools_analysis' 7 | 8 | setup( 9 | name=package_name, 10 | version='3.1.0', 11 | packages=find_packages(exclude=['test']), 12 | data_files=[ 13 | ('share/' + package_name, ['package.xml']), 14 | ('share/' + package_name + '/launch', glob.glob('launch/*.launch.py')), 15 | ('share/ament_index/resource_index/packages', 16 | ['resource/' + package_name]), 17 | ], 18 | install_requires=['setuptools'], 19 | maintainer=( 20 | 'Christophe Bedard, ' 21 | 'Ingo Lütkebohle' 22 | ), 23 | maintainer_email=( 24 | 'bedard.christophe@gmail.com, ' 25 | 'ingo.luetkebohle@de.bosch.com' 26 | ), 27 | author=( 28 | 'Christophe Bedard, ' 29 | 'Ingo Lütkebohle' 30 | ), 31 | author_email=( 32 | 'fixed-term.christophe.bourquebedard@de.bosch.com, ' 33 | 'ingo.luetkebohle@de.bosch.com' 34 | ), 35 | url='https://github.com/ros-tracing/tracetools_analysis', 36 | keywords=[], 37 | description='Tools for analysing trace data.', 38 | long_description=( 39 | 'This package provides tools for analysing trace data, from ' 40 | 'building a model of the trace data to providing plotting utilities.' 41 | ), 42 | entry_points={ 43 | 'console_scripts': [ 44 | f'convert = {package_name}.convert:main', 45 | f'process = {package_name}.process:main', 46 | f'auto = {package_name}.scripts.auto:main', 47 | f'cb_durations = {package_name}.scripts.cb_durations:main', 48 | f'memory_usage = {package_name}.scripts.memory_usage:main', 49 | ], 50 | }, 51 | license='Apache 2.0', 52 | tests_require=['pytest'], 53 | ) 54 | -------------------------------------------------------------------------------- /tracetools_analysis/test/test_copyright.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_copyright.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.copyright 20 | @pytest.mark.linter 21 | def test_copyright(): 22 | rc = main(argv=['.', 'test']) 23 | assert rc == 0, 'Found errors' 24 | -------------------------------------------------------------------------------- /tracetools_analysis/test/test_flake8.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_flake8.main import main_with_errors 16 | import pytest 17 | 18 | 19 | @pytest.mark.flake8 20 | @pytest.mark.linter 21 | def test_flake8(): 22 | rc, errors = main_with_errors(argv=[]) 23 | assert rc == 0, \ 24 | 'Found %d code style errors / warnings:\n' % len(errors) + \ 25 | '\n'.join(errors) 26 | -------------------------------------------------------------------------------- /tracetools_analysis/test/test_mypy.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Canonical Ltd 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_mypy.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.mypy 20 | @pytest.mark.linter 21 | def test_mypy(): 22 | assert main(argv=[]) == 0, 'Found errors' 23 | -------------------------------------------------------------------------------- /tracetools_analysis/test/test_pep257.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_pep257.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.linter 20 | @pytest.mark.pep257 21 | def test_pep257(): 22 | rc = main(argv=[]) 23 | assert rc == 0, 'Found code style errors / warnings' 24 | -------------------------------------------------------------------------------- /tracetools_analysis/test/test_xmllint.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_xmllint.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.linter 20 | @pytest.mark.xmllint 21 | def test_xmllint(): 22 | rc = main(argv=[]) 23 | assert rc == 0, 'Found errors' 24 | -------------------------------------------------------------------------------- /tracetools_analysis/test/tracetools_analysis/test_autoprocessor.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Apex.AI, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Dict 16 | from typing import Set 17 | import unittest 18 | 19 | from tracetools_analysis.processor import AutoProcessor 20 | from tracetools_analysis.processor import EventHandler 21 | from tracetools_analysis.processor import EventMetadata 22 | from tracetools_analysis.processor import HandlerMap 23 | 24 | 25 | class AbstractEventHandler(EventHandler): 26 | 27 | def __init__(self, **kwargs) -> None: 28 | if type(self) is AbstractEventHandler: 29 | raise RuntimeError() 30 | super().__init__(**kwargs) 31 | 32 | 33 | class SubSubEventHandler(AbstractEventHandler): 34 | 35 | def __init__(self) -> None: 36 | handler_map: HandlerMap = { 37 | 'myeventname': self._handler_whatever, 38 | 'myeventname69': self._handler_whatever, 39 | } 40 | super().__init__(handler_map=handler_map) 41 | 42 | @staticmethod 43 | def required_events() -> Set[str]: 44 | return { 45 | 'myeventname', 46 | 'myeventname69', 47 | } 48 | 49 | def _handler_whatever( 50 | self, event: Dict, metadata: EventMetadata 51 | ) -> None: 52 | pass 53 | 54 | 55 | class SubSubEventHandler2(AbstractEventHandler): 56 | 57 | def __init__(self) -> None: 58 | handler_map: HandlerMap = { 59 | 'myeventname2': self._handler_whatever, 60 | } 61 | super().__init__(handler_map=handler_map) 62 | 63 | @staticmethod 64 | def required_events() -> Set[str]: 65 | return { 66 | 'myeventname2', 67 | } 68 | 69 | def _handler_whatever( 70 | self, event: Dict, metadata: EventMetadata 71 | ) -> None: 72 | pass 73 | 74 | 75 | class SubEventHandler(EventHandler): 76 | 77 | def __init__(self) -> None: 78 | handler_map: HandlerMap = { 79 | 'myeventname3': self._handler_whatever, 80 | } 81 | super().__init__(handler_map=handler_map) 82 | 83 | @staticmethod 84 | def required_events() -> Set[str]: 85 | return { 86 | 'myeventname3', 87 | } 88 | 89 | def _handler_whatever( 90 | self, event: Dict, metadata: EventMetadata 91 | ) -> None: 92 | pass 93 | 94 | 95 | class TestAutoProcessor(unittest.TestCase): 96 | 97 | def __init__(self, *args) -> None: 98 | super().__init__( 99 | *args, 100 | ) 101 | 102 | def test_separate_methods(self) -> None: 103 | # Testing logic/methods separately, since we don't actually want to process 104 | 105 | # Getting subclasses 106 | subclasses = AutoProcessor._get_subclasses(EventHandler) 107 | # Will also contain the real classes 108 | self.assertTrue( 109 | all( 110 | handler in subclasses 111 | for handler in [ 112 | AbstractEventHandler, 113 | SubSubEventHandler, 114 | SubSubEventHandler2, 115 | SubEventHandler, 116 | ] 117 | ) 118 | ) 119 | 120 | # Finding applicable classes 121 | event_names = { 122 | 'myeventname', 123 | 'myeventname2', 124 | 'myeventname3', 125 | } 126 | applicable_handler_classes = AutoProcessor._get_applicable_event_handler_classes( 127 | event_names, 128 | subclasses, 129 | ) 130 | self.assertTrue( 131 | all( 132 | handler in applicable_handler_classes 133 | for handler in [ 134 | AbstractEventHandler, 135 | SubSubEventHandler2, 136 | SubEventHandler, 137 | ] 138 | ) and 139 | SubSubEventHandler not in applicable_handler_classes 140 | ) 141 | 142 | # Creating instances 143 | instances = AutoProcessor._get_event_handler_instances(applicable_handler_classes) 144 | for instance in instances: 145 | self.assertTrue(type(instance) is not AbstractEventHandler) 146 | 147 | def test_all(self) -> None: 148 | # Test the main method with all the logic 149 | events = [ 150 | { 151 | '_name': 'myeventname', 152 | '_timestamp': 0, 153 | 'cpu_id': 0, 154 | }, 155 | { 156 | '_name': 'myeventname2', 157 | '_timestamp': 69, 158 | 'cpu_id': 0, 159 | }, 160 | { 161 | '_name': 'myeventname3', 162 | '_timestamp': 6969, 163 | 'cpu_id': 0, 164 | }, 165 | ] 166 | instances = AutoProcessor.get_applicable_event_handlers(events) 167 | for instance in instances: 168 | self.assertTrue(type(instance) is not AbstractEventHandler) 169 | # Will also contain the real classes 170 | self.assertEqual( 171 | sum( 172 | isinstance(instance, handler_class) 173 | for handler_class in [SubEventHandler, SubSubEventHandler2] 174 | for instance in instances 175 | ), 176 | 2, 177 | ) 178 | 179 | 180 | if __name__ == '__main__': 181 | unittest.main() 182 | -------------------------------------------------------------------------------- /tracetools_analysis/test/tracetools_analysis/test_data_model_util.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from datetime import datetime 16 | from datetime import timezone 17 | from typing import Dict 18 | import unittest 19 | 20 | from packaging.version import Version 21 | from pandas import __version__ as pandas_version 22 | from pandas import DataFrame 23 | from pandas.testing import assert_frame_equal 24 | 25 | from tracetools_analysis.data_model import DataModel 26 | from tracetools_analysis.processor import EventHandler 27 | from tracetools_analysis.processor import EventMetadata 28 | from tracetools_analysis.processor import HandlerMap 29 | from tracetools_analysis.utils import DataModelUtil 30 | 31 | 32 | class TestDataModelUtil(unittest.TestCase): 33 | 34 | def __init__(self, *args) -> None: 35 | super().__init__( 36 | *args, 37 | ) 38 | 39 | @unittest.skipIf( 40 | Version(pandas_version) < Version('2.2.0'), 41 | 'skip due to missing fix: pandas-dev/pandas#55812', 42 | ) 43 | def test_convert_time_columns(self) -> None: 44 | input_df = DataFrame( 45 | data=[ 46 | { 47 | 'timestamp': 1565177400000*1000000, 48 | 'random_thingy': 'abc', 49 | 'some_duration': 3000000, 50 | 'const_number': 123456, 51 | }, 52 | { 53 | 'timestamp': 946684800000*1000000, 54 | 'random_thingy': 'def', 55 | 'some_duration': 10000000, 56 | 'const_number': 789101112, 57 | }, 58 | ], 59 | ) 60 | expected_df = DataFrame( 61 | data=[ 62 | { 63 | 'timestamp': datetime(2019, 8, 7, 11, 30, 0, tzinfo=timezone.utc), 64 | 'random_thingy': 'abc', 65 | 'some_duration': 3, 66 | 'const_number': 123456, 67 | }, 68 | { 69 | 'timestamp': datetime(2000, 1, 1, 0, 0, 0, tzinfo=timezone.utc), 70 | 'random_thingy': 'def', 71 | 'some_duration': 10, 72 | 'const_number': 789101112, 73 | }, 74 | ], 75 | ) 76 | result_df = DataModelUtil.convert_time_columns( 77 | input_df, 78 | ['some_duration'], 79 | ['timestamp'], 80 | inplace=True, 81 | ) 82 | assert_frame_equal(result_df, expected_df, check_dtype=False) 83 | 84 | def test_compute_column_difference(self) -> None: 85 | input_df = DataFrame( 86 | data=[ 87 | { 88 | 'a': 10, 89 | 'b': 13, 90 | 'c': 1, 91 | }, 92 | { 93 | 'a': 1, 94 | 'b': 3, 95 | 'c': 69, 96 | }, 97 | ], 98 | ) 99 | expected_df = DataFrame( 100 | data=[ 101 | { 102 | 'a': 10, 103 | 'b': 13, 104 | 'c': 1, 105 | 'diff': 3, 106 | }, 107 | { 108 | 'a': 1, 109 | 'b': 3, 110 | 'c': 69, 111 | 'diff': 2, 112 | }, 113 | ], 114 | ) 115 | DataModelUtil.compute_column_difference( 116 | input_df, 117 | 'b', 118 | 'a', 119 | 'diff', 120 | ) 121 | assert_frame_equal(input_df, expected_df) 122 | 123 | def handler_whatever( 124 | self, event: Dict, metadata: EventMetadata 125 | ) -> None: 126 | pass 127 | 128 | def test_creation(self) -> None: 129 | handler_map: HandlerMap = {'fake': self.handler_whatever} 130 | data_model = DataModel() 131 | 132 | # Should handle the event handler not having any data model 133 | handler_none = EventHandler( 134 | handler_map=handler_map, 135 | ) 136 | data_model_util_none = DataModelUtil(handler_none) 137 | self.assertIsNone(data_model_util_none.data) 138 | 139 | # Should work when given an event handler with a data model 140 | handler_data = EventHandler( 141 | handler_map=handler_map, 142 | data_model=data_model, 143 | ) 144 | data_model_util_data = DataModelUtil(handler_data) 145 | self.assertTrue(data_model_util_data.data is data_model) 146 | 147 | # Should work when given a data model directly 148 | handler_data_direct = EventHandler( 149 | handler_map=handler_map, 150 | data_model=data_model, 151 | ) 152 | data_model_util_direct = DataModelUtil(handler_data_direct.data) 153 | self.assertTrue(data_model_util_direct.data is data_model) 154 | 155 | 156 | if __name__ == '__main__': 157 | unittest.main() 158 | -------------------------------------------------------------------------------- /tracetools_analysis/test/tracetools_analysis/test_dependency_solver.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import unittest 16 | 17 | from tracetools_analysis.processor import Dependant 18 | from tracetools_analysis.processor import DependencySolver 19 | 20 | 21 | class DepEmtpy(Dependant): 22 | 23 | def __init__(self, **kwargs) -> None: 24 | self.myparam = kwargs.get('myparam', None) 25 | 26 | 27 | class DepOne(Dependant): 28 | 29 | @staticmethod 30 | def dependencies(): 31 | return [DepEmtpy] 32 | 33 | 34 | class DepOne2(Dependant): 35 | 36 | @staticmethod 37 | def dependencies(): 38 | return [DepEmtpy] 39 | 40 | 41 | class DepTwo(Dependant): 42 | 43 | @staticmethod 44 | def dependencies(): 45 | return [DepOne, DepOne2] 46 | 47 | 48 | class TestDependencySolver(unittest.TestCase): 49 | 50 | def __init__(self, *args) -> None: 51 | super().__init__( 52 | *args, 53 | ) 54 | 55 | def test_single_dep(self) -> None: 56 | depone_instance = DepOne() 57 | 58 | # DepEmtpy should be added before 59 | solution = DependencySolver(depone_instance).solve() 60 | self.assertEqual(len(solution), 2, 'solution length invalid') 61 | self.assertIsInstance(solution[0], DepEmtpy) 62 | self.assertIs(solution[1], depone_instance) 63 | 64 | def test_single_dep_existing(self) -> None: 65 | depempty_instance = DepEmtpy() 66 | depone_instance = DepOne() 67 | 68 | # Already in order 69 | solution = DependencySolver(depempty_instance, depone_instance).solve() 70 | self.assertEqual(len(solution), 2, 'solution length invalid') 71 | self.assertIs(solution[0], depempty_instance, 'wrong solution order') 72 | self.assertIs(solution[1], depone_instance, 'wrong solution order') 73 | 74 | # Out of order 75 | solution = DependencySolver(depone_instance, depempty_instance).solve() 76 | self.assertEqual(len(solution), 2, 'solution length invalid') 77 | self.assertIs(solution[0], depempty_instance, 'solution does not use existing instance') 78 | self.assertIs(solution[1], depone_instance, 'solution does not use existing instance') 79 | 80 | def test_duplicate_dependency(self) -> None: 81 | deptwo_instance = DepTwo() 82 | 83 | # DepOne and DepOne2 both depend on DepEmpty 84 | solution = DependencySolver(deptwo_instance).solve() 85 | self.assertEqual(len(solution), 4, 'solution length invalid') 86 | self.assertIsInstance(solution[0], DepEmtpy) 87 | self.assertIsInstance(solution[1], DepOne) 88 | self.assertIsInstance(solution[2], DepOne2) 89 | self.assertIs(solution[3], deptwo_instance) 90 | 91 | # Existing instance of DepEmpty, in order 92 | depempty_instance = DepEmtpy() 93 | solution = DependencySolver(depempty_instance, deptwo_instance).solve() 94 | self.assertEqual(len(solution), 4, 'solution length invalid') 95 | self.assertIsInstance(solution[0], DepEmtpy) 96 | self.assertIsInstance(solution[1], DepOne) 97 | self.assertIsInstance(solution[2], DepOne2) 98 | self.assertIs(solution[3], deptwo_instance) 99 | 100 | # Existing instance of DepEmpty, not in order 101 | solution = DependencySolver(deptwo_instance, depempty_instance).solve() 102 | self.assertEqual(len(solution), 4, 'solution length invalid') 103 | self.assertIsInstance(solution[0], DepEmtpy) 104 | self.assertIsInstance(solution[1], DepOne) 105 | self.assertIsInstance(solution[2], DepOne2) 106 | self.assertIs(solution[3], deptwo_instance) 107 | 108 | def test_kwargs(self) -> None: 109 | depone_instance = DepOne() 110 | 111 | # Pass parameter and check that the new instance has it 112 | solution = DependencySolver(depone_instance, myparam='myvalue').solve() 113 | self.assertEqual(len(solution), 2, 'solution length invalid') 114 | self.assertEqual(solution[0].myparam, 'myvalue', 'parameter not passed on') # type: ignore 115 | 116 | 117 | if __name__ == '__main__': 118 | unittest.main() 119 | -------------------------------------------------------------------------------- /tracetools_analysis/test/tracetools_analysis/test_loading.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright 2019 Apex.AI, Inc. 3 | # Copyright 2020 Christophe Bedard 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import contextlib 18 | from io import StringIO 19 | import os 20 | import shutil 21 | import tempfile 22 | import unittest 23 | 24 | from tracetools_analysis.loading import _inspect_input_path 25 | 26 | 27 | class TestLoading(unittest.TestCase): 28 | 29 | def __init__(self, *args) -> None: 30 | super().__init__( 31 | *args, 32 | ) 33 | 34 | def setUp(self): 35 | self.test_dir_path = tempfile.mkdtemp() 36 | 37 | # Create directory that contains a 'converted' file 38 | self.with_converted_file_dir = os.path.join( 39 | self.test_dir_path, 40 | 'with_converted_file', 41 | ) 42 | os.mkdir(self.with_converted_file_dir) 43 | self.converted_file_path = os.path.join( 44 | self.with_converted_file_dir, 45 | 'converted', 46 | ) 47 | open(self.converted_file_path, 'a').close() 48 | self.assertTrue(os.path.exists(self.converted_file_path)) 49 | 50 | # Create directory that contains a file with another name that is not 'converted' 51 | self.without_converted_file_dir = os.path.join( 52 | self.test_dir_path, 53 | 'without_converted_file', 54 | ) 55 | os.mkdir(self.without_converted_file_dir) 56 | self.random_file_path = os.path.join( 57 | self.without_converted_file_dir, 58 | 'a_file', 59 | ) 60 | open(self.random_file_path, 'a').close() 61 | self.assertTrue(os.path.exists(self.random_file_path)) 62 | 63 | def tearDown(self): 64 | shutil.rmtree(self.test_dir_path) 65 | 66 | def test_inspect_input_path( 67 | self, 68 | quiet: bool = False, 69 | ) -> None: 70 | # Should find converted file under directory 71 | file_path, create_file = _inspect_input_path(self.with_converted_file_dir, False, quiet) 72 | self.assertEqual(self.converted_file_path, file_path) 73 | self.assertFalse(create_file) 74 | # Should find it but set it to be re-created 75 | file_path, create_file = _inspect_input_path(self.with_converted_file_dir, True, quiet) 76 | self.assertEqual(self.converted_file_path, file_path) 77 | self.assertTrue(create_file) 78 | 79 | # Should fail to find converted file under directory 80 | file_path, create_file = _inspect_input_path(self.without_converted_file_dir, False, quiet) 81 | self.assertIsNone(file_path) 82 | self.assertFalse(create_file) 83 | file_path, create_file = _inspect_input_path(self.without_converted_file_dir, True, quiet) 84 | self.assertIsNone(file_path) 85 | self.assertFalse(create_file) 86 | 87 | # Should accept any file path if it exists 88 | file_path, create_file = _inspect_input_path(self.random_file_path, False, quiet) 89 | self.assertEqual(self.random_file_path, file_path) 90 | self.assertFalse(create_file) 91 | # Should set it to be re-created 92 | file_path, create_file = _inspect_input_path(self.random_file_path, True, quiet) 93 | self.assertEqual(self.random_file_path, file_path) 94 | self.assertTrue(create_file) 95 | 96 | # TODO try with a trace directory 97 | 98 | def test_inspect_input_path_quiet(self) -> None: 99 | temp_stdout = StringIO() 100 | with contextlib.redirect_stdout(temp_stdout): 101 | # Just run the other test again 102 | self.test_inspect_input_path(quiet=True) 103 | # Shouldn't be any output 104 | output = temp_stdout.getvalue() 105 | self.assertEqual(0, len(output), f'was not quiet: "{output}"') 106 | -------------------------------------------------------------------------------- /tracetools_analysis/test/tracetools_analysis/test_processor.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import contextlib 16 | from io import StringIO 17 | from typing import Dict 18 | from typing import Set 19 | import unittest 20 | 21 | from tracetools_analysis.processor import EventHandler 22 | from tracetools_analysis.processor import EventMetadata 23 | from tracetools_analysis.processor import HandlerMap 24 | from tracetools_analysis.processor import Processor 25 | 26 | 27 | class StubHandler1(EventHandler): 28 | 29 | def __init__(self) -> None: 30 | handler_map: HandlerMap = { 31 | 'myeventname': self._handler_whatever, 32 | } 33 | super().__init__(handler_map=handler_map) 34 | self.handler_called = False 35 | 36 | def _handler_whatever( 37 | self, event: Dict, metadata: EventMetadata 38 | ) -> None: 39 | self.handler_called = True 40 | 41 | 42 | class StubHandler2(EventHandler): 43 | 44 | def __init__(self) -> None: 45 | handler_map: HandlerMap = { 46 | 'myeventname': self._handler_whatever, 47 | } 48 | super().__init__(handler_map=handler_map) 49 | self.handler_called = False 50 | 51 | def _handler_whatever( 52 | self, event: Dict, metadata: EventMetadata 53 | ) -> None: 54 | self.handler_called = True 55 | 56 | 57 | class WrongHandler(EventHandler): 58 | 59 | def __init__(self) -> None: 60 | handler_map: HandlerMap = { 61 | 'myeventname': self._handler_wrong, # type: ignore # intentionally wrong 62 | } 63 | super().__init__(handler_map=handler_map) 64 | 65 | def _handler_wrong( 66 | self, 67 | ) -> None: 68 | pass 69 | 70 | 71 | class MissingEventHandler(EventHandler): 72 | 73 | def __init__(self) -> None: 74 | handler_map: HandlerMap = { 75 | 'myeventname': self._handler_whatever, 76 | } 77 | super().__init__(handler_map=handler_map) 78 | 79 | @staticmethod 80 | def required_events() -> Set[str]: 81 | return { 82 | 'no_handler_for_this', 83 | 'myeventname', 84 | } 85 | 86 | def _handler_whatever( 87 | self, event: Dict, metadata: EventMetadata 88 | ) -> None: 89 | pass 90 | 91 | 92 | class EventHandlerWithRequiredEvent(EventHandler): 93 | 94 | def __init__(self) -> None: 95 | handler_map: HandlerMap = { 96 | 'myrequiredevent': self._handler_whatever, 97 | } 98 | super().__init__(handler_map=handler_map) 99 | 100 | @staticmethod 101 | def required_events() -> Set[str]: 102 | return { 103 | 'myrequiredevent', 104 | } 105 | 106 | def _handler_whatever( 107 | self, event: Dict, metadata: EventMetadata 108 | ) -> None: 109 | pass 110 | 111 | 112 | class TestProcessor(unittest.TestCase): 113 | 114 | def __init__(self, *args) -> None: 115 | super().__init__( 116 | *args, 117 | ) 118 | 119 | def test_event_handler_process(self) -> None: 120 | # Should not be called directly 121 | with self.assertRaises(AssertionError): 122 | EventHandler.process([]) 123 | 124 | def test_handler_wrong_signature(self) -> None: 125 | handler = WrongHandler() 126 | mock_event = { 127 | '_name': 'myeventname', 128 | '_timestamp': 0, 129 | 'cpu_id': 0, 130 | } 131 | processor = Processor(handler) 132 | with self.assertRaises(TypeError): 133 | processor.process([mock_event]) 134 | 135 | def test_handler_method_with_merge(self) -> None: 136 | handler1 = StubHandler1() 137 | handler2 = StubHandler2() 138 | mock_event = { 139 | '_name': 'myeventname', 140 | '_timestamp': 0, 141 | 'cpu_id': 0, 142 | } 143 | processor = Processor(handler1, handler2) 144 | processor.process([mock_event]) 145 | self.assertTrue(handler1.handler_called, 'event handler not called') 146 | self.assertTrue(handler2.handler_called, 'event handler not called') 147 | 148 | def test_assert_handler_functions_for_required_events(self) -> None: 149 | with self.assertRaises(AssertionError): 150 | MissingEventHandler() 151 | 152 | def test_check_required_events(self) -> None: 153 | mock_event = { 154 | '_name': 'myeventname', 155 | '_timestamp': 0, 156 | 'cpu_id': 0, 157 | } 158 | # Fails check 159 | with self.assertRaises(Processor.RequiredEventNotFoundError): 160 | Processor(EventHandlerWithRequiredEvent()).process([mock_event]) 161 | 162 | required_mock_event = { 163 | '_name': 'myrequiredevent', 164 | '_timestamp': 69, 165 | 'cpu_id': 0, 166 | } 167 | # Passes check 168 | Processor(EventHandlerWithRequiredEvent()).process([required_mock_event, mock_event]) 169 | 170 | def test_get_handler_by_type(self) -> None: 171 | handler1 = StubHandler1() 172 | handler2 = StubHandler2() 173 | processor = Processor(handler1, handler2) 174 | result = processor.get_handler_by_type(StubHandler1) 175 | self.assertTrue(result is handler1) 176 | 177 | def test_processor_quiet(self) -> None: 178 | handler1 = StubHandler1() 179 | mock_event = { 180 | '_name': 'myeventname', 181 | '_timestamp': 0, 182 | 'cpu_id': 0, 183 | } 184 | temp_stdout = StringIO() 185 | with contextlib.redirect_stdout(temp_stdout): 186 | processor = Processor(handler1, quiet=True) 187 | processor.process([mock_event]) 188 | # Shouldn't be any output 189 | output = temp_stdout.getvalue() 190 | self.assertEqual(0, len(output), f'Processor was not quiet: "{output}"') 191 | 192 | 193 | if __name__ == '__main__': 194 | unittest.main() 195 | -------------------------------------------------------------------------------- /tracetools_analysis/test/tracetools_analysis/test_profile_handler.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Any 16 | from typing import Dict 17 | from typing import List 18 | import unittest 19 | 20 | from pandas import DataFrame 21 | from pandas.testing import assert_frame_equal 22 | 23 | from tracetools_analysis.processor import Processor 24 | from tracetools_analysis.processor.profile import ProfileHandler 25 | from tracetools_read import DictEvent 26 | 27 | 28 | # TEST DATA 29 | # 30 | # + Threads: 31 | # 0: does whatever 32 | # 1: contains one instance of the functions of interest 33 | # 2: contains another instance of the functions of interest 34 | # 35 | # + Functions structure 36 | # function_a 37 | # function_aa 38 | # function_b 39 | # 40 | # + Timeline 41 | # tid 1 2 42 | # func a aa b a aa b 43 | # time 44 | # 0 : whatever 45 | # 3 : sched_switch from tid 0 to tid 1 46 | # 5 : tid 1, func_entry: function_a 47 | # 7 : sched_switch from tid 1 to tid 0 2 48 | # 10 : sched_switch from tid 0 to tid 2 49 | # 11 : tid 2, func_entry: function_a 50 | # 15 : sched_switch from tid 2 to tid 1 4 51 | # 16 : tid 1, func_entry: function_aa 1 52 | # 20 : sched_switch from tid 1 to tid 2 4 4 53 | # 27 : tid 2, func_entry: function_aa 7 54 | # 29 : sched_switch from tid 2 to tid 1 2 2 55 | # 30 : tid 1, func_exit: (function_aa) 1 1 56 | # 32 : sched_switch from tid 1 to tid 0 2 57 | # 34 : sched_switch from tid 0 to tid 2 58 | # 35 : tid 2, func_exit: (function_aa) 1 1 59 | # 37 : tid 2, func_exit: (function_a) 2 60 | # 39 : tid 2, func_entry: function_b 61 | # 40 : tid 2, func_exit: (function_b) 1 62 | # 41 : sched_switch from tid 2 to tid 1 63 | # 42 : tid 1, func_exit: (function_a) 1 64 | # 44 : tid 1, func_entry: function_b 65 | # 47 : sched_switch from tid 1 to tid 0 3 66 | # 49 : sched_switch from tid 0 to tid 1 67 | # 60 : tid 1, func_exit: (function_b) 11 68 | # 69 : sched_switch from tid 1 to tid 0 69 | # 70 | # total 11 5 14 16 3 1 71 | 72 | 73 | input_events = [ 74 | { 75 | '_name': 'sched_switch', 76 | '_timestamp': 3, 77 | 'prev_tid': 0, 78 | 'next_tid': 1, 79 | }, 80 | { 81 | '_name': 'lttng_ust_cyg_profile_fast:func_entry', 82 | '_timestamp': 5, 83 | 'vtid': 1, 84 | 'addr': '0xfA', 85 | }, 86 | { 87 | '_name': 'sched_switch', 88 | '_timestamp': 7, 89 | 'prev_tid': 1, 90 | 'next_tid': 0, 91 | }, 92 | { 93 | '_name': 'sched_switch', 94 | '_timestamp': 10, 95 | 'prev_tid': 0, 96 | 'next_tid': 2, 97 | }, 98 | { 99 | '_name': 'lttng_ust_cyg_profile_fast:func_entry', 100 | '_timestamp': 11, 101 | 'vtid': 2, 102 | 'addr': '0xfA', 103 | }, 104 | { 105 | '_name': 'sched_switch', 106 | '_timestamp': 15, 107 | 'prev_tid': 2, 108 | 'next_tid': 1, 109 | }, 110 | { 111 | '_name': 'lttng_ust_cyg_profile_fast:func_entry', 112 | '_timestamp': 16, 113 | 'vtid': 1, 114 | 'addr': '0xfAA', 115 | }, 116 | { 117 | '_name': 'sched_switch', 118 | '_timestamp': 20, 119 | 'prev_tid': 1, 120 | 'next_tid': 2, 121 | }, 122 | { 123 | '_name': 'lttng_ust_cyg_profile_fast:func_entry', 124 | '_timestamp': 27, 125 | 'vtid': 2, 126 | 'addr': '0xfAA', 127 | }, 128 | { 129 | '_name': 'sched_switch', 130 | '_timestamp': 29, 131 | 'prev_tid': 2, 132 | 'next_tid': 1, 133 | }, 134 | { 135 | '_name': 'lttng_ust_cyg_profile_fast:func_exit', 136 | '_timestamp': 30, 137 | 'vtid': 1, 138 | }, 139 | { 140 | '_name': 'sched_switch', 141 | '_timestamp': 32, 142 | 'prev_tid': 1, 143 | 'next_tid': 0, 144 | }, 145 | { 146 | '_name': 'sched_switch', 147 | '_timestamp': 34, 148 | 'prev_tid': 0, 149 | 'next_tid': 2, 150 | }, 151 | { 152 | '_name': 'lttng_ust_cyg_profile_fast:func_exit', 153 | '_timestamp': 35, 154 | 'vtid': 2, 155 | }, 156 | { 157 | '_name': 'lttng_ust_cyg_profile_fast:func_exit', 158 | '_timestamp': 37, 159 | 'vtid': 2, 160 | }, 161 | { 162 | '_name': 'lttng_ust_cyg_profile_fast:func_entry', 163 | '_timestamp': 39, 164 | 'vtid': 2, 165 | 'addr': '0xfB', 166 | }, 167 | { 168 | '_name': 'lttng_ust_cyg_profile_fast:func_exit', 169 | '_timestamp': 40, 170 | 'vtid': 2, 171 | }, 172 | { 173 | '_name': 'sched_switch', 174 | '_timestamp': 41, 175 | 'prev_tid': 2, 176 | 'next_tid': 1, 177 | }, 178 | { 179 | '_name': 'lttng_ust_cyg_profile_fast:func_exit', 180 | '_timestamp': 42, 181 | 'vtid': 1, 182 | }, 183 | { 184 | '_name': 'lttng_ust_cyg_profile_fast:func_entry', 185 | '_timestamp': 44, 186 | 'vtid': 1, 187 | 'addr': '0xfB', 188 | }, 189 | { 190 | '_name': 'sched_switch', 191 | '_timestamp': 47, 192 | 'prev_tid': 1, 193 | 'next_tid': 0, 194 | }, 195 | { 196 | '_name': 'sched_switch', 197 | '_timestamp': 49, 198 | 'prev_tid': 0, 199 | 'next_tid': 1, 200 | }, 201 | { 202 | '_name': 'lttng_ust_cyg_profile_fast:func_exit', 203 | '_timestamp': 60, 204 | 'vtid': 1, 205 | }, 206 | { 207 | '_name': 'sched_switch', 208 | '_timestamp': 69, 209 | 'prev_tid': 1, 210 | 'next_tid': 0, 211 | }, 212 | ] 213 | 214 | 215 | expected = [ 216 | { 217 | 'tid': 1, 218 | 'depth': 1, 219 | 'function_name': '0xfAA', 220 | 'parent_name': '0xfA', 221 | 'start_timestamp': 16, 222 | 'duration': 14, 223 | 'actual_duration': 5, 224 | }, 225 | { 226 | 'tid': 2, 227 | 'depth': 1, 228 | 'function_name': '0xfAA', 229 | 'parent_name': '0xfA', 230 | 'start_timestamp': 27, 231 | 'duration': 8, 232 | 'actual_duration': 3, 233 | }, 234 | { 235 | 'tid': 2, 236 | 'depth': 0, 237 | 'function_name': '0xfA', 238 | 'parent_name': None, 239 | 'start_timestamp': 11, 240 | 'duration': 26, 241 | 'actual_duration': 16, 242 | }, 243 | { 244 | 'tid': 2, 245 | 'depth': 0, 246 | 'function_name': '0xfB', 247 | 'parent_name': None, 248 | 'start_timestamp': 39, 249 | 'duration': 1, 250 | 'actual_duration': 1, 251 | }, 252 | { 253 | 'tid': 1, 254 | 'depth': 0, 255 | 'function_name': '0xfA', 256 | 'parent_name': None, 257 | 'start_timestamp': 5, 258 | 'duration': 37, 259 | 'actual_duration': 11, 260 | }, 261 | { 262 | 'tid': 1, 263 | 'depth': 0, 264 | 'function_name': '0xfB', 265 | 'parent_name': None, 266 | 'start_timestamp': 44, 267 | 'duration': 16, 268 | 'actual_duration': 14, 269 | }, 270 | ] 271 | 272 | 273 | address_to_func = { 274 | '0xfA': '0xfA', 275 | '0xfAA': '0xfAA', 276 | '0xfB': '0xfB', 277 | } 278 | 279 | 280 | class TestProfileHandler(unittest.TestCase): 281 | 282 | def __init__(self, *args) -> None: 283 | super().__init__( 284 | *args, 285 | ) 286 | 287 | @staticmethod 288 | def build_expected_df(expected_data: List[Dict[str, Any]]) -> DataFrame: 289 | # Columns should be in the same order 290 | return DataFrame.from_dict(expected_data) 291 | 292 | @staticmethod 293 | def transform_fake_fields(events: List[DictEvent]) -> None: 294 | for event in events: 295 | # Actual value does not matter here; it just needs to be there 296 | event['cpu_id'] = 69 297 | if event['_name'] == 'lttng_ust_cyg_profile_fast:func_entry': 298 | # The 'addr' field is supposed to be an int 299 | event['addr'] = ProfileHandler.addr_to_int(event['addr']) 300 | 301 | @classmethod 302 | def setUpClass(cls): 303 | cls.transform_fake_fields(input_events) 304 | cls.expected = cls.build_expected_df(expected) 305 | cls.handler = ProfileHandler(address_to_func=address_to_func) 306 | cls.processor = Processor(cls.handler) 307 | cls.processor.process(input_events) 308 | 309 | def test_profiling(self) -> None: 310 | handler = self.__class__.handler # type: ignore 311 | expected_df = self.__class__.expected # type: ignore 312 | result_df = handler.data.times 313 | assert_frame_equal(result_df, expected_df) 314 | 315 | 316 | if __name__ == '__main__': 317 | unittest.main() 318 | -------------------------------------------------------------------------------- /tracetools_analysis/test/tracetools_analysis/test_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright 2019 Apex.AI, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import unittest 17 | 18 | from tracetools_analysis import time_diff_to_str 19 | from tracetools_analysis.data_model.ros2 import Ros2DataModel 20 | from tracetools_analysis.utils.ros2 import Ros2DataModelUtil 21 | 22 | 23 | class TestUtils(unittest.TestCase): 24 | 25 | def __init__(self, *args) -> None: 26 | super().__init__( 27 | *args, 28 | ) 29 | 30 | def test_time_diff_to_str(self) -> None: 31 | self.assertEqual('11 ms', time_diff_to_str(0.0106)) 32 | self.assertEqual('6.9 s', time_diff_to_str(6.9069)) 33 | self.assertEqual('1 m 10 s', time_diff_to_str(69.6969)) 34 | self.assertEqual('6 m 10 s', time_diff_to_str(369.6969)) 35 | self.assertEqual('2 m 0 s', time_diff_to_str(120.499999999)) 36 | 37 | def test_ros2_no_callbacks(self) -> None: 38 | data_model = Ros2DataModel() 39 | data_model.finalize() 40 | util = Ros2DataModelUtil(data_model) 41 | self.assertEqual({}, util.get_callback_symbols()) 42 | 43 | def test_ros2_no_lifecycle_transitions(self) -> None: 44 | data_model = Ros2DataModel() 45 | data_model.finalize() 46 | util = Ros2DataModelUtil(data_model) 47 | self.assertEqual({}, util.get_lifecycle_node_state_intervals()) 48 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Tools for analysing trace data.""" 16 | 17 | 18 | def time_diff_to_str( 19 | time_diff: float, 20 | ) -> str: 21 | """ 22 | Format time difference as a string. 23 | 24 | :param time_diff: the difference between two timepoints (e.g. `time.time()`) 25 | """ 26 | if time_diff < 1.0: 27 | # ms 28 | return f'{time_diff * 1000:.0f} ms' 29 | elif time_diff < 60.0: 30 | # s 31 | return f'{time_diff:.1f} s' 32 | else: 33 | # m s 34 | return f'{time_diff // 60.0:.0f} m {time_diff % 60.0:.0f} s' 35 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/conversion/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/conversion/ctf.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Module with CTF to pickle conversion functions.""" 16 | 17 | from pickle import Pickler 18 | 19 | from tracetools_read.trace import event_to_dict 20 | from tracetools_read.trace import get_trace_ctf_events 21 | 22 | 23 | def ctf_to_pickle(trace_directory: str, target: Pickler) -> int: 24 | """ 25 | Load CTF trace, convert events, and dump to a pickle file. 26 | 27 | :param trace_directory: the trace directory 28 | :param target: the target file to write to 29 | :return: the number of events written 30 | """ 31 | ctf_events = get_trace_ctf_events(trace_directory) 32 | 33 | count = 0 34 | count_written = 0 35 | 36 | for event in ctf_events: 37 | count += 1 38 | 39 | pod = event_to_dict(event) 40 | target.dump(pod) 41 | count_written += 1 42 | 43 | return count_written 44 | 45 | 46 | def convert(trace_directory: str, output_file_path: str) -> int: 47 | """ 48 | Convert CTF trace to pickle file. 49 | 50 | :param trace_directory: the trace directory 51 | :param output_file_path: the path to the output file that will be created 52 | :return: the number of events written to the output file 53 | """ 54 | with open(output_file_path, 'wb') as f: 55 | p = Pickler(f, protocol=4) 56 | count = ctf_to_pickle(trace_directory, p) 57 | 58 | return count 59 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/convert.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright 2019 Robert Bosch GmbH 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """Entrypoint/script to convert CTF trace data to a file.""" 17 | 18 | import argparse 19 | import os 20 | import sys 21 | import time 22 | 23 | from tracetools_analysis.conversion import ctf 24 | 25 | from . import time_diff_to_str 26 | 27 | 28 | DEFAULT_CONVERT_FILE_NAME = 'converted' 29 | 30 | 31 | def add_args(parser: argparse.ArgumentParser) -> None: 32 | parser.add_argument( 33 | 'trace_directory', 34 | help='the path to the main trace directory') 35 | parser.add_argument( 36 | '-o', '--output-file', dest='output_file_name', metavar='OUTPUT', 37 | default=DEFAULT_CONVERT_FILE_NAME, 38 | help='the name of the output file to generate, ' 39 | 'under $trace_directory (default: %(default)s)') 40 | 41 | 42 | def parse_args() -> argparse.Namespace: 43 | parser = argparse.ArgumentParser( 44 | description=( 45 | 'Convert trace data to a file. ' 46 | "DEPRECATED: use the 'process' verb directly." 47 | ), 48 | ) 49 | add_args(parser) 50 | return parser.parse_args() 51 | 52 | 53 | def convert( 54 | trace_directory: str, 55 | output_file_name: str = DEFAULT_CONVERT_FILE_NAME, 56 | ) -> int: 57 | """ 58 | Convert trace directory to a file. 59 | 60 | The output file will be placed under the trace directory. 61 | 62 | :param trace_directory: the path to the trace directory to import 63 | :param outout_file_name: the name of the output file 64 | """ 65 | trace_directory = os.path.expanduser(trace_directory) 66 | if not os.path.isdir(trace_directory): 67 | print(f'trace directory does not exist: {trace_directory}', file=sys.stderr) 68 | return 1 69 | 70 | print(f'converting trace directory: {trace_directory}') 71 | output_file_path = os.path.join(trace_directory, output_file_name) 72 | start_time = time.time() 73 | count = ctf.convert(trace_directory, output_file_path) 74 | time_diff = time.time() - start_time 75 | print(f'converted {count} events in {time_diff_to_str(time_diff)}') 76 | print(f'output written to: {output_file_path}') 77 | return 0 78 | 79 | 80 | def main(): 81 | args = parse_args() 82 | 83 | trace_directory = args.trace_directory 84 | output_file_name = args.output_file_name 85 | 86 | import warnings 87 | warnings.warn("'convert' is deprecated, use 'process' directly instead", stacklevel=2) 88 | convert(trace_directory, output_file_name) 89 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/data_model/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # Copyright 2021 Christophe Bedard 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """Base data model module.""" 17 | 18 | from typing import Any 19 | from typing import Dict 20 | from typing import List 21 | 22 | 23 | DataModelIntermediateStorage = List[Dict[str, Any]] 24 | 25 | 26 | class DataModel(): 27 | """ 28 | Container with pre-processed data for an analysis to use. 29 | 30 | Contains data for an analysis to use. This is a middleground between trace events data and the 31 | output data of an analysis. 32 | It uses native/simple Python data structures (e.g. lists of dicts) during processing, but 33 | converts them to pandas `DataFrame` at the end. 34 | """ 35 | 36 | def __init__(self) -> None: 37 | self._finalized = False 38 | 39 | def finalize(self) -> None: 40 | """ 41 | Finalize the data model. 42 | 43 | Call this once data is done being generated or added to the model. 44 | Finalization tasks are up to the inheriting/concrete class. 45 | """ 46 | # Avoid calling it twice for data models which might be shared 47 | if not self._finalized: 48 | self._finalized = True 49 | self._finalize() 50 | 51 | def _finalize(self) -> None: 52 | """ 53 | Do the finalization. 54 | 55 | Only called once. 56 | """ 57 | raise NotImplementedError 58 | 59 | def print_data(self) -> None: 60 | """Print the data model.""" 61 | raise NotImplementedError 62 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/data_model/cpu_time.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # Copyright 2021 Christophe Bedard 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """Module for CPU time data model.""" 17 | 18 | import pandas as pd 19 | 20 | from . import DataModel 21 | from . import DataModelIntermediateStorage 22 | 23 | 24 | class CpuTimeDataModel(DataModel): 25 | """ 26 | Container to model pre-processed CPU time data for analysis. 27 | 28 | Contains every duration instance. 29 | """ 30 | 31 | def __init__(self) -> None: 32 | """Create a CpuTimeDataModel.""" 33 | super().__init__() 34 | self._times: DataModelIntermediateStorage = [] 35 | 36 | def add_duration( 37 | self, 38 | tid: int, 39 | start_timestamp: int, 40 | duration: int, 41 | cpu_id: int, 42 | ) -> None: 43 | self._times.append({ 44 | 'tid': tid, 45 | 'start_timestamp': start_timestamp, 46 | 'duration': duration, 47 | 'cpu_id': cpu_id, 48 | }) 49 | 50 | def _finalize(self) -> None: 51 | self.times = pd.DataFrame.from_dict(self._times) 52 | 53 | def print_data(self) -> None: 54 | print('====================CPU TIME DATA MODEL====================') 55 | tail = 20 56 | print(f'Times (tail={tail}):') 57 | print(self.times.tail(tail).to_string()) 58 | print('===========================================================') 59 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/data_model/memory_usage.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Apex.AI, Inc. 2 | # Copyright 2021 Christophe Bedard 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """Module for memory usage data model.""" 17 | 18 | import pandas as pd 19 | 20 | from . import DataModel 21 | from . import DataModelIntermediateStorage 22 | 23 | 24 | class MemoryUsageDataModel(DataModel): 25 | """ 26 | Container to model pre-processed memory usage data for analysis. 27 | 28 | Contains changes in memory allocation (e.g. + for malloc, - for free) with the corresponding 29 | timestamp. 30 | """ 31 | 32 | def __init__(self) -> None: 33 | """Create a MemoryUsageDataModel.""" 34 | super().__init__() 35 | self._memory_diff: DataModelIntermediateStorage = [] 36 | 37 | def add_memory_difference( 38 | self, 39 | timestamp: int, 40 | tid: int, 41 | memory_diff: int, 42 | ) -> None: 43 | self._memory_diff.append({ 44 | 'timestamp': timestamp, 45 | 'tid': tid, 46 | 'memory_diff': memory_diff, 47 | }) 48 | 49 | def _finalize(self) -> None: 50 | self.memory_diff = pd.DataFrame.from_dict(self._memory_diff) 51 | 52 | def print_data(self) -> None: 53 | print('==================MEMORY USAGE DATA MODEL==================') 54 | tail = 20 55 | print(f'Memory difference (tail={tail}):\n{self.memory_diff.tail(tail).to_string()}') 56 | print('===========================================================') 57 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/data_model/profile.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # Copyright 2021 Christophe Bedard 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """Module for profile data model.""" 17 | 18 | from typing import Optional 19 | 20 | import pandas as pd 21 | 22 | from . import DataModel 23 | from . import DataModelIntermediateStorage 24 | 25 | 26 | class ProfileDataModel(DataModel): 27 | """ 28 | Container to model pre-processed profiling data for analysis. 29 | 30 | Duration is the time difference between the function entry and the function exit. 31 | Actual duration is the actual time spent executing the function (or a child function). 32 | """ 33 | 34 | def __init__(self) -> None: 35 | """Create a ProfileDataModel.""" 36 | super().__init__() 37 | self._times: DataModelIntermediateStorage = [] 38 | 39 | def add_duration( 40 | self, 41 | tid: int, 42 | depth: int, 43 | function_name: str, 44 | parent_name: Optional[str], 45 | start_timestamp: int, 46 | duration: int, 47 | actual_duration: int, 48 | ) -> None: 49 | self._times.append({ 50 | 'tid': tid, 51 | 'depth': depth, 52 | 'function_name': function_name, 53 | 'parent_name': parent_name, 54 | 'start_timestamp': start_timestamp, 55 | 'duration': duration, 56 | 'actual_duration': actual_duration, 57 | }) 58 | 59 | def _finalize(self) -> None: 60 | self.times = pd.DataFrame.from_dict(self._times) 61 | 62 | def print_data(self) -> None: 63 | print('====================PROFILE DATA MODEL====================') 64 | tail = 20 65 | print(f'Times (tail={tail}):') 66 | print(self.times.tail(tail).to_string()) 67 | print('==========================================================') 68 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/loading/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Module for loading traces.""" 16 | 17 | import os 18 | import pickle 19 | import sys 20 | from typing import Dict 21 | from typing import List 22 | from typing import Optional 23 | from typing import Tuple 24 | 25 | from tracetools_read.trace import is_trace_directory 26 | 27 | from ..convert import convert 28 | from ..convert import DEFAULT_CONVERT_FILE_NAME 29 | 30 | 31 | def _inspect_input_path( 32 | input_path: str, 33 | force_conversion: bool = False, 34 | quiet: bool = False, 35 | ) -> Tuple[Optional[str], bool]: 36 | """ 37 | Check input path for a converted file or a trace directory. 38 | 39 | If the input path is a file, it uses it as a converted file. 40 | If the input path is a directory, it checks if there is a "converted" file directly inside it, 41 | otherwise it tries to import the path as a trace directory. 42 | If `force_conversion` is set to `True`, even if a converted file is found, it will ask to 43 | re-create it. 44 | 45 | :param input_path: the path to a converted file or trace directory 46 | :param force_conversion: whether to re-create converted file even if it is found 47 | :param quiet: whether to not print any normal output 48 | :return: 49 | the path to a converted file (or `None` if could not find), 50 | `True` if the given converted file should be (re-)created, `False` otherwise 51 | """ 52 | input_path = os.path.expanduser(input_path) 53 | converted_file_path = None 54 | # Check if not a file 55 | if not os.path.isfile(input_path): 56 | input_directory = input_path 57 | # Might be a (trace) directory 58 | # Check if there is a converted file under the given directory 59 | prospective_converted_file = os.path.join(input_directory, DEFAULT_CONVERT_FILE_NAME) 60 | if os.path.isfile(prospective_converted_file): 61 | # Use that as the converted input file 62 | converted_file_path = prospective_converted_file 63 | if force_conversion: 64 | if not quiet: 65 | print( 66 | f'found converted file but will re-create it: {prospective_converted_file}' 67 | ) 68 | return prospective_converted_file, True 69 | else: 70 | if not quiet: 71 | print(f'found converted file: {prospective_converted_file}') 72 | return prospective_converted_file, False 73 | else: 74 | # Check if it is a trace directory 75 | # Result could be unexpected because it will look for trace directories recursively 76 | # (e.g. '/' is a valid trace directory if there is at least one trace anywhere) 77 | if is_trace_directory(input_directory): 78 | # Convert trace directory first to create converted file 79 | return prospective_converted_file, True 80 | else: 81 | # We cannot do anything 82 | print( 83 | f'cannot find either a trace directory or a converted file: {input_directory}', 84 | file=sys.stderr) 85 | return None, False 86 | else: 87 | converted_file_path = input_path 88 | if force_conversion: 89 | # It's a file, but re-create it anyway 90 | if not quiet: 91 | print(f'found converted file but will re-create it: {converted_file_path}') 92 | return converted_file_path, True 93 | else: 94 | # Simplest use-case: given path is an existing converted file 95 | # No need to print anything 96 | return converted_file_path, False 97 | 98 | 99 | def _convert_if_needed( 100 | input_path: str, 101 | force_conversion: bool = False, 102 | quiet: bool = False, 103 | ) -> Optional[str]: 104 | """ 105 | Inspect input path and convert trace directory to file if necessary. 106 | 107 | :param input_path: the path to a converted file or trace directory 108 | :param force_conversion: whether to re-create converted file even if it is found 109 | :param quiet: whether to not print any output 110 | :return: the path to the converted file, or `None` if it failed 111 | """ 112 | converted_file_path, create_converted_file = _inspect_input_path( 113 | input_path, 114 | force_conversion, 115 | quiet, 116 | ) 117 | 118 | if converted_file_path is None: 119 | return None 120 | 121 | # Convert trace directory to file if necessary 122 | if create_converted_file: 123 | input_directory = os.path.dirname(converted_file_path) 124 | input_file_name = os.path.basename(converted_file_path) 125 | convert(input_directory, input_file_name) 126 | 127 | return converted_file_path 128 | 129 | 130 | def load_file( 131 | input_path: str, 132 | do_convert_if_needed: bool = True, 133 | force_conversion: bool = False, 134 | quiet: bool = False, 135 | ) -> List[Dict]: 136 | """ 137 | Load file containing converted trace events. 138 | 139 | :param input_path: the path to a converted file or trace directory 140 | :param do_convert_if_needed: whether to create the converted file if needed (else, let it fail) 141 | :param force_conversion: whether to re-create converted file even if it is found 142 | :param quiet: whether to not print any output 143 | :return: the list of events read from the file 144 | """ 145 | if do_convert_if_needed or force_conversion: 146 | file_path = _convert_if_needed(input_path, force_conversion, quiet) 147 | else: 148 | file_path = input_path 149 | 150 | if file_path is None: 151 | raise RuntimeError(f'could not use input path: {input_path}') 152 | 153 | events = [] 154 | with open(os.path.expanduser(file_path), 'rb') as f: 155 | p = pickle.Unpickler(f) 156 | while True: 157 | try: 158 | events.append(p.load()) 159 | except EOFError: 160 | break # we're done 161 | 162 | return events 163 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/process.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright 2019 Robert Bosch GmbH 3 | # Copyright 2021 Christophe Bedard 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Entrypoint/script to process events from a converted file to build a ROS model.""" 18 | 19 | import argparse 20 | import os 21 | import sys 22 | import time 23 | 24 | from tracetools_analysis.loading import load_file 25 | from tracetools_analysis.processor import Processor 26 | from tracetools_analysis.processor.ros2 import Ros2Handler 27 | 28 | from . import time_diff_to_str 29 | 30 | 31 | def add_args(parser: argparse.ArgumentParser) -> None: 32 | parser.add_argument( 33 | 'input_path', 34 | help='the path to a converted file to import and process, ' 35 | 'or the path to a trace directory to convert and process') 36 | parser.add_argument( 37 | '-f', '--force-conversion', dest='force_conversion', 38 | action='store_true', default=False, 39 | help='re-convert trace directory even if converted file is found') 40 | command_group = parser.add_mutually_exclusive_group() 41 | command_group.add_argument( 42 | '-s', '--hide-results', dest='hide_results', 43 | action='store_true', default=False, 44 | help='hide/suppress results from being printed') 45 | command_group.add_argument( 46 | '-c', '--convert-only', dest='convert_only', 47 | action='store_true', default=False, 48 | help=( 49 | 'only do the first step of converting the file, without processing it ' 50 | '(this should not be necessary, since conversion is done automatically and is mostly ' 51 | 'just an implementation detail)' 52 | )) 53 | 54 | 55 | def parse_args() -> argparse.Namespace: 56 | parser = argparse.ArgumentParser( 57 | description='Process ROS 2 trace data and output model data.') 58 | add_args(parser) 59 | return parser.parse_args() 60 | 61 | 62 | def process( 63 | input_path: str, 64 | force_conversion: bool = False, 65 | hide_results: bool = False, 66 | convert_only: bool = False, 67 | ) -> int: 68 | """ 69 | Process ROS 2 trace data and output model data. 70 | 71 | The trace data will be automatically converted into 72 | an internal intermediate representation if needed. 73 | 74 | :param input_path: the path to a converted file or trace directory 75 | :param force_conversion: whether to re-creating converted file even if it is found 76 | :param hide_results: whether to hide results and not print them 77 | :param convert_only: whether to only convert the file into our internal intermediate 78 | representation, without processing it. This should usually not be necessary since 79 | conversion is done automatically only when needed or when explicitly requested with 80 | force_conversion; conversion is mostly just an implementation detail 81 | """ 82 | input_path = os.path.expanduser(input_path) 83 | if not os.path.exists(input_path): 84 | print(f'input path does not exist: {input_path}', file=sys.stderr) 85 | return 1 86 | 87 | start_time = time.time() 88 | 89 | events = load_file(input_path, do_convert_if_needed=True, force_conversion=force_conversion) 90 | 91 | # Return now if we only need to convert the file 92 | if convert_only: 93 | return 0 94 | 95 | processor = Processor(Ros2Handler()) 96 | processor.process(events) 97 | 98 | time_diff = time.time() - start_time 99 | if not hide_results: 100 | processor.print_data() 101 | print(f'processed {len(events)} events in {time_diff_to_str(time_diff)}') 102 | return 0 103 | 104 | 105 | def main(): 106 | args = parse_args() 107 | 108 | process( 109 | args.input_path, 110 | args.force_conversion, 111 | args.hide_results, 112 | args.convert_only, 113 | ) 114 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/processor/cpu_time.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Module for CPU time events processing.""" 16 | 17 | from typing import Dict 18 | from typing import Set 19 | 20 | from tracetools_read import get_field 21 | 22 | from . import EventHandler 23 | from . import EventMetadata 24 | from . import HandlerMap 25 | from ..data_model.cpu_time import CpuTimeDataModel 26 | 27 | 28 | class CpuTimeHandler(EventHandler): 29 | """ 30 | Handler that extracts data for CPU time. 31 | 32 | It extracts timestamps from sched_switch events to later compute CPU time per thread. 33 | """ 34 | 35 | def __init__( 36 | self, 37 | **kwargs, 38 | ) -> None: 39 | """Create a CpuTimeHandler.""" 40 | # Link event to handling method 41 | handler_map: HandlerMap = { 42 | 'sched_switch': 43 | self._handle_sched_switch, 44 | } 45 | super().__init__( 46 | handler_map=handler_map, 47 | data_model=CpuTimeDataModel(), 48 | **kwargs, 49 | ) 50 | 51 | # Temporary buffers 52 | # cpu_id -> start timestamp of the running thread 53 | self._cpu_start: Dict[int, int] = {} 54 | 55 | @staticmethod 56 | def required_events() -> Set[str]: 57 | return { 58 | 'sched_switch', 59 | } 60 | 61 | @property 62 | def data(self) -> CpuTimeDataModel: 63 | return super().data # type: ignore 64 | 65 | def _handle_sched_switch( 66 | self, event: Dict, metadata: EventMetadata 67 | ) -> None: 68 | timestamp = metadata.timestamp 69 | cpu_id = metadata.cpu_id 70 | # Process if there is a previous thread timestamp 71 | # TODO instead of discarding it, use first ever timestamp 72 | # of the trace (with TraceCollection.timestamp_begin) 73 | prev_timestamp = self._cpu_start.get(cpu_id, None) 74 | if prev_timestamp is not None: 75 | prev_tid = get_field(event, 'prev_tid') 76 | duration = timestamp - prev_timestamp 77 | self.data.add_duration(prev_tid, prev_timestamp, duration, cpu_id) 78 | # Set start timestamp of next thread 79 | self._cpu_start[cpu_id] = timestamp 80 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/processor/memory_usage.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Apex.AI, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Module for memory usage events processing.""" 16 | 17 | from typing import Dict 18 | from typing import Set 19 | 20 | from tracetools_read import get_field 21 | 22 | from . import EventHandler 23 | from . import EventMetadata 24 | from . import HandlerMap 25 | from ..data_model.memory_usage import MemoryUsageDataModel 26 | 27 | 28 | class MemoryUsageHandler(EventHandler): 29 | """Generic handler for memory usage.""" 30 | 31 | def __init__( 32 | self, 33 | **kwargs, 34 | ) -> None: 35 | if type(self) is MemoryUsageHandler: 36 | raise RuntimeError('Do not instantiate directly!') 37 | super().__init__( 38 | data_model=MemoryUsageDataModel(), 39 | **kwargs, 40 | ) 41 | 42 | @property 43 | def data(self) -> MemoryUsageDataModel: 44 | return super().data # type: ignore 45 | 46 | def _update( 47 | self, 48 | timestamp: int, 49 | tid: int, 50 | memory_difference: int, 51 | ) -> None: 52 | # Add to data model 53 | self.data.add_memory_difference(timestamp, tid, memory_difference) 54 | 55 | 56 | class UserspaceMemoryUsageHandler(MemoryUsageHandler): 57 | """ 58 | Handler that extracts userspace memory usage data. 59 | 60 | It uses the following events: 61 | * lttng_ust_libc:malloc 62 | * lttng_ust_libc:calloc 63 | * lttng_ust_libc:realloc 64 | * lttng_ust_libc:free 65 | * lttng_ust_libc:memalign 66 | * lttng_ust_libc:posix_memalign 67 | 68 | The above events are generated when LD_PRELOAD-ing liblttng-ust-libc-wrapper.so, see: 69 | https://lttng.org/docs/v2.10/#doc-liblttng-ust-libc-pthread-wrapper 70 | 71 | Implementation inspired by Trace Compass' implementation: 72 | https://git.eclipse.org/c/tracecompass/org.eclipse.tracecompass.git/tree/lttng/org.eclipse.tracecompass.lttng2.ust.core/src/org/eclipse/tracecompass/internal/lttng2/ust/core/analysis/memory/UstMemoryStateProvider.java#n161 73 | """ 74 | 75 | def __init__( 76 | self, 77 | **kwargs, 78 | ) -> None: 79 | # Link event to handling method 80 | handler_map: HandlerMap = { 81 | 'lttng_ust_libc:malloc': 82 | self._handle_malloc, 83 | 'lttng_ust_libc:calloc': 84 | self._handle_calloc, 85 | 'lttng_ust_libc:realloc': 86 | self._handle_realloc, 87 | 'lttng_ust_libc:free': 88 | self._handle_free, 89 | 'lttng_ust_libc:memalign': 90 | self._handle_memalign, 91 | 'lttng_ust_libc:posix_memalign': 92 | self._handle_posix_memalign, 93 | } 94 | super().__init__( 95 | handler_map=handler_map, 96 | **kwargs, 97 | ) 98 | 99 | # Temporary buffers 100 | # pointer -> current memory size 101 | # (used to know keep track of the memory size allocated at a given pointer) 102 | self._memory: Dict[int, int] = {} 103 | 104 | @staticmethod 105 | def required_events() -> Set[str]: 106 | return { 107 | 'lttng_ust_libc:malloc', 108 | 'lttng_ust_libc:free', 109 | } 110 | 111 | def _handle_malloc( 112 | self, event: Dict, metadata: EventMetadata 113 | ) -> None: 114 | ptr = get_field(event, 'ptr') 115 | if ptr != 0: 116 | size = get_field(event, 'size') 117 | self._handle(event, metadata, ptr, size) 118 | 119 | def _handle_calloc( 120 | self, event: Dict, metadata: EventMetadata 121 | ) -> None: 122 | ptr = get_field(event, 'ptr') 123 | if ptr != 0: 124 | nmemb = get_field(event, 'nmemb') 125 | size = get_field(event, 'size') 126 | self._handle(event, metadata, ptr, size * nmemb) 127 | 128 | def _handle_realloc( 129 | self, event: Dict, metadata: EventMetadata 130 | ) -> None: 131 | ptr = get_field(event, 'ptr') 132 | if ptr != 0: 133 | new_ptr = get_field(event, 'in_ptr') 134 | size = get_field(event, 'size') 135 | self._handle(event, metadata, ptr, 0) 136 | self._handle(event, metadata, new_ptr, size) 137 | 138 | def _handle_free( 139 | self, event: Dict, metadata: EventMetadata 140 | ) -> None: 141 | ptr = get_field(event, 'ptr') 142 | if ptr != 0: 143 | self._handle(event, metadata, ptr, 0) 144 | 145 | def _handle_memalign( 146 | self, event: Dict, metadata: EventMetadata 147 | ) -> None: 148 | ptr = get_field(event, 'ptr') 149 | if ptr != 0: 150 | size = get_field(event, 'size') 151 | self._handle(event, metadata, ptr, size) 152 | 153 | def _handle_posix_memalign( 154 | self, event: Dict, metadata: EventMetadata 155 | ) -> None: 156 | ptr = get_field(event, 'out_ptr') 157 | if ptr != 0: 158 | size = get_field(event, 'size') 159 | self._handle(event, metadata, ptr, size) 160 | 161 | def _handle( 162 | self, 163 | event: Dict, 164 | metadata: EventMetadata, 165 | ptr: int, 166 | size: int, 167 | ) -> None: 168 | timestamp = metadata.timestamp 169 | tid = metadata.tid 170 | 171 | memory_difference = size 172 | # Store the size allocated for the given pointer 173 | if memory_difference != 0: 174 | self._memory[ptr] = memory_difference 175 | else: 176 | # Othersize, if size is 0, it means it was deleted 177 | # Try to fetch the size stored previously 178 | allocated_memory = self._memory.get(ptr, None) 179 | if allocated_memory is not None: 180 | memory_difference = -allocated_memory 181 | 182 | self._update(timestamp, tid, memory_difference) 183 | 184 | 185 | class KernelMemoryUsageHandler(MemoryUsageHandler): 186 | """ 187 | Handler that extracts userspace memory usage data. 188 | 189 | It uses the following events: 190 | * kmem_mm_page_alloc 191 | * kmem_mm_page_free 192 | 193 | Implementation inspired by Trace Compass' implementation: 194 | https://git.eclipse.org/c/tracecompass/org.eclipse.tracecompass.git/tree/analysis/org.eclipse.tracecompass.analysis.os.linux.core/src/org/eclipse/tracecompass/analysis/os/linux/core/kernelmemoryusage/KernelMemoryStateProvider.java#n84 195 | """ 196 | 197 | PAGE_SIZE = 4096 198 | 199 | def __init__( 200 | self, 201 | **kwargs, 202 | ) -> None: 203 | # Link event to handling method 204 | handler_map: HandlerMap = { 205 | 'kmem_mm_page_alloc': 206 | self._handle_malloc, 207 | 'kmem_mm_page_free': 208 | self._handle_free, 209 | } 210 | super().__init__( 211 | handler_map=handler_map, 212 | **kwargs, 213 | ) 214 | 215 | @staticmethod 216 | def required_events() -> Set[str]: 217 | return { 218 | 'kmem_mm_page_alloc', 219 | 'kmem_mm_page_free', 220 | } 221 | 222 | def _handle_malloc( 223 | self, event: Dict, metadata: EventMetadata 224 | ) -> None: 225 | self._handle(event, metadata, self.PAGE_SIZE) 226 | 227 | def _handle_free( 228 | self, event: Dict, metadata: EventMetadata 229 | ) -> None: 230 | self._handle(event, metadata, -self.PAGE_SIZE) 231 | 232 | def _handle( 233 | self, 234 | event: Dict, 235 | metadata: EventMetadata, 236 | inc: int, 237 | ) -> None: 238 | order = get_field(event, 'order') 239 | inc <<= order 240 | 241 | timestamp = metadata.timestamp 242 | tid = metadata.tid 243 | 244 | self._update(timestamp, tid, inc) 245 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/processor/profile.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Module for profile events processing.""" 16 | 17 | from collections import defaultdict 18 | from typing import Dict 19 | from typing import List 20 | from typing import Set 21 | from typing import Union 22 | 23 | from tracetools_read import get_field 24 | 25 | from . import EventHandler 26 | from . import EventMetadata 27 | from . import HandlerMap 28 | from ..data_model.profile import ProfileDataModel 29 | 30 | 31 | class ProfileHandler(EventHandler): 32 | """ 33 | Handler that extracts profiling information. 34 | 35 | It uses the following events: 36 | * lttng_ust_cyg_profile_fast:func_entry 37 | * lttng_ust_cyg_profile_fast:func_exit 38 | * sched_switch 39 | 40 | The above events are generated when using -finstrument-functions with gcc and LD_PRELOAD-ing 41 | liblttng-ust-cyg-profile-fast.so, see: 42 | https://lttng.org/docs/v2.10/#doc-liblttng-ust-cyg-profile 43 | 44 | TODO get debug_info from babeltrace for 45 | lttng_ust_cyg_profile_fast:func_entry events 46 | (or resolve { address -> function } name another way) 47 | """ 48 | 49 | def __init__( 50 | self, 51 | address_to_func: Dict[Union[int, str], str] = {}, 52 | **kwargs, 53 | ) -> None: 54 | """ 55 | Create a ProfileHandler. 56 | 57 | :param address_to_func: the mapping from function address (`int` or hex `str`) to name 58 | """ 59 | handler_map: HandlerMap = { 60 | 'lttng_ust_cyg_profile_fast:func_entry': 61 | self._handle_function_entry, 62 | 'lttng_ust_cyg_profile_fast:func_exit': 63 | self._handle_function_exit, 64 | 'sched_switch': 65 | self._handle_sched_switch, 66 | } 67 | super().__init__( 68 | handler_map=handler_map, 69 | data_model=ProfileDataModel(), 70 | **kwargs, 71 | ) 72 | 73 | self._address_to_func = { 74 | self.addr_to_int(addr): name for addr, name in address_to_func.items() 75 | } 76 | 77 | # Temporary buffers 78 | # tid -> 79 | # list:[ 80 | # functions currently executing (ordered by relative depth), with info: 81 | # [ 82 | # function name, 83 | # start timestamp, 84 | # last execution start timestamp of the function, 85 | # total duration, 86 | # ] 87 | # ] 88 | self._current_funcs: Dict[int, List[List[Union[str, int]]]] = defaultdict(list) 89 | 90 | @staticmethod 91 | def required_events() -> Set[str]: 92 | return { 93 | 'lttng_ust_cyg_profile_fast:func_entry', 94 | 'lttng_ust_cyg_profile_fast:func_exit', 95 | 'sched_switch', 96 | } 97 | 98 | @property 99 | def data(self) -> ProfileDataModel: 100 | return super().data # type: ignore 101 | 102 | @staticmethod 103 | def addr_to_int(addr: Union[int, str]) -> int: 104 | """Transform an address into an `int` if it's a hex `str`.""" 105 | return int(addr, 16) if isinstance(addr, str) else addr 106 | 107 | def _handle_sched_switch( 108 | self, event: Dict, metadata: EventMetadata 109 | ) -> None: 110 | timestamp = metadata.timestamp 111 | # If function(s) currently running stop(s) executing 112 | prev_tid = get_field(event, 'prev_tid') 113 | prev_info_list = self._current_funcs.get(prev_tid, None) 114 | if prev_info_list is not None: 115 | # Increment durations using last start timestamp 116 | for info in prev_info_list: 117 | last_start = info[2] 118 | total_duration = info[3] 119 | total_duration += timestamp - last_start 120 | info[2] = -1 121 | info[3] = total_duration 122 | # If stopped function(s) start(s) executing again 123 | next_tid = get_field(event, 'next_tid') 124 | next_info_list = self._current_funcs.get(next_tid, None) 125 | if next_info_list is not None: 126 | # Set last start timestamp to now 127 | for info in next_info_list: 128 | assert info[2] == -1 129 | info[2] = timestamp 130 | 131 | def _handle_function_entry( 132 | self, event: Dict, metadata: EventMetadata 133 | ) -> None: 134 | function_name = self._get_function_name(event) 135 | # Push function data to stack, setting both timestamps to now 136 | self._current_funcs[metadata.tid].append([ 137 | function_name, 138 | metadata.timestamp, 139 | metadata.timestamp, 140 | 0, 141 | ]) 142 | 143 | def _handle_function_exit( 144 | self, event: Dict, metadata: EventMetadata 145 | ) -> None: 146 | # Pop function data from stack 147 | tid = metadata.tid 148 | tid_functions = self._current_funcs[tid] 149 | function_depth = len(tid_functions) - 1 150 | info = tid_functions.pop() 151 | function_name = info[0] 152 | start_timestamp = info[1] 153 | last_start_timestamp = info[2] 154 | total_duration = info[3] 155 | # Add to data model 156 | parent_name = tid_functions[-1][0] if function_depth > 0 else None 157 | duration = metadata.timestamp - start_timestamp 158 | actual_duration = (metadata.timestamp - last_start_timestamp) + total_duration 159 | self.data.add_duration( 160 | tid, 161 | function_depth, 162 | function_name, # type: ignore 163 | parent_name, # type: ignore 164 | start_timestamp, # type: ignore 165 | duration, 166 | actual_duration, 167 | ) 168 | 169 | def _get_function_name( 170 | self, event: Dict 171 | ) -> str: 172 | address = get_field(event, 'addr') 173 | resolution = self._resolve_function_address(address) 174 | if resolution is None: 175 | resolution = self.int_to_hex_str(address) 176 | return resolution 177 | 178 | def _resolve_function_address( 179 | self, address: int 180 | ) -> Union[str, None]: 181 | return self._address_to_func.get(address, None) 182 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/processor/ros2.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # Copyright 2020 Christophe Bedard 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """Module for trace events processor and ROS 2 model creation.""" 17 | 18 | from typing import Dict 19 | from typing import Set 20 | from typing import Tuple 21 | 22 | from tracetools_read import get_field 23 | from tracetools_trace.tools import tracepoints as tp 24 | 25 | from . import EventHandler 26 | from . import EventMetadata 27 | from . import HandlerMap 28 | from ..data_model.ros2 import Ros2DataModel 29 | 30 | 31 | class Ros2Handler(EventHandler): 32 | """ 33 | ROS 2-aware event handling class implementation. 34 | 35 | Handles a trace's events and builds a model with the data. 36 | """ 37 | 38 | def __init__( 39 | self, 40 | **kwargs, 41 | ) -> None: 42 | """Create a Ros2Handler.""" 43 | # Link a ROS trace event to its corresponding handling method 44 | handler_map: HandlerMap = { 45 | tp.rcl_init: 46 | self._handle_rcl_init, 47 | tp.rcl_node_init: 48 | self._handle_rcl_node_init, 49 | tp.rmw_publisher_init: 50 | self._handle_rmw_publisher_init, 51 | tp.rcl_publisher_init: 52 | self._handle_rcl_publisher_init, 53 | tp.rclcpp_publish: 54 | self._handle_rclcpp_publish, 55 | tp.rcl_publish: 56 | self._handle_rcl_publish, 57 | tp.rmw_publish: 58 | self._handle_rmw_publish, 59 | tp.rmw_subscription_init: 60 | self._handle_rmw_subscription_init, 61 | tp.rcl_subscription_init: 62 | self._handle_rcl_subscription_init, 63 | tp.rclcpp_subscription_init: 64 | self._handle_rclcpp_subscription_init, 65 | tp.rclcpp_subscription_callback_added: 66 | self._handle_rclcpp_subscription_callback_added, 67 | tp.rmw_take: 68 | self._handle_rmw_take, 69 | tp.rcl_take: 70 | self._handle_rcl_take, 71 | tp.rclcpp_take: 72 | self._handle_rclcpp_take, 73 | tp.rcl_service_init: 74 | self._handle_rcl_service_init, 75 | tp.rclcpp_service_callback_added: 76 | self._handle_rclcpp_service_callback_added, 77 | tp.rcl_client_init: 78 | self._handle_rcl_client_init, 79 | tp.rcl_timer_init: 80 | self._handle_rcl_timer_init, 81 | tp.rclcpp_timer_callback_added: 82 | self._handle_rclcpp_timer_callback_added, 83 | tp.rclcpp_timer_link_node: 84 | self._handle_rclcpp_timer_link_node, 85 | tp.rclcpp_callback_register: 86 | self._handle_rclcpp_callback_register, 87 | tp.callback_start: 88 | self._handle_callback_start, 89 | tp.callback_end: 90 | self._handle_callback_end, 91 | tp.rcl_lifecycle_state_machine_init: 92 | self._handle_rcl_lifecycle_state_machine_init, 93 | tp.rcl_lifecycle_transition: 94 | self._handle_rcl_lifecycle_transition, 95 | } 96 | super().__init__( 97 | handler_map=handler_map, 98 | data_model=Ros2DataModel(), 99 | **kwargs, 100 | ) 101 | 102 | # Temporary buffers 103 | self._callback_instances: Dict[int, Tuple[Dict, EventMetadata]] = {} 104 | 105 | @staticmethod 106 | def required_events() -> Set[str]: 107 | return { 108 | tp.rcl_init, 109 | } 110 | 111 | @property 112 | def data(self) -> Ros2DataModel: 113 | return super().data # type: ignore 114 | 115 | def _handle_rcl_init( 116 | self, event: Dict, metadata: EventMetadata, 117 | ) -> None: 118 | context_handle = get_field(event, 'context_handle') 119 | timestamp = metadata.timestamp 120 | pid = metadata.pid 121 | version = get_field(event, 'version') 122 | self.data.add_context(context_handle, timestamp, pid, version) 123 | 124 | def _handle_rcl_node_init( 125 | self, event: Dict, metadata: EventMetadata, 126 | ) -> None: 127 | handle = get_field(event, 'node_handle') 128 | timestamp = metadata.timestamp 129 | tid = metadata.tid 130 | rmw_handle = get_field(event, 'rmw_handle') 131 | name = get_field(event, 'node_name') 132 | namespace = get_field(event, 'namespace') 133 | self.data.add_node(handle, timestamp, tid, rmw_handle, name, namespace) 134 | 135 | def _handle_rmw_publisher_init( 136 | self, event: Dict, metadata: EventMetadata, 137 | ) -> None: 138 | handle = get_field(event, 'rmw_publisher_handle') 139 | timestamp = metadata.timestamp 140 | gid = get_field(event, 'gid') 141 | self.data.add_rmw_publisher(handle, timestamp, gid) 142 | 143 | def _handle_rcl_publisher_init( 144 | self, event: Dict, metadata: EventMetadata, 145 | ) -> None: 146 | handle = get_field(event, 'publisher_handle') 147 | timestamp = metadata.timestamp 148 | node_handle = get_field(event, 'node_handle') 149 | rmw_handle = get_field(event, 'rmw_publisher_handle') 150 | topic_name = get_field(event, 'topic_name') 151 | depth = get_field(event, 'queue_depth') 152 | self.data.add_rcl_publisher(handle, timestamp, node_handle, rmw_handle, topic_name, depth) 153 | 154 | def _handle_rclcpp_publish( 155 | self, event: Dict, metadata: EventMetadata, 156 | ) -> None: 157 | timestamp = metadata.timestamp 158 | message = get_field(event, 'message') 159 | self.data.add_rclcpp_publish_instance(timestamp, message) 160 | 161 | def _handle_rcl_publish( 162 | self, event: Dict, metadata: EventMetadata, 163 | ) -> None: 164 | handle = get_field(event, 'publisher_handle') 165 | timestamp = metadata.timestamp 166 | message = get_field(event, 'message') 167 | self.data.add_rcl_publish_instance(handle, timestamp, message) 168 | 169 | def _handle_rmw_publish( 170 | self, event: Dict, metadata: EventMetadata, 171 | ) -> None: 172 | timestamp = metadata.timestamp 173 | message = get_field(event, 'message') 174 | self.data.add_rmw_publish_instance(timestamp, message) 175 | 176 | def _handle_rmw_subscription_init( 177 | self, event: Dict, metadata: EventMetadata, 178 | ) -> None: 179 | handle = get_field(event, 'rmw_subscription_handle') 180 | timestamp = metadata.timestamp 181 | gid = get_field(event, 'gid') 182 | self.data.add_rmw_subscription(handle, timestamp, gid) 183 | 184 | def _handle_rcl_subscription_init( 185 | self, event: Dict, metadata: EventMetadata, 186 | ) -> None: 187 | handle = get_field(event, 'subscription_handle') 188 | timestamp = metadata.timestamp 189 | node_handle = get_field(event, 'node_handle') 190 | rmw_handle = get_field(event, 'rmw_subscription_handle') 191 | topic_name = get_field(event, 'topic_name') 192 | depth = get_field(event, 'queue_depth') 193 | self.data.add_rcl_subscription( 194 | handle, timestamp, node_handle, rmw_handle, topic_name, depth, 195 | ) 196 | 197 | def _handle_rclcpp_subscription_init( 198 | self, event: Dict, metadata: EventMetadata, 199 | ) -> None: 200 | subscription_pointer = get_field(event, 'subscription') 201 | timestamp = metadata.timestamp 202 | handle = get_field(event, 'subscription_handle') 203 | self.data.add_rclcpp_subscription(subscription_pointer, timestamp, handle) 204 | 205 | def _handle_rclcpp_subscription_callback_added( 206 | self, event: Dict, metadata: EventMetadata, 207 | ) -> None: 208 | subscription_pointer = get_field(event, 'subscription') 209 | timestamp = metadata.timestamp 210 | callback_object = get_field(event, 'callback') 211 | self.data.add_callback_object(subscription_pointer, timestamp, callback_object) 212 | 213 | def _handle_rmw_take( 214 | self, event: Dict, metadata: EventMetadata, 215 | ) -> None: 216 | subscription_handle = get_field(event, 'rmw_subscription_handle') 217 | timestamp = metadata.timestamp 218 | message = get_field(event, 'message') 219 | source_timestamp = get_field(event, 'source_timestamp') 220 | taken = bool(get_field(event, 'taken')) 221 | self.data.add_rmw_take_instance( 222 | subscription_handle, timestamp, message, source_timestamp, taken 223 | ) 224 | 225 | def _handle_rcl_take( 226 | self, event: Dict, metadata: EventMetadata, 227 | ) -> None: 228 | timestamp = metadata.timestamp 229 | message = get_field(event, 'message') 230 | self.data.add_rcl_take_instance(timestamp, message) 231 | 232 | def _handle_rclcpp_take( 233 | self, event: Dict, metadata: EventMetadata, 234 | ) -> None: 235 | timestamp = metadata.timestamp 236 | message = get_field(event, 'message') 237 | self.data.add_rclcpp_take_instance(timestamp, message) 238 | 239 | def _handle_rcl_service_init( 240 | self, event: Dict, metadata: EventMetadata, 241 | ) -> None: 242 | handle = get_field(event, 'service_handle') 243 | timestamp = metadata.timestamp 244 | node_handle = get_field(event, 'node_handle') 245 | rmw_handle = get_field(event, 'rmw_service_handle') 246 | service_name = get_field(event, 'service_name') 247 | self.data.add_service(handle, timestamp, node_handle, rmw_handle, service_name) 248 | 249 | def _handle_rclcpp_service_callback_added( 250 | self, event: Dict, metadata: EventMetadata, 251 | ) -> None: 252 | handle = get_field(event, 'service_handle') 253 | timestamp = metadata.timestamp 254 | callback_object = get_field(event, 'callback') 255 | self.data.add_callback_object(handle, timestamp, callback_object) 256 | 257 | def _handle_rcl_client_init( 258 | self, event: Dict, metadata: EventMetadata, 259 | ) -> None: 260 | handle = get_field(event, 'client_handle') 261 | timestamp = metadata.timestamp 262 | node_handle = get_field(event, 'node_handle') 263 | rmw_handle = get_field(event, 'rmw_client_handle') 264 | service_name = get_field(event, 'service_name') 265 | self.data.add_client(handle, timestamp, node_handle, rmw_handle, service_name) 266 | 267 | def _handle_rcl_timer_init( 268 | self, event: Dict, metadata: EventMetadata, 269 | ) -> None: 270 | handle = get_field(event, 'timer_handle') 271 | timestamp = metadata.timestamp 272 | period = get_field(event, 'period') 273 | tid = metadata.tid 274 | self.data.add_timer(handle, timestamp, period, tid) 275 | 276 | def _handle_rclcpp_timer_callback_added( 277 | self, event: Dict, metadata: EventMetadata, 278 | ) -> None: 279 | handle = get_field(event, 'timer_handle') 280 | timestamp = metadata.timestamp 281 | callback_object = get_field(event, 'callback') 282 | self.data.add_callback_object(handle, timestamp, callback_object) 283 | 284 | def _handle_rclcpp_timer_link_node( 285 | self, event: Dict, metadata: EventMetadata, 286 | ) -> None: 287 | handle = get_field(event, 'timer_handle') 288 | timestamp = metadata.timestamp 289 | node_handle = get_field(event, 'node_handle') 290 | self.data.add_timer_node_link(handle, timestamp, node_handle) 291 | 292 | def _handle_rclcpp_callback_register( 293 | self, event: Dict, metadata: EventMetadata, 294 | ) -> None: 295 | callback_object = get_field(event, 'callback') 296 | timestamp = metadata.timestamp 297 | symbol = get_field(event, 'symbol') 298 | self.data.add_callback_symbol(callback_object, timestamp, symbol) 299 | 300 | def _handle_callback_start( 301 | self, event: Dict, metadata: EventMetadata, 302 | ) -> None: 303 | # Add to dict 304 | callback_addr = get_field(event, 'callback') 305 | self._callback_instances[callback_addr] = (event, metadata) 306 | 307 | def _handle_callback_end( 308 | self, event: Dict, metadata: EventMetadata, 309 | ) -> None: 310 | # Fetch from dict 311 | callback_object = get_field(event, 'callback') 312 | callback_instance_data = self._callback_instances.get(callback_object) 313 | if callback_instance_data is not None: 314 | (event_start, metadata_start) = callback_instance_data 315 | del self._callback_instances[callback_object] 316 | duration = metadata.timestamp - metadata_start.timestamp 317 | is_intra_process = get_field(event_start, 'is_intra_process', raise_if_not_found=False) 318 | self.data.add_callback_instance( 319 | callback_object, 320 | metadata_start.timestamp, 321 | duration, 322 | bool(is_intra_process)) 323 | else: 324 | print(f'No matching callback start for callback object "{callback_object}"') 325 | 326 | def _handle_rcl_lifecycle_state_machine_init( 327 | self, event: Dict, metadata: EventMetadata, 328 | ) -> None: 329 | node_handle = get_field(event, 'node_handle') 330 | state_machine = get_field(event, 'state_machine') 331 | self.data.add_lifecycle_state_machine(state_machine, node_handle) 332 | 333 | def _handle_rcl_lifecycle_transition( 334 | self, event: Dict, metadata: EventMetadata, 335 | ) -> None: 336 | timestamp = metadata.timestamp 337 | state_machine = get_field(event, 'state_machine') 338 | start_label = get_field(event, 'start_label') 339 | goal_label = get_field(event, 'goal_label') 340 | self.data.add_lifecycle_state_transition(state_machine, start_label, goal_label, timestamp) 341 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Apex.AI, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import sys 16 | from typing import List 17 | 18 | 19 | def get_input_path( 20 | argv: List[str] = sys.argv, 21 | ) -> str: 22 | if len(argv) < 2: 23 | print('Syntax: [trace directory | converted tracefile]') 24 | sys.exit(1) 25 | return argv[1] 26 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/scripts/auto.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Apex.AI, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from tracetools_analysis.loading import load_file 16 | from tracetools_analysis.processor import AutoProcessor 17 | 18 | from . import get_input_path 19 | 20 | 21 | def main(): 22 | input_path = get_input_path() 23 | 24 | events = load_file(input_path) 25 | processor = AutoProcessor(events) 26 | processor.print_data() 27 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/scripts/cb_durations.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | # Copyright 2019 Robert Bosch GmbH 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import numpy as np 17 | import pandas as pd 18 | 19 | from tracetools_analysis.loading import load_file 20 | from tracetools_analysis.processor.ros2 import Ros2Handler 21 | from tracetools_analysis.utils.ros2 import Ros2DataModelUtil 22 | 23 | from . import get_input_path 24 | 25 | 26 | removals = [ 27 | 'void (', 'rclcpp::', 'std::shared_ptr<', '>', '::msg' 28 | ] 29 | replaces = [ 30 | ('?)', '?') 31 | ] 32 | 33 | 34 | def format_fn(fname: str): 35 | for r in removals: 36 | fname = fname.replace(r, '') 37 | for a, b in replaces: 38 | fname = fname.replace(a, b) 39 | 40 | return fname 41 | 42 | 43 | def main(): 44 | input_path = get_input_path() 45 | 46 | events = load_file(input_path) 47 | handler = Ros2Handler.process(events) 48 | du = Ros2DataModelUtil(handler.data) 49 | 50 | stat_data = [] 51 | for ptr, name in du.get_callback_symbols().items(): 52 | # Convert to milliseconds to display it 53 | durations = du.get_callback_durations(ptr)['duration'] * 1000 / np.timedelta64(1, 's') 54 | stat_data.append(( 55 | durations.count(), 56 | durations.sum(), 57 | durations.mean(), 58 | durations.std(), 59 | format_fn(name), 60 | )) 61 | 62 | stat_df = pd.DataFrame( 63 | columns=['Count', 'Sum (ms)', 'Mean (ms)', 'Std', 'Name'], 64 | data=stat_data, 65 | ) 66 | print(stat_df.sort_values(by='Sum (ms)', ascending=False).to_string()) 67 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/scripts/memory_usage.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Apex.AI, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from tracetools_analysis.loading import load_file 16 | from tracetools_analysis.processor import Processor 17 | from tracetools_analysis.processor.memory_usage import KernelMemoryUsageHandler 18 | from tracetools_analysis.processor.memory_usage import UserspaceMemoryUsageHandler 19 | from tracetools_analysis.processor.ros2 import Ros2Handler 20 | from tracetools_analysis.utils.memory_usage import MemoryUsageDataModelUtil 21 | from tracetools_analysis.utils.ros2 import Ros2DataModelUtil 22 | 23 | from . import get_input_path 24 | 25 | 26 | def main(): 27 | input_path = get_input_path() 28 | 29 | events = load_file(input_path) 30 | ust_memory_handler = UserspaceMemoryUsageHandler() 31 | kernel_memory_handler = KernelMemoryUsageHandler() 32 | ros2_handler = Ros2Handler() 33 | Processor(ust_memory_handler, kernel_memory_handler, ros2_handler).process(events) 34 | 35 | memory_data_util = MemoryUsageDataModelUtil( 36 | userspace=ust_memory_handler.data, 37 | kernel=kernel_memory_handler.data, 38 | ) 39 | ros2_data_util = Ros2DataModelUtil(ros2_handler.data) 40 | 41 | summary_df = memory_data_util.get_max_memory_usage_per_tid() 42 | tids = ros2_data_util.get_tids() 43 | filtered_df = summary_df.loc[summary_df['tid'].isin(tids)] 44 | print('\n' + filtered_df.to_string(index=False)) 45 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Module for data model utility classes.""" 16 | 17 | from datetime import datetime as dt 18 | from typing import List 19 | from typing import Optional 20 | from typing import Union 21 | 22 | import numpy as np 23 | from pandas import DataFrame 24 | 25 | from ..data_model import DataModel 26 | from ..processor import EventHandler 27 | 28 | 29 | class DataModelUtil(): 30 | """ 31 | Base data model util class, which provides functions to get more info about a data model. 32 | 33 | This class provides basic util functions. 34 | """ 35 | 36 | def __init__( 37 | self, 38 | data_object: Union[DataModel, EventHandler, None], 39 | ) -> None: 40 | """ 41 | Create a DataModelUtil. 42 | 43 | :param data_object: the data model or the event handler which has a data model 44 | """ 45 | self.__data = data_object.data if isinstance(data_object, EventHandler) else data_object 46 | 47 | @property 48 | def data(self) -> Optional[DataModel]: 49 | return self.__data 50 | 51 | @staticmethod 52 | def convert_time_columns( 53 | original: DataFrame, 54 | columns_ns_to_ms: Union[List[str], str] = [], 55 | columns_ns_to_datetime: Union[List[str], str] = [], 56 | inplace: bool = True, 57 | ) -> DataFrame: 58 | """ 59 | Convert time columns from nanoseconds to either milliseconds or `datetime` objects. 60 | 61 | :param original: the original `DataFrame` 62 | :param columns_ns_to_ms: the column(s) for which to convert ns to ms 63 | :param columns_ns_to_datetime: the column(s) for which to convert ns to `datetime` 64 | :param inplace: whether to convert in place or to return a copy 65 | :return: the resulting `DataFrame` 66 | """ 67 | if not isinstance(columns_ns_to_ms, list): 68 | columns_ns_to_ms = list(columns_ns_to_ms) 69 | if not isinstance(columns_ns_to_datetime, list): 70 | columns_ns_to_datetime = list(columns_ns_to_datetime) 71 | 72 | df = original if inplace else original.copy() 73 | # Convert from ns to ms 74 | if len(columns_ns_to_ms) > 0: 75 | df[columns_ns_to_ms] = df[columns_ns_to_ms].applymap( 76 | lambda t: t / 1000000.0 if not np.isnan(t) else t 77 | ) 78 | # Convert from ns to ms + ms to datetime, as UTC 79 | if len(columns_ns_to_datetime) > 0: 80 | df[columns_ns_to_datetime] = df[columns_ns_to_datetime].applymap( 81 | lambda t: dt.utcfromtimestamp(t / 1000000000.0) if not np.isnan(t) else t 82 | ) 83 | return df 84 | 85 | @staticmethod 86 | def compute_column_difference( 87 | df: DataFrame, 88 | left_column: str, 89 | right_column: str, 90 | diff_column: str, 91 | ) -> None: 92 | """ 93 | Create new column with difference between two columns. 94 | 95 | :param df: the dataframe (inplace) 96 | :param left_column: the name of the left column 97 | :param right_column: the name of the right column 98 | :param diff_column: the name of the new column with differences 99 | """ 100 | df[diff_column] = df.apply(lambda row: row[left_column] - row[right_column], axis=1) 101 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/utils/cpu_time.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Module for CPU time data model utils.""" 16 | 17 | from typing import Union 18 | 19 | from pandas import DataFrame 20 | 21 | from . import DataModelUtil 22 | from ..data_model.cpu_time import CpuTimeDataModel 23 | from ..processor.cpu_time import CpuTimeHandler 24 | 25 | 26 | class CpuTimeDataModelUtil(DataModelUtil): 27 | """CPU time data model utility class.""" 28 | 29 | def __init__( 30 | self, 31 | data_object: Union[CpuTimeDataModel, CpuTimeHandler], 32 | ) -> None: 33 | """ 34 | Create a CpuTimeDataModelUtil. 35 | 36 | :param data_object: the data model or the event handler which has a data model 37 | """ 38 | super().__init__(data_object) 39 | 40 | @property 41 | def data(self) -> CpuTimeDataModel: 42 | return super().data # type: ignore 43 | 44 | def get_time_per_thread(self) -> DataFrame: 45 | """Get a DataFrame of total duration for each thread.""" 46 | return self.data.times.loc[:, ['tid', 'duration']].groupby(by='tid').sum() 47 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/utils/memory_usage.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Apex.AI, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Module for memory usage data model utils.""" 16 | 17 | from collections import defaultdict 18 | from typing import Dict 19 | from typing import List 20 | from typing import Optional 21 | from typing import Union 22 | 23 | from pandas import DataFrame 24 | 25 | from . import DataModelUtil 26 | from ..data_model.memory_usage import MemoryUsageDataModel 27 | from ..processor.memory_usage import KernelMemoryUsageHandler 28 | from ..processor.memory_usage import UserspaceMemoryUsageHandler 29 | 30 | 31 | class MemoryUsageDataModelUtil(DataModelUtil): 32 | """Memory usage data model utility class.""" 33 | 34 | def __init__( 35 | self, 36 | *, 37 | userspace: Union[MemoryUsageDataModel, UserspaceMemoryUsageHandler, None] = None, 38 | kernel: Union[MemoryUsageDataModel, KernelMemoryUsageHandler, None] = None, 39 | ) -> None: 40 | """ 41 | Create a MemoryUsageDataModelUtil. 42 | 43 | At least one non-`None` `MemoryUsageDataModel` must be given. 44 | 45 | :param userspace: the userspace data model object to use or the event handler 46 | :param kernel: the kernel data model object to use or the event handler 47 | """ 48 | if userspace is None and kernel is None: 49 | raise RuntimeError('must provide at least one (userspace or kernel) data model!') 50 | 51 | # Not giving any model to the base class; we'll own them ourselves 52 | super().__init__(None) 53 | 54 | self.data_ust = userspace.data \ 55 | if isinstance(userspace, UserspaceMemoryUsageHandler) else userspace 56 | self.data_kernel = kernel.data \ 57 | if isinstance(kernel, KernelMemoryUsageHandler) else kernel 58 | 59 | @staticmethod 60 | def format_size(size: int, precision: int = 2): 61 | """ 62 | Format a memory size to a string with a units suffix. 63 | 64 | From: https://stackoverflow.com/a/32009595/6476709 65 | 66 | :param size: the memory size, in bytes 67 | :param precision: the number of digits to display after the period 68 | """ 69 | suffixes = ['B', 'KB', 'MB', 'GB', 'TB'] 70 | suffix_index = 0 71 | mem_size = float(size) 72 | while mem_size > 1024.0 and suffix_index < 4: 73 | # Increment the index of the suffix 74 | suffix_index += 1 75 | # Apply the division 76 | mem_size = mem_size / 1024.0 77 | return f'{mem_size:.{precision}f} {suffixes[suffix_index]}' 78 | 79 | def get_max_memory_usage_per_tid(self) -> DataFrame: 80 | """ 81 | Get the maximum memory usage per tid. 82 | 83 | :return dataframe with maximum memory usage (userspace & kernel) per tid 84 | """ 85 | tids_ust = None 86 | tids_kernel = None 87 | ust_memory_usage_dfs = self.get_absolute_userspace_memory_usage_by_tid() 88 | if ust_memory_usage_dfs is not None: 89 | tids_ust = set(ust_memory_usage_dfs.keys()) 90 | kernel_memory_usage_dfs = self.get_absolute_kernel_memory_usage_by_tid() 91 | if kernel_memory_usage_dfs is not None: 92 | tids_kernel = set(kernel_memory_usage_dfs.keys()) 93 | # Use only the userspace tid values if available, otherwise use the kernel tid values 94 | tids = tids_ust or tids_kernel 95 | # Should not happen, since it is checked in __init__ 96 | if tids is None: 97 | raise RuntimeError('no data') 98 | data = [ 99 | [ 100 | tid, 101 | self.format_size(ust_memory_usage_dfs[tid]['memory_usage'].max(), precision=1) 102 | if ust_memory_usage_dfs is not None 103 | and ust_memory_usage_dfs.get(tid) is not None 104 | else None, 105 | self.format_size(kernel_memory_usage_dfs[tid]['memory_usage'].max(), precision=1) 106 | if kernel_memory_usage_dfs is not None 107 | and kernel_memory_usage_dfs.get(tid) is not None 108 | else None, 109 | ] 110 | for tid in tids 111 | ] 112 | return DataFrame(data, columns=['tid', 'max_memory_usage_ust', 'max_memory_usage_kernel']) 113 | 114 | def get_absolute_userspace_memory_usage_by_tid(self) -> Optional[Dict[int, DataFrame]]: 115 | """ 116 | Get absolute userspace memory usage over time per tid. 117 | 118 | :return (tid -> DataFrame of absolute memory usage over time) 119 | """ 120 | if self.data_ust is None: 121 | return None 122 | return self._get_absolute_memory_usage_by_tid(self.data_ust) 123 | 124 | def get_absolute_kernel_memory_usage_by_tid(self) -> Optional[Dict[int, DataFrame]]: 125 | """ 126 | Get absolute kernel memory usage over time per tid. 127 | 128 | :return (tid -> DataFrame of absolute memory usage over time) 129 | """ 130 | if self.data_kernel is None: 131 | return None 132 | return self._get_absolute_memory_usage_by_tid(self.data_kernel) 133 | 134 | def _get_absolute_memory_usage_by_tid( 135 | self, 136 | data_model: MemoryUsageDataModel, 137 | ) -> Dict[int, DataFrame]: 138 | previous: Dict[int, int] = defaultdict(int) 139 | data: Dict[int, List[Dict[str, int]]] = defaultdict(list) 140 | for index, row in data_model.memory_diff.iterrows(): 141 | timestamp = row['timestamp'] 142 | tid = int(row['tid']) 143 | diff = row['memory_diff'] 144 | previous_value = previous[tid] 145 | next_value = previous_value + diff 146 | data[tid].append({ 147 | 'timestamp': timestamp, 148 | 'tid': tid, 149 | 'memory_usage': previous_value, 150 | }) 151 | data[tid].append({ 152 | 'timestamp': timestamp, 153 | 'tid': tid, 154 | 'memory_usage': next_value, 155 | }) 156 | previous[tid] = next_value 157 | return { 158 | tid: self.convert_time_columns( 159 | DataFrame(data[tid], columns=['timestamp', 'tid', 'memory_usage']), 160 | columns_ns_to_datetime=['timestamp'], 161 | inplace=True, 162 | ) 163 | for tid in data 164 | } 165 | -------------------------------------------------------------------------------- /tracetools_analysis/tracetools_analysis/utils/profile.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Robert Bosch GmbH 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Module for profiling data model utils.""" 16 | 17 | from collections import defaultdict 18 | from typing import Dict 19 | from typing import List 20 | from typing import Set 21 | from typing import Union 22 | 23 | from pandas import DataFrame 24 | 25 | from . import DataModelUtil 26 | from ..data_model.profile import ProfileDataModel 27 | from ..processor.profile import ProfileHandler 28 | 29 | 30 | class ProfileDataModelUtil(DataModelUtil): 31 | """Profiling data model utility class.""" 32 | 33 | def __init__( 34 | self, 35 | data_object: Union[ProfileDataModel, ProfileHandler], 36 | ) -> None: 37 | """ 38 | Create a ProfileDataModelUtil. 39 | 40 | :param data_object: the data model or the event handler which has a data model 41 | """ 42 | super().__init__(data_object) 43 | 44 | @property 45 | def data(self) -> ProfileDataModel: 46 | return super().data # type: ignore 47 | 48 | def with_tid( 49 | self, 50 | tid: int, 51 | ) -> DataFrame: 52 | return self.data.times.loc[self.data.times['tid'] == tid] 53 | 54 | def get_tids(self) -> Set[int]: 55 | """Get the TIDs in the data model.""" 56 | return set(self.data.times['tid']) 57 | 58 | def get_call_tree( 59 | self, 60 | tid: int, 61 | ) -> Dict[str, Set[str]]: 62 | depth_names = self.with_tid(tid)[ 63 | ['depth', 'function_name', 'parent_name'] 64 | ].drop_duplicates() 65 | # print(depth_names.to_string()) 66 | tree: Dict[str, Set[str]] = defaultdict(set) 67 | for _, row in depth_names.iterrows(): 68 | depth = row['depth'] 69 | name = row['function_name'] 70 | parent = row['parent_name'] 71 | if depth == 0: 72 | tree[name] 73 | else: 74 | tree[parent].add(name) 75 | return dict(tree) 76 | 77 | def get_function_duration_data( 78 | self, 79 | tid: int, 80 | ) -> List[Dict[str, Union[int, str, DataFrame]]]: 81 | """Get duration data for each function.""" 82 | tid_df = self.with_tid(tid) 83 | depth_names = tid_df[['depth', 'function_name', 'parent_name']].drop_duplicates() 84 | functions_data = [] 85 | for _, row in depth_names.iterrows(): 86 | depth = row['depth'] 87 | name = row['function_name'] 88 | parent = row['parent_name'] 89 | data = tid_df.loc[ 90 | (tid_df['depth'] == depth) & 91 | (tid_df['function_name'] == name) 92 | ][['start_timestamp', 'duration', 'actual_duration']] 93 | self.compute_column_difference( 94 | data, 95 | 'duration', 96 | 'actual_duration', 97 | 'duration_difference', 98 | ) 99 | functions_data.append({ 100 | 'depth': depth, 101 | 'function_name': name, 102 | 'parent_name': parent, 103 | 'data': data, 104 | }) 105 | return functions_data 106 | --------------------------------------------------------------------------------