├── .gitignore ├── Cargo.toml ├── LICENSE-MIT ├── .vscode └── launch.json ├── README.md ├── LICENSE-APACHE └── src ├── context_switch.rs └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /fixtures 3 | .DS_Store 4 | /Cargo.lock 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fxprof-perf-convert" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | byteorder = "1.4.3" 11 | debugid = "0.8.0" 12 | framehop = "0.7.1" 13 | # framehop = { path = "../framehop" } 14 | memchr = "2.4.1" 15 | memmap2 = "0.5.3" 16 | object = "0.28.3" 17 | profiler-get-symbols = "0.14.0" 18 | # profiler-get-symbols = { path = "../profiler-get-symbols/lib" } 19 | fxprof-processed-profile = "0.4.0" 20 | # fxprof-processed-profile = { path = "../perfrecord/fxprof_processed_profile" } 21 | linux-perf-data = "0.6.0" 22 | # linux-perf-data = { path = "../linux-perf-data" } 23 | serde_json = "1.0.81" 24 | 25 | [profile.release] 26 | debug = true 27 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Markus Stange 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'fxprof-perf-convert'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=fxprof-perf-convert", 15 | "--package=fxprof-perf-convert" 16 | ], 17 | "filter": { 18 | "name": "fxprof-perf-convert", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": ["fixtures/x86_64/rustup-perf.data"], 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | { 26 | "type": "lldb", 27 | "request": "launch", 28 | "name": "Debug unit tests in executable 'fxprof-perf-convert'", 29 | "cargo": { 30 | "args": [ 31 | "test", 32 | "--no-run", 33 | "--bin=fxprof-perf-convert", 34 | "--package=fxprof-perf-convert" 35 | ], 36 | "filter": { 37 | "name": "fxprof-perf-convert", 38 | "kind": "bin" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fxprof-perf-convert 2 | 3 | A converter from the Linux perf `perf.data` format into the [Firefox Profiler](https://profiler.firefox.com/) format, specifically into the [processed profile format](https://crates.io/crates/fxprof-processed-profile). 4 | 5 | Here's an [example profile of Firefox](https://share.firefox.dev/37QbKlM). And here's [a profile of the conversion process of that example](https://share.firefox.dev/3wh6CQZ). Both of these profiles were obtained by running `perf record --call-graph dwarf` and then converting the perf.data file. 6 | 7 | ## Run 8 | 9 | For best results, run perf record as root and use the following arguments to capture context switch events and off-cpu stacks: 10 | 11 | ``` 12 | $ sudo perf record -e cycles -e sched:sched_switch --switch-events --sample-cpu -m 8M --aio --call-graph dwarf,32768 --pid 13 | $ sudo chown $USER perf.data 14 | ``` 15 | 16 | Then run the converter: 17 | 18 | ``` 19 | $ cargo run --release -- perf.data 20 | ``` 21 | 22 | This creates a file called `profile-conv.json`. 23 | 24 | Then open the profile in the Firefox profiler: 25 | 26 | ``` 27 | $ profiler-symbol-server profile-conv.json # Install with `cargo install profiler-symbol-server` 28 | ``` 29 | 30 | That's it. 31 | 32 | ## More command lines 33 | 34 | If you don't want to attach to an existing process, and instead want to launch a new process, you can use something like this: 35 | 36 | ``` 37 | $ sudo perf record -e cycles -e sched:sched_switch --switch-events --sample-cpu -m 8M --aio --call-graph dwarf,32768 sudo -u $USER env "PATH=$PATH" sh -c 'YOUR COMMAND' && sudo chown $USER perf.data 38 | ``` 39 | 40 | It's not the best. If you know of a better way to make perf run as root and invoke a program as non-root, please let me know. Thanks! 41 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/context_switch.rs: -------------------------------------------------------------------------------- 1 | /// Accumulates thread running times (for "CPU deltas") and simulates off-cpu sampling, 2 | /// with the help of context switch events. 3 | /// 4 | /// In the Firefox Profiler format, a sample's "CPU delta" is the accumulated duration 5 | /// for which the thread was running on the CPU since the previous sample. 6 | /// 7 | /// # Off-CPU sampling 8 | /// 9 | /// The goal of off-cpu sampling is to know what happened on the thread between on-CPU 10 | /// samples, with about the same "accuracy" as for on-CPU samples. 11 | /// 12 | /// There can be lots of context switch events and we don't want to flood the profile 13 | /// with a sample for each context switch. 14 | /// 15 | /// However, we also don't want to enforce a "minimum sleep time" because doing so 16 | /// would skew the weighting of short sleeps. Really, what we're after is something 17 | /// that looks comparable to what a wall-clock profiler would produce. 18 | /// 19 | /// We solve this by accumulating the off-cpu duration of all thread sleeps, regardless 20 | /// of the individual sleep length. Once the accumulated off-cpu duration exceeds a 21 | /// threshold (a multiple of the off-cpu "sampling interval"), we emit a sample. 22 | /// 23 | /// ## Details 24 | /// 25 | /// A thread is either running (= on-cpu) or sleeping (= off-cpu). 26 | /// 27 | /// The running time is the time between switch-in and switch-out. 28 | /// The sleeping time is the time between switch-out and switch-in. 29 | /// 30 | /// After every thread sleep, we accumulate the off-cpu time. 31 | /// Now there are two cases: 32 | /// 33 | /// Does the accumulated time cross an "off-cpu sampling" threshold? 34 | /// If yes, turn it into an off-cpu sampling group and consume a multiple of the interval. 35 | /// If no, don't emit any samples. The next sample's cpu delta will just be smaller. 36 | pub struct ContextSwitchHandler { 37 | off_cpu_sampling_interval_ns: u64, 38 | } 39 | 40 | impl ContextSwitchHandler { 41 | pub fn new(off_cpu_sampling_interval_ns: u64) -> Self { 42 | Self { 43 | off_cpu_sampling_interval_ns, 44 | } 45 | } 46 | 47 | pub fn handle_switch_out(&self, timestamp: u64, thread: &mut ThreadContextSwitchData) { 48 | match &thread.state { 49 | ThreadState::Unknown => { 50 | // This "switch-out" is the first time we've heard of the thread. So it must 51 | // have been running until just now, but we didn't get any samples from it. 52 | 53 | // Just store the new state. 54 | thread.state = ThreadState::Off { 55 | off_switch_timestamp: timestamp, 56 | }; 57 | } 58 | 59 | ThreadState::On { 60 | last_observed_on_timestamp, 61 | } => { 62 | // The thread was running and is now context-switched out. 63 | // Accumulate the running time since we last saw it. This delta will be picked 64 | // up by the next sample we emit. 65 | let on_duration = timestamp - last_observed_on_timestamp; 66 | thread.on_cpu_duration_since_last_sample += on_duration; 67 | 68 | thread.state = ThreadState::Off { 69 | off_switch_timestamp: timestamp, 70 | }; 71 | } 72 | ThreadState::Off { .. } => { 73 | // We are already in the Off state but received another Switch-Out record. 74 | // This is unexpected; Switch-Out records are the only records that can 75 | // get us into the Off state and we do not expect two Switch-Out records 76 | // without an in-between Switch-In record. 77 | // However, in practice this case has been observed due to a duplicated 78 | // Switch-Out record in the perf.data file: the record just appeared twice 79 | // right after itself, same timestamp, same everything. I don't know if 80 | // this duplication indicates a bug in the kernel or in the perf tool or 81 | // maybe is not considered a bug at all. 82 | } 83 | } 84 | } 85 | 86 | pub fn handle_switch_in( 87 | &self, 88 | timestamp: u64, 89 | thread: &mut ThreadContextSwitchData, 90 | ) -> Option { 91 | let off_cpu_sample = match thread.state { 92 | ThreadState::On { 93 | last_observed_on_timestamp, 94 | } => { 95 | // We are already in the On state, most likely due to a Sample record which 96 | // arrived just before the Switch-In record. 97 | // This is quite normal. Thread switching is done by some kernel code which 98 | // executes on the CPU, and this CPU work can get sampled before the CPU gets 99 | // to the code that emits the Switch-In record. 100 | let on_duration = timestamp - last_observed_on_timestamp; 101 | thread.on_cpu_duration_since_last_sample += on_duration; 102 | 103 | None 104 | } 105 | ThreadState::Off { 106 | off_switch_timestamp, 107 | } => { 108 | // The thread was sleeping and is now starting to run again. 109 | // Accumulate the off-cpu time. 110 | let off_duration = timestamp - off_switch_timestamp; 111 | thread.off_cpu_duration_since_last_off_cpu_sample += off_duration; 112 | 113 | // We just added some off-cpu time. If the accumulated off-cpu time exceeds the 114 | // off-cpu sampling interval, we want to consume some of it and turn it into an 115 | // off-cpu sampling group. 116 | self.maybe_consume_off_cpu(timestamp, thread) 117 | } 118 | ThreadState::Unknown => { 119 | // This "switch-in" is the first time we've heard of the thread. 120 | // It must have been sleeping at some stack, but we don't know in what stack, 121 | // so it seems pointless to emit a sample for the sleep time. We also don't 122 | // know what the reason for the "sleep" was: It could have been because the 123 | // thread was blocked (most common) or because it was pre-empted. 124 | 125 | None 126 | } 127 | }; 128 | 129 | thread.state = ThreadState::On { 130 | last_observed_on_timestamp: timestamp, 131 | }; 132 | 133 | off_cpu_sample 134 | } 135 | 136 | pub fn handle_sample( 137 | &self, 138 | timestamp: u64, 139 | thread: &mut ThreadContextSwitchData, 140 | ) -> Option { 141 | let off_cpu_sample = match thread.state { 142 | ThreadState::On { 143 | last_observed_on_timestamp, 144 | } => { 145 | // The last time we heard from this thread, it was already running. 146 | // Accumulate the running time. 147 | let on_duration = timestamp - last_observed_on_timestamp; 148 | thread.on_cpu_duration_since_last_sample += on_duration; 149 | 150 | None 151 | } 152 | ThreadState::Off { 153 | off_switch_timestamp, 154 | } => { 155 | // The last time we heard from this thread, it was being context switched away from. 156 | // We are processing a sample on it so we know it is running again. Treat this sample 157 | // as a switch-in event. 158 | let off_duration = timestamp - off_switch_timestamp; 159 | thread.off_cpu_duration_since_last_off_cpu_sample += off_duration; 160 | 161 | // We just added some off-cpu time. If the accumulated off-cpu time exceeds the 162 | // off-cpu sampling interval, we want to consume some of it and turn it into an 163 | // off-cpu sampling group. 164 | self.maybe_consume_off_cpu(timestamp, thread) 165 | } 166 | ThreadState::Unknown => { 167 | // This sample is the first time we've ever head from a thread. 168 | // We don't know whether it was running or sleeping. 169 | // Do nothing. The first sample will have a CPU delta of 0. 170 | 171 | None 172 | } 173 | }; 174 | 175 | thread.state = ThreadState::On { 176 | last_observed_on_timestamp: timestamp, 177 | }; 178 | 179 | off_cpu_sample 180 | } 181 | 182 | fn maybe_consume_off_cpu( 183 | &self, 184 | timestamp: u64, 185 | thread: &mut ThreadContextSwitchData, 186 | ) -> Option { 187 | // If the accumulated off-cpu time exceeds the off-cpu sampling interval, 188 | // we want to consume some of it and turn it into an off-cpu sampling group. 189 | let interval = self.off_cpu_sampling_interval_ns; 190 | if thread.off_cpu_duration_since_last_off_cpu_sample < interval { 191 | return None; 192 | } 193 | 194 | // Let's turn the accumulated off-cpu time into an off-cpu sample group. 195 | let sample_count = thread.off_cpu_duration_since_last_off_cpu_sample / interval; 196 | debug_assert!(sample_count >= 1); 197 | 198 | let consumed_duration = sample_count * interval; 199 | let remaining_duration = 200 | thread.off_cpu_duration_since_last_off_cpu_sample - consumed_duration; 201 | 202 | let begin_timestamp = 203 | timestamp - (thread.off_cpu_duration_since_last_off_cpu_sample - interval); 204 | let end_timestamp = timestamp - remaining_duration; 205 | debug_assert_eq!( 206 | end_timestamp - begin_timestamp, 207 | (sample_count - 1) * interval 208 | ); 209 | 210 | // Consume the consumed duration and save the leftover duration. 211 | thread.off_cpu_duration_since_last_off_cpu_sample = remaining_duration; 212 | 213 | Some(OffCpuSampleGroup { 214 | begin_timestamp, 215 | end_timestamp, 216 | sample_count, 217 | }) 218 | } 219 | 220 | pub fn consume_cpu_delta(&self, thread: &mut ThreadContextSwitchData) -> u64 { 221 | std::mem::replace(&mut thread.on_cpu_duration_since_last_sample, 0) 222 | } 223 | } 224 | 225 | #[derive(Debug, Clone, PartialEq, Eq)] 226 | pub struct OffCpuSampleGroup { 227 | pub begin_timestamp: u64, 228 | pub end_timestamp: u64, 229 | pub sample_count: u64, 230 | } 231 | 232 | #[derive(Default, Clone, Debug, PartialEq, Eq)] 233 | pub struct ThreadContextSwitchData { 234 | state: ThreadState, 235 | on_cpu_duration_since_last_sample: u64, 236 | off_cpu_duration_since_last_off_cpu_sample: u64, 237 | } 238 | 239 | #[derive(Clone, Debug, PartialEq, Eq)] 240 | enum ThreadState { 241 | Unknown, 242 | Off { off_switch_timestamp: u64 }, 243 | On { last_observed_on_timestamp: u64 }, 244 | } 245 | 246 | impl Default for ThreadState { 247 | fn default() -> Self { 248 | ThreadState::Unknown 249 | } 250 | } 251 | 252 | #[cfg(test)] 253 | mod test { 254 | use super::{ContextSwitchHandler, OffCpuSampleGroup, ThreadContextSwitchData}; 255 | 256 | #[test] 257 | fn it_works() { 258 | // sampling interval: 10 259 | // 260 | // 0 10 20 30 40 50 60 261 | // 01234567890123456789012345678901234567890123456789012345678901 262 | // ===__========__=_____==____===___________________============= 263 | // ^ v v v ^ ^ 264 | // 265 | // Graph legend: 266 | // = Thread is running. 267 | // _ Thread is sleeping. 268 | // ^ On-cpu sample 269 | // v Off-cpu sample 270 | 271 | let mut thread = ThreadContextSwitchData::default(); 272 | let handler = ContextSwitchHandler::new(10); 273 | let s = handler.handle_switch_in(0, &mut thread); 274 | assert_eq!(s, None); 275 | handler.handle_switch_out(3, &mut thread); 276 | let s = handler.handle_switch_in(5, &mut thread); 277 | assert_eq!(s, None); 278 | let s = handler.handle_sample(12, &mut thread); 279 | let delta = handler.consume_cpu_delta(&mut thread); 280 | assert_eq!(s, None); 281 | assert_eq!(delta, 10); 282 | handler.handle_switch_out(13, &mut thread); 283 | let s = handler.handle_switch_in(15, &mut thread); 284 | assert_eq!(s, None); 285 | handler.handle_switch_out(16, &mut thread); 286 | let s = handler.handle_switch_in(21, &mut thread); 287 | assert_eq!(s, None); 288 | handler.handle_switch_out(23, &mut thread); 289 | let s = handler.handle_switch_in(27, &mut thread); 290 | assert_eq!( 291 | s, 292 | Some(OffCpuSampleGroup { 293 | begin_timestamp: 24, 294 | end_timestamp: 24, 295 | sample_count: 1 296 | }) 297 | ); 298 | let delta = handler.consume_cpu_delta(&mut thread); 299 | assert_eq!(delta, 4); 300 | handler.handle_switch_out(30, &mut thread); 301 | let s = handler.handle_switch_in(48, &mut thread); 302 | assert_eq!( 303 | s, 304 | Some(OffCpuSampleGroup { 305 | begin_timestamp: 37, 306 | end_timestamp: 47, 307 | sample_count: 2 308 | }) 309 | ); 310 | let delta = handler.consume_cpu_delta(&mut thread); 311 | assert_eq!(delta, 3); 312 | let s = handler.handle_sample(51, &mut thread); 313 | let delta = handler.consume_cpu_delta(&mut thread); 314 | assert_eq!(s, None); 315 | assert_eq!(delta, 3); 316 | let s = handler.handle_sample(61, &mut thread); 317 | let delta = handler.consume_cpu_delta(&mut thread); 318 | assert_eq!(s, None); 319 | assert_eq!(delta, 10); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod context_switch; 2 | 3 | use byteorder::LittleEndian; 4 | use context_switch::{ContextSwitchHandler, OffCpuSampleGroup, ThreadContextSwitchData}; 5 | use debugid::{CodeId, DebugId}; 6 | use framehop::aarch64::UnwindRegsAarch64; 7 | use framehop::x86_64::UnwindRegsX86_64; 8 | use framehop::{FrameAddress, Module, ModuleSvmaInfo, ModuleUnwindData, TextByteData, Unwinder}; 9 | use fxprof_processed_profile::{ 10 | CategoryColor, CategoryPairHandle, CpuDelta, Frame, LibraryInfo, ProcessHandle, Profile, 11 | ReferenceTimestamp, ThreadHandle, Timestamp, 12 | }; 13 | use linux_perf_data::linux_perf_event_reader; 14 | use linux_perf_data::{AttributeDescription, DsoInfo, DsoKey, PerfFileReader, PerfFileRecord}; 15 | use linux_perf_event_reader::constants::{ 16 | PERF_CONTEXT_GUEST, PERF_CONTEXT_GUEST_KERNEL, PERF_CONTEXT_GUEST_USER, PERF_CONTEXT_KERNEL, 17 | PERF_CONTEXT_MAX, PERF_CONTEXT_USER, PERF_REG_ARM64_LR, PERF_REG_ARM64_PC, PERF_REG_ARM64_SP, 18 | PERF_REG_ARM64_X29, PERF_REG_X86_BP, PERF_REG_X86_IP, PERF_REG_X86_SP, 19 | }; 20 | use linux_perf_event_reader::{ 21 | AttrFlags, CommOrExecRecord, CommonData, ContextSwitchRecord, CpuMode, EventRecord, 22 | ForkOrExitRecord, Mmap2FileId, Mmap2Record, MmapRecord, PerfEventType, RawDataU64, Regs, 23 | SampleRecord, SamplingPolicy, SoftwareCounterType, 24 | }; 25 | use object::{Object, ObjectSection, ObjectSegment, SectionKind}; 26 | use profiler_get_symbols::{debug_id_for_object, DebugIdExt}; 27 | use std::borrow::Cow; 28 | use std::collections::HashMap; 29 | use std::io::{BufReader, BufWriter, Read}; 30 | use std::path::PathBuf; 31 | use std::time::{Duration, SystemTime}; 32 | use std::{fs::File, ops::Range, path::Path}; 33 | 34 | fn main() { 35 | let mut args = std::env::args_os().skip(1); 36 | if args.len() < 1 { 37 | eprintln!("Usage: {} ", std::env::args().next().unwrap()); 38 | std::process::exit(1); 39 | } 40 | let path = args.next().unwrap(); 41 | let path = Path::new(&path) 42 | .canonicalize() 43 | .expect("Couldn't form absolute path"); 44 | 45 | let input_file = File::open(&path).unwrap(); 46 | let reader = BufReader::new(input_file); 47 | let perf_file = PerfFileReader::parse_file(reader).expect("Parsing failed"); 48 | 49 | let profile = match perf_file.perf_file.arch().unwrap() { 50 | Some("x86_64") => { 51 | let cache = framehop::x86_64::CacheX86_64::new(); 52 | convert::>, ConvertRegsX86_64, _>( 53 | perf_file, 54 | path.parent(), 55 | cache, 56 | ) 57 | } 58 | Some("aarch64") => { 59 | let cache = framehop::aarch64::CacheAarch64::new(); 60 | convert::>, ConvertRegsAarch64, _>( 61 | perf_file, 62 | path.parent(), 63 | cache, 64 | ) 65 | } 66 | Some(other_arch) => { 67 | eprintln!("Unsupported arch {}", other_arch); 68 | let cache = framehop::x86_64::CacheX86_64::new(); 69 | convert::>, ConvertRegsX86_64, _>( 70 | perf_file, 71 | path.parent(), 72 | cache, 73 | ) 74 | } 75 | None => { 76 | eprintln!("Can't unwind because I don't know the arch"); 77 | std::process::exit(1); 78 | } 79 | }; 80 | 81 | let output_file = File::create("profile-conv.json").unwrap(); 82 | let writer = BufWriter::new(output_file); 83 | serde_json::to_writer(writer, &profile).expect("Couldn't write JSON"); 84 | eprintln!("Saved converted profile to profile-conv.json"); 85 | } 86 | 87 | trait ConvertRegs { 88 | type UnwindRegs; 89 | fn convert_regs(regs: &Regs) -> (u64, u64, Self::UnwindRegs); 90 | } 91 | 92 | struct ConvertRegsX86_64; 93 | impl ConvertRegs for ConvertRegsX86_64 { 94 | type UnwindRegs = UnwindRegsX86_64; 95 | fn convert_regs(regs: &Regs) -> (u64, u64, UnwindRegsX86_64) { 96 | let ip = regs.get(PERF_REG_X86_IP).unwrap(); 97 | let sp = regs.get(PERF_REG_X86_SP).unwrap(); 98 | let bp = regs.get(PERF_REG_X86_BP).unwrap(); 99 | let regs = UnwindRegsX86_64::new(ip, sp, bp); 100 | (ip, sp, regs) 101 | } 102 | } 103 | 104 | struct ConvertRegsAarch64; 105 | impl ConvertRegs for ConvertRegsAarch64 { 106 | type UnwindRegs = UnwindRegsAarch64; 107 | fn convert_regs(regs: &Regs) -> (u64, u64, UnwindRegsAarch64) { 108 | let ip = regs.get(PERF_REG_ARM64_PC).unwrap(); 109 | let lr = regs.get(PERF_REG_ARM64_LR).unwrap(); 110 | let sp = regs.get(PERF_REG_ARM64_SP).unwrap(); 111 | let fp = regs.get(PERF_REG_ARM64_X29).unwrap(); 112 | let regs = UnwindRegsAarch64::new(lr, sp, fp); 113 | (ip, sp, regs) 114 | } 115 | } 116 | 117 | #[derive(Debug, Clone)] 118 | struct EventInterpretation { 119 | main_event_attr_index: usize, 120 | #[allow(unused)] 121 | main_event_name: String, 122 | sampling_is_time_based: Option, 123 | have_context_switches: bool, 124 | sched_switch_attr_index: Option, 125 | } 126 | 127 | impl EventInterpretation { 128 | pub fn divine_from_attrs(attrs: &[AttributeDescription]) -> Self { 129 | let main_event_attr_index = 0; 130 | let main_event_name = attrs[0] 131 | .name 132 | .as_deref() 133 | .unwrap_or("") 134 | .to_string(); 135 | let sampling_is_time_based = match (attrs[0].attr.type_, attrs[0].attr.sampling_policy) { 136 | (_, SamplingPolicy::NoSampling) => { 137 | panic!("Can only convert profiles with sampled events") 138 | } 139 | (_, SamplingPolicy::Frequency(freq)) => { 140 | let nanos = 1_000_000_000 / freq; 141 | Some(nanos) 142 | } 143 | ( 144 | PerfEventType::Software( 145 | SoftwareCounterType::CpuClock | SoftwareCounterType::TaskClock, 146 | ), 147 | SamplingPolicy::Period(period), 148 | ) => { 149 | // Assume that we're using a nanosecond clock. TODO: Check how we can know this for sure 150 | let nanos = u64::from(period); 151 | Some(nanos) 152 | } 153 | (_, SamplingPolicy::Period(_)) => None, 154 | }; 155 | let have_context_switches = attrs[0].attr.flags.contains(AttrFlags::CONTEXT_SWITCH); 156 | let sched_switch_attr_index = attrs 157 | .iter() 158 | .position(|attr_desc| attr_desc.name.as_deref() == Some("sched:sched_switch")); 159 | 160 | Self { 161 | main_event_attr_index, 162 | main_event_name, 163 | sampling_is_time_based, 164 | have_context_switches, 165 | sched_switch_attr_index, 166 | } 167 | } 168 | } 169 | 170 | fn convert(file: PerfFileReader, extra_dir: Option<&Path>, cache: U::Cache) -> Profile 171 | where 172 | U: Unwinder>> + Default, 173 | C: ConvertRegs, 174 | R: Read, 175 | { 176 | let PerfFileReader { 177 | mut perf_file, 178 | mut record_iter, 179 | } = file; 180 | let build_ids = perf_file.build_ids().ok().unwrap_or_default(); 181 | let first_sample_time = perf_file 182 | .sample_time_range() 183 | .unwrap() 184 | .map_or(0, |r| r.first_sample_time); 185 | let little_endian = perf_file.endian() == linux_perf_data::Endianness::LittleEndian; 186 | let host = perf_file.hostname().unwrap().unwrap_or(""); 187 | let perf_version = perf_file 188 | .perf_version() 189 | .unwrap() 190 | .unwrap_or(""); 191 | let linux_version = perf_file.os_release().unwrap(); 192 | let attributes = perf_file.event_attributes(); 193 | for event_name in attributes.iter().filter_map(|attr| attr.name()) { 194 | println!("event {}", event_name); 195 | } 196 | let interpretation = EventInterpretation::divine_from_attrs(attributes); 197 | 198 | let product = "Converted perf profile"; 199 | let mut converter = Converter::::new( 200 | product, 201 | build_ids, 202 | first_sample_time, 203 | host, 204 | perf_version, 205 | linux_version, 206 | little_endian, 207 | cache, 208 | extra_dir, 209 | interpretation.clone(), 210 | ); 211 | 212 | let mut last_timestamp = 0; 213 | 214 | while let Ok(Some(record)) = record_iter.next_record(&mut perf_file) { 215 | let (record, parsed_record, attr_index) = match record { 216 | PerfFileRecord::EventRecord { attr_index, record } => match record.parse() { 217 | Ok(r) => (record, r, attr_index), 218 | Err(_) => continue, 219 | }, 220 | PerfFileRecord::UserRecord(_) => continue, 221 | }; 222 | if let Some(timestamp) = record.timestamp() { 223 | if timestamp < last_timestamp { 224 | println!( 225 | "bad timestamp ordering; {} is earlier but arrived after {}", 226 | timestamp, last_timestamp 227 | ); 228 | } 229 | last_timestamp = timestamp; 230 | } 231 | match parsed_record { 232 | EventRecord::Sample(e) => { 233 | if attr_index == interpretation.main_event_attr_index { 234 | converter.handle_sample::(e); 235 | } else if interpretation.sched_switch_attr_index == Some(attr_index) { 236 | converter.handle_sched_switch::(e); 237 | } 238 | } 239 | EventRecord::Fork(e) => { 240 | converter.handle_thread_start(e); 241 | } 242 | EventRecord::Comm(e) => { 243 | converter.handle_thread_name_update(e, record.timestamp()); 244 | } 245 | EventRecord::Exit(e) => { 246 | converter.handle_thread_end(e); 247 | } 248 | EventRecord::Mmap(e) => { 249 | converter.handle_mmap(e); 250 | } 251 | EventRecord::Mmap2(e) => { 252 | converter.handle_mmap2(e); 253 | } 254 | EventRecord::ContextSwitch(e) => { 255 | let common = match record.common_data() { 256 | Ok(common) => common, 257 | Err(_) => continue, 258 | }; 259 | converter.handle_context_switch(e, common); 260 | } 261 | _ => { 262 | // println!("{:?}", record.record_type); 263 | } 264 | } 265 | } 266 | 267 | converter.finish() 268 | } 269 | 270 | struct Converter 271 | where 272 | U: Unwinder>> + Default, 273 | { 274 | cache: U::Cache, 275 | profile: Profile, 276 | processes: Processes, 277 | threads: Threads, 278 | stack_converter: StackConverter, 279 | kernel_modules: Vec, 280 | timestamp_converter: TimestampConverter, 281 | current_sample_time: u64, 282 | build_ids: HashMap, 283 | little_endian: bool, 284 | have_product_name: bool, 285 | host: String, 286 | perf_version: String, 287 | linux_version: Option, 288 | extra_binary_artifact_dir: Option, 289 | context_switch_handler: ContextSwitchHandler, 290 | off_cpu_weight_per_sample: i32, 291 | have_context_switches: bool, 292 | } 293 | 294 | const DEFAULT_OFF_CPU_SAMPLING_INTERVAL_NS: u64 = 1_000_000; // 1ms 295 | 296 | impl Converter 297 | where 298 | U: Unwinder>> + Default, 299 | { 300 | #[allow(clippy::too_many_arguments)] 301 | pub fn new( 302 | product: &str, 303 | build_ids: HashMap, 304 | first_sample_time: u64, 305 | host: &str, 306 | perf_version: &str, 307 | linux_version: Option<&str>, 308 | little_endian: bool, 309 | cache: U::Cache, 310 | extra_binary_artifact_dir: Option<&Path>, 311 | interpretation: EventInterpretation, 312 | ) -> Self { 313 | let interval = match interpretation.sampling_is_time_based { 314 | Some(nanos) => Duration::from_nanos(nanos), 315 | None => Duration::from_millis(1), 316 | }; 317 | let mut profile = Profile::new( 318 | product, 319 | ReferenceTimestamp::from_system_time(SystemTime::now()), 320 | interval, 321 | ); 322 | let user_category = profile.add_category("User", CategoryColor::Yellow).into(); 323 | let kernel_category = profile.add_category("Kernel", CategoryColor::Orange).into(); 324 | let (off_cpu_sampling_interval_ns, off_cpu_weight_per_sample) = 325 | match &interpretation.sampling_is_time_based { 326 | Some(interval_ns) => (*interval_ns, 1), 327 | None => (DEFAULT_OFF_CPU_SAMPLING_INTERVAL_NS, 0), 328 | }; 329 | Self { 330 | profile, 331 | cache, 332 | processes: Processes(HashMap::new()), 333 | threads: Threads(HashMap::new()), 334 | stack_converter: StackConverter { 335 | user_category, 336 | kernel_category, 337 | }, 338 | kernel_modules: Vec::new(), 339 | timestamp_converter: TimestampConverter::with_reference_timestamp(first_sample_time), 340 | current_sample_time: first_sample_time, 341 | build_ids, 342 | little_endian, 343 | have_product_name: false, 344 | host: host.to_string(), 345 | perf_version: perf_version.to_string(), 346 | linux_version: linux_version.map(ToOwned::to_owned), 347 | extra_binary_artifact_dir: extra_binary_artifact_dir.map(ToOwned::to_owned), 348 | off_cpu_weight_per_sample, 349 | context_switch_handler: ContextSwitchHandler::new(off_cpu_sampling_interval_ns), 350 | have_context_switches: interpretation.have_context_switches, 351 | } 352 | } 353 | 354 | pub fn finish(self) -> Profile { 355 | self.profile 356 | } 357 | 358 | pub fn handle_sample>(&mut self, e: SampleRecord) { 359 | let pid = e.pid.expect("Can't handle samples without pids"); 360 | let tid = e.tid.expect("Can't handle samples without tids"); 361 | let timestamp = e 362 | .timestamp 363 | .expect("Can't handle samples without timestamps"); 364 | self.current_sample_time = timestamp; 365 | 366 | let profile_timestamp = self.timestamp_converter.convert_time(timestamp); 367 | 368 | let is_main = pid == tid; 369 | let process = self 370 | .processes 371 | .get_by_pid(pid, &mut self.profile, &self.kernel_modules); 372 | 373 | let mut stack = Vec::new(); 374 | Self::get_sample_stack::(&e, &process.unwinder, &mut self.cache, &mut stack); 375 | 376 | let thread = 377 | self.threads 378 | .get_by_tid(tid, process.profile_process, is_main, &mut self.profile); 379 | 380 | if thread.last_sample_timestamp == Some(timestamp) { 381 | // Duplicate sample. Ignore. 382 | return; 383 | } 384 | 385 | let thread_handle = thread.profile_thread; 386 | 387 | let off_cpu_sample = self 388 | .context_switch_handler 389 | .handle_sample(timestamp, &mut thread.context_switch_data); 390 | if let Some(off_cpu_sample) = off_cpu_sample { 391 | let cpu_delta_ns = self 392 | .context_switch_handler 393 | .consume_cpu_delta(&mut thread.context_switch_data); 394 | process_off_cpu_sample_group( 395 | off_cpu_sample, 396 | thread_handle, 397 | cpu_delta_ns, 398 | &self.timestamp_converter, 399 | self.off_cpu_weight_per_sample, 400 | &thread.off_cpu_stack, 401 | &mut self.profile, 402 | ); 403 | } 404 | // Clear any saved off-CPU stack. 405 | thread.off_cpu_stack = Vec::new(); 406 | 407 | let cpu_delta = if self.have_context_switches { 408 | CpuDelta::from_nanos( 409 | self.context_switch_handler 410 | .consume_cpu_delta(&mut thread.context_switch_data), 411 | ) 412 | } else if let Some(period) = e.period { 413 | // If the observed perf event is one of the clock time events, or cycles, then we should convert it to a CpuDelta. 414 | // TODO: Detect event type 415 | CpuDelta::from_nanos(period) 416 | } else { 417 | CpuDelta::from_nanos(0) 418 | }; 419 | 420 | let frames = self.stack_converter.convert_stack(stack); 421 | self.profile 422 | .add_sample(thread_handle, profile_timestamp, frames, cpu_delta, 1); 423 | thread.last_sample_timestamp = Some(timestamp); 424 | } 425 | 426 | pub fn handle_sched_switch>( 427 | &mut self, 428 | e: SampleRecord, 429 | ) { 430 | let pid = e.pid.expect("Can't handle samples without pids"); 431 | let tid = e.tid.expect("Can't handle samples without tids"); 432 | let is_main = pid == tid; 433 | let process = self 434 | .processes 435 | .get_by_pid(pid, &mut self.profile, &self.kernel_modules); 436 | 437 | let mut stack = Vec::new(); 438 | Self::get_sample_stack::(&e, &process.unwinder, &mut self.cache, &mut stack); 439 | 440 | let stack = self 441 | .stack_converter 442 | .convert_stack_no_kernel(&stack) 443 | .collect(); 444 | 445 | let thread = 446 | self.threads 447 | .get_by_tid(tid, process.profile_process, is_main, &mut self.profile); 448 | thread.off_cpu_stack = stack; 449 | } 450 | 451 | /// Get the stack contained in this sample, and put it into `stack`. 452 | /// 453 | /// We can have both the kernel stack and the user stack, or just one of 454 | /// them, or neither. 455 | /// 456 | /// If this sample has a kernel stack, it's always in `e.callchain`. 457 | /// 458 | /// If this sample has a user stack, its source depends on the method of 459 | /// stackwalking that was requested during recording: 460 | /// 461 | /// - With frame pointer unwinding (the default on x86, `perf record -g`, 462 | /// or more explicitly `perf record --call-graph fp`), the user stack 463 | /// is walked during sampling by the kernel and appended to e.callchain. 464 | /// - With DWARF unwinding (`perf record --call-graph dwarf`), the raw 465 | /// bytes on the stack are just copied into the perf.data file, and we 466 | /// need to do the unwinding now, based on the register values in 467 | /// `e.user_regs` and the raw stack bytes in `e.user_stack`. 468 | fn get_sample_stack>( 469 | e: &SampleRecord, 470 | unwinder: &U, 471 | cache: &mut U::Cache, 472 | stack: &mut Vec, 473 | ) { 474 | stack.truncate(0); 475 | 476 | // CpuMode::from_misc(e.raw.misc) 477 | 478 | // Get the first fragment of the stack from e.callchain. 479 | if let Some(callchain) = e.callchain { 480 | let mut is_first_frame = true; 481 | let mut mode = StackMode::from(e.cpu_mode); 482 | for i in 0..callchain.len() { 483 | let address = callchain.get(i).unwrap(); 484 | if address >= PERF_CONTEXT_MAX { 485 | if let Some(new_mode) = StackMode::from_context_frame(address) { 486 | mode = new_mode; 487 | } 488 | continue; 489 | } 490 | 491 | let stack_frame = match is_first_frame { 492 | true => StackFrame::InstructionPointer(address, mode), 493 | false => StackFrame::ReturnAddress(address, mode), 494 | }; 495 | stack.push(stack_frame); 496 | 497 | is_first_frame = false; 498 | } 499 | } 500 | 501 | // Append the user stack with the help of DWARF unwinding. 502 | if let (Some(regs), Some((user_stack, _))) = (&e.user_regs, e.user_stack) { 503 | let ustack_bytes = RawDataU64::from_raw_data::(user_stack); 504 | let (pc, sp, regs) = C::convert_regs(regs); 505 | let mut read_stack = |addr: u64| { 506 | // ustack_bytes has the stack bytes starting from the current stack pointer. 507 | let offset = addr.checked_sub(sp).ok_or(())?; 508 | let index = usize::try_from(offset / 8).map_err(|_| ())?; 509 | ustack_bytes.get(index).ok_or(()) 510 | }; 511 | 512 | // Unwind. 513 | let mut frames = unwinder.iter_frames(pc, regs, cache, &mut read_stack); 514 | loop { 515 | let frame = match frames.next() { 516 | Ok(Some(frame)) => frame, 517 | Ok(None) => break, 518 | Err(_) => { 519 | stack.push(StackFrame::TruncatedStackMarker); 520 | break; 521 | } 522 | }; 523 | let stack_frame = match frame { 524 | FrameAddress::InstructionPointer(addr) => { 525 | StackFrame::InstructionPointer(addr, StackMode::User) 526 | } 527 | FrameAddress::ReturnAddress(addr) => { 528 | StackFrame::ReturnAddress(addr.into(), StackMode::User) 529 | } 530 | }; 531 | stack.push(stack_frame); 532 | } 533 | } 534 | 535 | if stack.is_empty() { 536 | if let Some(ip) = e.ip { 537 | stack.push(StackFrame::InstructionPointer(ip, e.cpu_mode.into())); 538 | } 539 | } 540 | } 541 | 542 | pub fn handle_mmap(&mut self, e: MmapRecord) { 543 | if !e.is_executable { 544 | return; 545 | } 546 | 547 | let mut path = e.path.as_slice(); 548 | let dso_key = match DsoKey::detect(&path, e.cpu_mode) { 549 | Some(dso_key) => dso_key, 550 | None => return, 551 | }; 552 | let mut build_id = None; 553 | if let Some(dso_info) = self.build_ids.get(&dso_key) { 554 | build_id = Some(&dso_info.build_id[..]); 555 | // Overwrite the path from the mmap record with the path from the build ID info. 556 | // These paths are usually the same, but in some cases the path from the build 557 | // ID info can be "better". For example, the synthesized mmap event for the 558 | // kernel vmlinux image usually has "[kernel.kallsyms]_text" whereas the build 559 | // ID info might have the full path to a kernel debug file, e.g. 560 | // "/usr/lib/debug/boot/vmlinux-4.16.0-1-amd64". 561 | path = Cow::Borrowed(&dso_info.path); 562 | } 563 | 564 | if e.pid == -1 { 565 | let debug_id = build_id.map(|id| DebugId::from_identifier(id, self.little_endian)); 566 | let path = std::str::from_utf8(&path).unwrap().to_string(); 567 | let mut debug_path = path.clone(); 568 | if debug_path.starts_with("[kernel.kallsyms]") { 569 | if let Some(linux_version) = self.linux_version.as_deref() { 570 | // Take a guess at the vmlinux debug file path. 571 | debug_path = format!("/usr/lib/debug/boot/vmlinux-{}", linux_version); 572 | } 573 | } 574 | 575 | self.kernel_modules.push(LibraryInfo { 576 | base_avma: e.address, 577 | avma_range: e.address..(e.address + e.length), 578 | debug_id: debug_id.unwrap_or_default(), 579 | path, 580 | debug_path, 581 | code_id: build_id.map(CodeId::from_binary), 582 | name: dso_key.name().to_string(), 583 | debug_name: dso_key.name().to_string(), 584 | arch: None, 585 | }); 586 | } else { 587 | let process = self 588 | .processes 589 | .get_by_pid(e.pid, &mut self.profile, &self.kernel_modules); 590 | if let Some(lib) = add_module_to_unwinder( 591 | &mut process.unwinder, 592 | &path, 593 | e.page_offset, 594 | e.address, 595 | e.length, 596 | build_id, 597 | self.extra_binary_artifact_dir.as_deref(), 598 | ) { 599 | self.profile.add_lib(process.profile_process, lib); 600 | } 601 | } 602 | } 603 | 604 | pub fn handle_mmap2(&mut self, e: Mmap2Record) { 605 | const PROT_EXEC: u32 = 0b100; 606 | if e.protection & PROT_EXEC == 0 { 607 | // Ignore non-executable mappings. 608 | return; 609 | } 610 | 611 | let path = e.path.as_slice(); 612 | let build_id = match &e.file_id { 613 | Mmap2FileId::BuildId(build_id) => Some(&build_id[..]), 614 | Mmap2FileId::InodeAndVersion(_) => { 615 | let dso_key = match DsoKey::detect(&path, e.cpu_mode) { 616 | Some(dso_key) => dso_key, 617 | None => return, 618 | }; 619 | self.build_ids.get(&dso_key).map(|db| &db.build_id[..]) 620 | } 621 | }; 622 | 623 | let process = self 624 | .processes 625 | .get_by_pid(e.pid, &mut self.profile, &self.kernel_modules); 626 | if let Some(lib) = add_module_to_unwinder( 627 | &mut process.unwinder, 628 | &path, 629 | e.page_offset, 630 | e.address, 631 | e.length, 632 | build_id, 633 | self.extra_binary_artifact_dir.as_deref(), 634 | ) { 635 | self.profile.add_lib(process.profile_process, lib); 636 | } 637 | } 638 | 639 | pub fn handle_context_switch(&mut self, e: ContextSwitchRecord, common: CommonData) { 640 | let pid = common.pid.expect("Can't handle samples without pids"); 641 | let tid = common.tid.expect("Can't handle samples without tids"); 642 | let timestamp = common 643 | .timestamp 644 | .expect("Can't handle context switch without time"); 645 | let is_main = pid == tid; 646 | let process = self 647 | .processes 648 | .get_by_pid(pid, &mut self.profile, &self.kernel_modules); 649 | let process_handle = process.profile_process; 650 | let thread = self 651 | .threads 652 | .get_by_tid(tid, process_handle, is_main, &mut self.profile); 653 | 654 | match e { 655 | ContextSwitchRecord::In { .. } => { 656 | let off_cpu_sample = self 657 | .context_switch_handler 658 | .handle_switch_in(timestamp, &mut thread.context_switch_data); 659 | if let Some(off_cpu_sample) = off_cpu_sample { 660 | let cpu_delta_ns = self 661 | .context_switch_handler 662 | .consume_cpu_delta(&mut thread.context_switch_data); 663 | process_off_cpu_sample_group( 664 | off_cpu_sample, 665 | thread.profile_thread, 666 | cpu_delta_ns, 667 | &self.timestamp_converter, 668 | self.off_cpu_weight_per_sample, 669 | &thread.off_cpu_stack, 670 | &mut self.profile, 671 | ); 672 | } 673 | // Clear the saved off-CPU stack. 674 | thread.off_cpu_stack = Vec::new(); 675 | } 676 | ContextSwitchRecord::Out { .. } => { 677 | self.context_switch_handler 678 | .handle_switch_out(timestamp, &mut thread.context_switch_data); 679 | } 680 | } 681 | } 682 | 683 | pub fn handle_thread_start(&mut self, e: ForkOrExitRecord) { 684 | let is_main = e.pid == e.tid; 685 | let start_time = self.timestamp_converter.convert_time(e.timestamp); 686 | let process = self 687 | .processes 688 | .get_by_pid(e.pid, &mut self.profile, &self.kernel_modules); 689 | let process_handle = process.profile_process; 690 | if is_main { 691 | self.profile 692 | .set_process_start_time(process_handle, start_time); 693 | } 694 | let thread = self 695 | .threads 696 | .get_by_tid(e.tid, process_handle, is_main, &mut self.profile); 697 | let thread_handle = thread.profile_thread; 698 | self.profile 699 | .set_thread_start_time(thread_handle, start_time); 700 | } 701 | 702 | pub fn handle_thread_end(&mut self, e: ForkOrExitRecord) { 703 | let is_main = e.pid == e.tid; 704 | let end_time = self.timestamp_converter.convert_time(e.timestamp); 705 | let process = self 706 | .processes 707 | .get_by_pid(e.pid, &mut self.profile, &self.kernel_modules); 708 | let process_handle = process.profile_process; 709 | let thread = self 710 | .threads 711 | .get_by_tid(e.tid, process_handle, is_main, &mut self.profile); 712 | let thread_handle = thread.profile_thread; 713 | self.profile.set_thread_end_time(thread_handle, end_time); 714 | self.threads.0.remove(&e.tid); 715 | if is_main { 716 | self.profile.set_process_end_time(process_handle, end_time); 717 | self.processes.0.remove(&e.pid); 718 | } 719 | } 720 | 721 | pub fn handle_thread_name_update(&mut self, e: CommOrExecRecord, timestamp: Option) { 722 | let is_main = e.pid == e.tid; 723 | if e.is_execve { 724 | // Mark the old thread / process as ended. 725 | // If the COMM record doesn't have a timestamp, take the last seen 726 | // timestamp from the previous sample. 727 | let timestamp = match timestamp { 728 | Some(0) | None => self.current_sample_time, 729 | Some(ts) => ts, 730 | }; 731 | let time = self.timestamp_converter.convert_time(timestamp); 732 | if let Some(t) = self.threads.0.get(&e.tid) { 733 | self.profile.set_thread_end_time(t.profile_thread, time); 734 | self.threads.0.remove(&e.tid); 735 | } 736 | if is_main { 737 | if let Some(p) = self.processes.0.get(&e.pid) { 738 | self.profile.set_process_end_time(p.profile_process, time); 739 | self.processes.0.remove(&e.pid); 740 | } 741 | } 742 | } 743 | 744 | let process_handle = self 745 | .processes 746 | .get_by_pid(e.pid, &mut self.profile, &self.kernel_modules) 747 | .profile_process; 748 | 749 | let name = e.name.as_slice(); 750 | let name = String::from_utf8_lossy(&name); 751 | let thread = self 752 | .threads 753 | .get_by_tid(e.tid, process_handle, is_main, &mut self.profile); 754 | let thread_handle = thread.profile_thread; 755 | 756 | self.profile.set_thread_name(thread_handle, &name); 757 | if is_main { 758 | self.profile.set_process_name(process_handle, &name); 759 | } 760 | 761 | if e.is_execve { 762 | // Mark this as the start time of the new thread / process. 763 | let time = self 764 | .timestamp_converter 765 | .convert_time(self.current_sample_time); 766 | self.profile.set_thread_start_time(thread_handle, time); 767 | if is_main { 768 | self.profile.set_process_start_time(process_handle, time); 769 | } 770 | } 771 | 772 | if !self.have_product_name && name != "perf-exec" { 773 | let product = format!( 774 | "{} on {} (perf version {})", 775 | name, self.host, self.perf_version 776 | ); 777 | self.profile.set_product(&product); 778 | self.have_product_name = true; 779 | } 780 | } 781 | } 782 | 783 | struct TimestampConverter { 784 | reference_ns: u64, 785 | } 786 | 787 | impl TimestampConverter { 788 | pub fn with_reference_timestamp(reference_ns: u64) -> Self { 789 | Self { reference_ns } 790 | } 791 | 792 | pub fn convert_time(&self, ktime_ns: u64) -> Timestamp { 793 | Timestamp::from_nanos_since_reference(ktime_ns.saturating_sub(self.reference_ns)) 794 | } 795 | } 796 | 797 | fn process_off_cpu_sample_group( 798 | off_cpu_sample: OffCpuSampleGroup, 799 | thread_handle: ThreadHandle, 800 | cpu_delta_ns: u64, 801 | timestamp_converter: &TimestampConverter, 802 | off_cpu_weight_per_sample: i32, 803 | off_cpu_stack: &[(Frame, CategoryPairHandle)], 804 | profile: &mut Profile, 805 | ) { 806 | let OffCpuSampleGroup { 807 | begin_timestamp, 808 | end_timestamp, 809 | sample_count, 810 | } = off_cpu_sample; 811 | 812 | // Add a sample at the beginning of the paused range. 813 | // This "first sample" will carry any leftover accumulated running time ("cpu delta"). 814 | let cpu_delta = CpuDelta::from_nanos(cpu_delta_ns); 815 | let weight = off_cpu_weight_per_sample; 816 | let frames = off_cpu_stack.iter().cloned(); 817 | let profile_timestamp = timestamp_converter.convert_time(begin_timestamp); 818 | profile.add_sample(thread_handle, profile_timestamp, frames, cpu_delta, weight); 819 | 820 | if sample_count > 1 { 821 | // Emit a "rest sample" with a CPU delta of zero covering the rest of the paused range. 822 | let cpu_delta = CpuDelta::from_nanos(0); 823 | let weight = i32::try_from(sample_count - 1).unwrap_or(0) * off_cpu_weight_per_sample; 824 | let frames = off_cpu_stack.iter().cloned(); 825 | let profile_timestamp = timestamp_converter.convert_time(end_timestamp); 826 | profile.add_sample(thread_handle, profile_timestamp, frames, cpu_delta, weight); 827 | } 828 | } 829 | 830 | #[derive(Debug, Clone, Copy)] 831 | struct StackConverter { 832 | user_category: CategoryPairHandle, 833 | kernel_category: CategoryPairHandle, 834 | } 835 | 836 | impl StackConverter { 837 | fn convert_stack( 838 | &self, 839 | stack: Vec, 840 | ) -> impl Iterator { 841 | let user_category = self.user_category; 842 | let kernel_category = self.kernel_category; 843 | stack.into_iter().rev().filter_map(move |frame| { 844 | let (location, mode) = match frame { 845 | StackFrame::InstructionPointer(addr, mode) => { 846 | (Frame::InstructionPointer(addr), mode) 847 | } 848 | StackFrame::ReturnAddress(addr, mode) => (Frame::ReturnAddress(addr), mode), 849 | StackFrame::TruncatedStackMarker => return None, 850 | }; 851 | let category = match mode { 852 | StackMode::User => user_category, 853 | StackMode::Kernel => kernel_category, 854 | }; 855 | Some((location, category)) 856 | }) 857 | } 858 | 859 | fn convert_stack_no_kernel<'a>( 860 | &self, 861 | stack: &'a [StackFrame], 862 | ) -> impl Iterator + 'a { 863 | let user_category = self.user_category; 864 | stack.iter().rev().filter_map(move |frame| { 865 | let (location, mode) = match *frame { 866 | StackFrame::InstructionPointer(addr, mode) => { 867 | (Frame::InstructionPointer(addr), mode) 868 | } 869 | StackFrame::ReturnAddress(addr, mode) => (Frame::ReturnAddress(addr), mode), 870 | StackFrame::TruncatedStackMarker => return None, 871 | }; 872 | match mode { 873 | StackMode::User => Some((location, user_category)), 874 | StackMode::Kernel => None, 875 | } 876 | }) 877 | } 878 | } 879 | 880 | struct Processes(HashMap>) 881 | where 882 | U: Unwinder>> + Default; 883 | 884 | impl Processes 885 | where 886 | U: Unwinder>> + Default, 887 | { 888 | pub fn get_by_pid( 889 | &mut self, 890 | pid: i32, 891 | profile: &mut Profile, 892 | global_modules: &[LibraryInfo], 893 | ) -> &mut Process { 894 | self.0.entry(pid).or_insert_with(|| { 895 | let name = format!("<{}>", pid); 896 | let handle = profile.add_process( 897 | &name, 898 | pid as u32, 899 | Timestamp::from_millis_since_reference(0.0), 900 | ); 901 | for module in global_modules.iter().cloned() { 902 | profile.add_lib(handle, module); 903 | } 904 | Process { 905 | profile_process: handle, 906 | unwinder: U::default(), 907 | } 908 | }) 909 | } 910 | } 911 | 912 | struct Threads(HashMap); 913 | 914 | impl Threads { 915 | pub fn get_by_tid( 916 | &mut self, 917 | tid: i32, 918 | process_handle: ProcessHandle, 919 | is_main: bool, 920 | profile: &mut Profile, 921 | ) -> &mut Thread { 922 | self.0.entry(tid).or_insert_with(|| { 923 | let profile_thread = profile.add_thread( 924 | process_handle, 925 | tid as u32, 926 | Timestamp::from_millis_since_reference(0.0), 927 | is_main, 928 | ); 929 | Thread { 930 | profile_thread, 931 | context_switch_data: Default::default(), 932 | last_sample_timestamp: None, 933 | off_cpu_stack: Vec::new(), 934 | } 935 | }) 936 | } 937 | } 938 | 939 | struct Thread { 940 | profile_thread: ThreadHandle, 941 | context_switch_data: ThreadContextSwitchData, 942 | last_sample_timestamp: Option, 943 | off_cpu_stack: Vec<(Frame, CategoryPairHandle)>, 944 | } 945 | 946 | struct Process { 947 | pub profile_process: ProcessHandle, 948 | pub unwinder: U, 949 | } 950 | 951 | #[derive(Clone, Debug)] 952 | pub enum StackFrame { 953 | InstructionPointer(u64, StackMode), 954 | ReturnAddress(u64, StackMode), 955 | TruncatedStackMarker, 956 | } 957 | 958 | #[derive(Debug, Clone, Copy)] 959 | pub enum StackMode { 960 | User, 961 | Kernel, 962 | } 963 | 964 | impl StackMode { 965 | /// Detect stack mode from a "context frame". 966 | /// 967 | /// Context frames are present in sample callchains; they're u64 addresses 968 | /// which are `>= PERF_CONTEXT_MAX`. 969 | pub fn from_context_frame(frame: u64) -> Option { 970 | match frame { 971 | PERF_CONTEXT_KERNEL | PERF_CONTEXT_GUEST_KERNEL => Some(Self::Kernel), 972 | PERF_CONTEXT_USER | PERF_CONTEXT_GUEST | PERF_CONTEXT_GUEST_USER => Some(Self::User), 973 | _ => None, 974 | } 975 | } 976 | } 977 | 978 | impl From for StackMode { 979 | /// Convert CpuMode into StackMode. 980 | fn from(cpu_mode: CpuMode) -> Self { 981 | match cpu_mode { 982 | CpuMode::Kernel | CpuMode::GuestKernel => Self::Kernel, 983 | _ => Self::User, 984 | } 985 | } 986 | } 987 | 988 | fn open_file_with_fallback( 989 | path: &Path, 990 | extra_dir: Option<&Path>, 991 | ) -> std::io::Result { 992 | match (std::fs::File::open(path), extra_dir, path.file_name()) { 993 | (Err(_), Some(extra_dir), Some(filename)) => { 994 | let p: PathBuf = [extra_dir, Path::new(filename)].iter().collect(); 995 | std::fs::File::open(&p) 996 | } 997 | (result, _, _) => result, 998 | } 999 | } 1000 | 1001 | fn compute_image_bias<'data: 'file, 'file>( 1002 | file: &'file impl Object<'data, 'file>, 1003 | mapping_start_file_offset: u64, 1004 | mapping_start_avma: u64, 1005 | mapping_size: u64, 1006 | ) -> Option { 1007 | let mapping_end_file_offset = mapping_start_file_offset + mapping_size; 1008 | 1009 | // Find one of the text sections in this mapping, to map file offsets to SVMAs. 1010 | // It would make more sense look for to ELF LOAD commands (which the `object` 1011 | // crate exposes as segments), but this does not work for the synthetic .so files 1012 | // created by `perf inject --jit` - those don't have LOAD commands. 1013 | let (section_start_file_offset, section_start_svma) = match file 1014 | .sections() 1015 | .filter(|s| s.kind() == SectionKind::Text) 1016 | .find_map(|s| match s.file_range() { 1017 | Some((section_start_file_offset, section_size)) => { 1018 | let section_end_file_offset = section_start_file_offset + section_size; 1019 | if mapping_start_file_offset <= section_start_file_offset 1020 | && section_end_file_offset <= mapping_end_file_offset 1021 | { 1022 | Some((section_start_file_offset, s.address())) 1023 | } else { 1024 | None 1025 | } 1026 | } 1027 | _ => None, 1028 | }) { 1029 | Some(section_info) => section_info, 1030 | None => { 1031 | println!( 1032 | "Could not find section covering file offset range 0x{:x}..0x{:x}", 1033 | mapping_start_file_offset, mapping_end_file_offset 1034 | ); 1035 | return None; 1036 | } 1037 | }; 1038 | 1039 | let section_start_avma = 1040 | mapping_start_avma + (section_start_file_offset - mapping_start_file_offset); 1041 | 1042 | // Compute the offset between AVMAs and SVMAs. This is the bias of the image. 1043 | Some(section_start_avma - section_start_svma) 1044 | } 1045 | 1046 | /// Tell the unwinder about this module, and alsos create a ProfileModule 1047 | /// so that the profile can be told about this module. 1048 | /// 1049 | /// The unwinder needs to know about it in case we need to do DWARF stack 1050 | /// unwinding - it needs to get the unwinding information from the binary. 1051 | /// The profile needs to know about this module so that it can assign 1052 | /// addresses in the stack to the right module and so that symbolication 1053 | /// knows where to get symbols for this module. 1054 | fn add_module_to_unwinder( 1055 | unwinder: &mut U, 1056 | path_slice: &[u8], 1057 | mapping_start_file_offset: u64, 1058 | mapping_start_avma: u64, 1059 | mapping_size: u64, 1060 | build_id: Option<&[u8]>, 1061 | extra_binary_artifact_dir: Option<&Path>, 1062 | ) -> Option 1063 | where 1064 | U: Unwinder>>, 1065 | { 1066 | let path = std::str::from_utf8(path_slice).unwrap(); 1067 | let objpath = Path::new(path); 1068 | 1069 | let file = open_file_with_fallback(objpath, extra_binary_artifact_dir).ok(); 1070 | if file.is_none() && !path.starts_with('[') { 1071 | // eprintln!("Could not open file {:?}", objpath); 1072 | } 1073 | 1074 | let mapping_end_avma = mapping_start_avma + mapping_size; 1075 | let avma_range = mapping_start_avma..mapping_end_avma; 1076 | 1077 | let code_id; 1078 | let debug_id; 1079 | let base_avma; 1080 | 1081 | if let Some(file) = file { 1082 | let mmap = match unsafe { memmap2::MmapOptions::new().map(&file) } { 1083 | Ok(mmap) => mmap, 1084 | Err(err) => { 1085 | eprintln!("Could not mmap file {}: {:?}", path, err); 1086 | return None; 1087 | } 1088 | }; 1089 | 1090 | fn section_data<'a>(section: &impl ObjectSection<'a>) -> Option> { 1091 | section.data().ok().map(|data| data.to_owned()) 1092 | } 1093 | 1094 | let file = match object::File::parse(&mmap[..]) { 1095 | Ok(file) => file, 1096 | Err(_) => { 1097 | eprintln!("File {:?} has unrecognized format", objpath); 1098 | return None; 1099 | } 1100 | }; 1101 | 1102 | // Verify build ID. 1103 | if let Some(build_id) = build_id { 1104 | match file.build_id().ok().flatten() { 1105 | Some(file_build_id) if build_id == file_build_id => { 1106 | // Build IDs match. Good. 1107 | } 1108 | Some(file_build_id) => { 1109 | let file_build_id = CodeId::from_binary(file_build_id); 1110 | let expected_build_id = CodeId::from_binary(build_id); 1111 | eprintln!( 1112 | "File {:?} has non-matching build ID {} (expected {})", 1113 | objpath, file_build_id, expected_build_id 1114 | ); 1115 | return None; 1116 | } 1117 | None => { 1118 | eprintln!( 1119 | "File {:?} does not contain a build ID, but we expected it to have one", 1120 | objpath 1121 | ); 1122 | return None; 1123 | } 1124 | } 1125 | } 1126 | 1127 | // Compute the AVMA that maps to SVMA zero. This is also called the "bias" of the 1128 | // image. On ELF it is also the image load address. 1129 | let base_svma = 0; 1130 | base_avma = compute_image_bias( 1131 | &file, 1132 | mapping_start_file_offset, 1133 | mapping_start_avma, 1134 | mapping_size, 1135 | )?; 1136 | 1137 | let text = file.section_by_name(".text"); 1138 | let text_env = file.section_by_name("text_env"); 1139 | let eh_frame = file.section_by_name(".eh_frame"); 1140 | let got = file.section_by_name(".got"); 1141 | let eh_frame_hdr = file.section_by_name(".eh_frame_hdr"); 1142 | 1143 | let unwind_data = match ( 1144 | eh_frame.as_ref().and_then(section_data), 1145 | eh_frame_hdr.as_ref().and_then(section_data), 1146 | ) { 1147 | (Some(eh_frame), Some(eh_frame_hdr)) => { 1148 | ModuleUnwindData::EhFrameHdrAndEhFrame(eh_frame_hdr, eh_frame) 1149 | } 1150 | (Some(eh_frame), None) => ModuleUnwindData::EhFrame(eh_frame), 1151 | (None, _) => ModuleUnwindData::None, 1152 | }; 1153 | 1154 | let text_data = if let Some(text_segment) = file 1155 | .segments() 1156 | .find(|segment| segment.name_bytes() == Ok(Some(b"__TEXT"))) 1157 | { 1158 | let (start, size) = text_segment.file_range(); 1159 | let address_range = base_avma + start..base_avma + start + size; 1160 | text_segment 1161 | .data() 1162 | .ok() 1163 | .map(|data| TextByteData::new(data.to_owned(), address_range)) 1164 | } else if let Some(text_section) = &text { 1165 | if let Some((start, size)) = text_section.file_range() { 1166 | let address_range = base_avma + start..base_avma + start + size; 1167 | text_section 1168 | .data() 1169 | .ok() 1170 | .map(|data| TextByteData::new(data.to_owned(), address_range)) 1171 | } else { 1172 | None 1173 | } 1174 | } else { 1175 | None 1176 | }; 1177 | 1178 | fn svma_range<'a>(section: &impl ObjectSection<'a>) -> Range { 1179 | section.address()..section.address() + section.size() 1180 | } 1181 | 1182 | let module = Module::new( 1183 | path.to_string(), 1184 | avma_range.clone(), 1185 | base_avma, 1186 | ModuleSvmaInfo { 1187 | base_svma, 1188 | text: text.as_ref().map(svma_range), 1189 | text_env: text_env.as_ref().map(svma_range), 1190 | stubs: None, 1191 | stub_helper: None, 1192 | eh_frame: eh_frame.as_ref().map(svma_range), 1193 | eh_frame_hdr: eh_frame_hdr.as_ref().map(svma_range), 1194 | got: got.as_ref().map(svma_range), 1195 | }, 1196 | unwind_data, 1197 | text_data, 1198 | ); 1199 | unwinder.add_module(module); 1200 | 1201 | debug_id = debug_id_for_object(&file)?; 1202 | code_id = file.build_id().ok().flatten().map(CodeId::from_binary); 1203 | } else { 1204 | // Without access to the binary file, make some guesses. We can't really 1205 | // know what the right base address is because we don't have the section 1206 | // information which lets us map between addresses and file offsets, but 1207 | // often svmas and file offsets are the same, so this is a reasonable guess. 1208 | base_avma = mapping_start_avma - mapping_start_file_offset; 1209 | 1210 | // If we have a build ID, convert it to a debug_id and a code_id. 1211 | debug_id = build_id 1212 | .map(|id| DebugId::from_identifier(id, true)) // TODO: endian 1213 | .unwrap_or_default(); 1214 | code_id = build_id.map(CodeId::from_binary); 1215 | } 1216 | 1217 | let name = objpath 1218 | .file_name() 1219 | .map_or("".into(), |f| f.to_string_lossy().to_string()); 1220 | Some(LibraryInfo { 1221 | base_avma, 1222 | avma_range, 1223 | debug_id, 1224 | code_id, 1225 | path: path.to_string(), 1226 | debug_path: path.to_string(), 1227 | debug_name: name.clone(), 1228 | name, 1229 | arch: None, 1230 | }) 1231 | } 1232 | --------------------------------------------------------------------------------