├── .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 | [](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 |
--------------------------------------------------------------------------------