1#!/usr/bin/env python3 2# 3# Copyright (C) 2021 The Android Open Source Project 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 18"""gecko_profile_generator.py: converts perf.data to Gecko Profile Format, 19 which can be read by https://profiler.firefox.com/. 20 21 Example: 22 ./app_profiler.py 23 ./gecko_profile_generator.py | gzip > gecko-profile.json.gz 24 25 Then open gecko-profile.json.gz in https://profiler.firefox.com/ 26""" 27 28import json 29import sys 30 31from dataclasses import dataclass, field 32from simpleperf_report_lib import ReportLib 33from simpleperf_utils import BaseArgumentParser, flatten_arg_list, ReportLibOptions 34from typing import List, Dict, Optional, NamedTuple, Set, Tuple 35 36 37StringID = int 38StackID = int 39FrameID = int 40CategoryID = int 41Milliseconds = float 42GeckoProfile = Dict 43 44 45# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156 46class Frame(NamedTuple): 47 string_id: StringID 48 relevantForJS: bool 49 innerWindowID: int 50 implementation: None 51 optimizations: None 52 line: None 53 column: None 54 category: CategoryID 55 subcategory: int 56 57 58# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216 59class Stack(NamedTuple): 60 prefix_id: Optional[StackID] 61 frame_id: FrameID 62 category_id: CategoryID 63 64 65# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90 66class Sample(NamedTuple): 67 stack_id: Optional[StackID] 68 time_ms: Milliseconds 69 responsiveness: int 70 71 72# Schema: https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/profile.js#L425 73# Colors must be defined in: 74# https://github.com/firefox-devtools/profiler/blob/50124adbfa488adba6e2674a8f2618cf34b59cd2/res/css/categories.css 75CATEGORIES = [ 76 { 77 "name": 'User', 78 # Follow Brendan Gregg's Flamegraph convention: yellow for userland 79 # https://github.com/brendangregg/FlameGraph/blob/810687f180f3c4929b5d965f54817a5218c9d89b/flamegraph.pl#L419 80 "color": 'yellow', 81 "subcategories": ['Other'] 82 }, 83 { 84 "name": 'Kernel', 85 # Follow Brendan Gregg's Flamegraph convention: orange for kernel 86 # https://github.com/brendangregg/FlameGraph/blob/810687f180f3c4929b5d965f54817a5218c9d89b/flamegraph.pl#L417 87 "color": 'orange', 88 "subcategories": ['Other'] 89 }, 90 { 91 "name": 'Native', 92 # Follow Brendan Gregg's Flamegraph convention: yellow for userland 93 # https://github.com/brendangregg/FlameGraph/blob/810687f180f3c4929b5d965f54817a5218c9d89b/flamegraph.pl#L419 94 "color": 'yellow', 95 "subcategories": ['Other'] 96 }, 97 { 98 "name": 'DEX', 99 # Follow Brendan Gregg's Flamegraph convention: green for Java/JIT 100 # https://github.com/brendangregg/FlameGraph/blob/810687f180f3c4929b5d965f54817a5218c9d89b/flamegraph.pl#L411 101 "color": 'green', 102 "subcategories": ['Other'] 103 }, 104 { 105 "name": 'OAT', 106 # Follow Brendan Gregg's Flamegraph convention: green for Java/JIT 107 # https://github.com/brendangregg/FlameGraph/blob/810687f180f3c4929b5d965f54817a5218c9d89b/flamegraph.pl#L411 108 "color": 'green', 109 "subcategories": ['Other'] 110 }, 111 # Not used by this exporter yet, but some Firefox Profiler code assumes 112 # there is an 'Other' category by searching for a category with 113 # color=grey, so include this. 114 { 115 "name": 'Other', 116 "color": 'grey', 117 "subcategories": ['Other'] 118 }, 119] 120 121 122@dataclass 123class Thread: 124 """A builder for a profile of a single thread. 125 126 Attributes: 127 comm: Thread command-line (name). 128 pid: process ID of containing process. 129 tid: thread ID. 130 samples: Timeline of profile samples. 131 frameTable: interned stack frame ID -> stack frame. 132 stringTable: interned string ID -> string. 133 stringMap: interned string -> string ID. 134 stackTable: interned stack ID -> stack. 135 stackMap: (stack prefix ID, leaf stack frame ID) -> interned Stack ID. 136 frameMap: Stack Frame string -> interned Frame ID. 137 """ 138 comm: str 139 pid: int 140 tid: int 141 samples: List[Sample] = field(default_factory=list) 142 frameTable: List[Frame] = field(default_factory=list) 143 stringTable: List[str] = field(default_factory=list) 144 # TODO: this is redundant with frameTable, could we remove this? 145 stringMap: Dict[str, int] = field(default_factory=dict) 146 stackTable: List[Stack] = field(default_factory=list) 147 stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict) 148 frameMap: Dict[str, int] = field(default_factory=dict) 149 150 def _intern_stack(self, frame_id: int, prefix_id: Optional[int]) -> int: 151 """Gets a matching stack, or saves the new stack. Returns a Stack ID.""" 152 key = (prefix_id, frame_id) 153 stack_id = self.stackMap.get(key) 154 if stack_id is not None: 155 return stack_id 156 stack_id = len(self.stackTable) 157 self.stackTable.append(Stack(prefix_id=prefix_id, 158 frame_id=frame_id, 159 category_id=0)) 160 self.stackMap[key] = stack_id 161 return stack_id 162 163 def _intern_string(self, string: str) -> int: 164 """Gets a matching string, or saves the new string. Returns a String ID.""" 165 string_id = self.stringMap.get(string) 166 if string_id is not None: 167 return string_id 168 string_id = len(self.stringTable) 169 self.stringTable.append(string) 170 self.stringMap[string] = string_id 171 return string_id 172 173 def _intern_frame(self, frame_str: str) -> int: 174 """Gets a matching stack frame, or saves the new frame. Returns a Frame ID.""" 175 frame_id = self.frameMap.get(frame_str) 176 if frame_id is not None: 177 return frame_id 178 frame_id = len(self.frameTable) 179 self.frameMap[frame_str] = frame_id 180 string_id = self._intern_string(frame_str) 181 182 category = 0 183 # Heuristic: kernel code contains "kallsyms" as the library name. 184 if "kallsyms" in frame_str or ".ko" in frame_str: 185 category = 1 186 elif ".so" in frame_str: 187 category = 2 188 elif ".vdex" in frame_str: 189 category = 3 190 elif ".oat" in frame_str: 191 category = 4 192 193 self.frameTable.append(Frame( 194 string_id=string_id, 195 relevantForJS=False, 196 innerWindowID=0, 197 implementation=None, 198 optimizations=None, 199 line=None, 200 column=None, 201 category=category, 202 subcategory=0, 203 )) 204 return frame_id 205 206 def _add_sample(self, comm: str, stack: List[str], time_ms: Milliseconds) -> None: 207 """Add a timestamped stack trace sample to the thread builder. 208 209 Args: 210 comm: command-line (name) of the thread at this sample 211 stack: sampled stack frames. Root first, leaf last. 212 time_ms: timestamp of sample in milliseconds 213 """ 214 # Unix threads often don't set their name immediately upon creation. 215 # Use the last name 216 if self.comm != comm: 217 self.comm = comm 218 219 prefix_stack_id = None 220 for frame in stack: 221 frame_id = self._intern_frame(frame) 222 prefix_stack_id = self._intern_stack(frame_id, prefix_stack_id) 223 224 self.samples.append(Sample(stack_id=prefix_stack_id, 225 time_ms=time_ms, 226 responsiveness=0)) 227 228 def _to_json_dict(self) -> Dict: 229 """Converts this Thread to GeckoThread JSON format.""" 230 # The samples aren't guaranteed to be in order. Sort them by time. 231 self.samples.sort(key=lambda s: s.time_ms) 232 233 # Gecko profile format is row-oriented data as List[List], 234 # And a schema for interpreting each index. 235 # Schema: 236 # https://github.com/firefox-devtools/profiler/blob/main/docs-developer/gecko-profile-format.md 237 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L230 238 return { 239 "tid": self.tid, 240 "pid": self.pid, 241 "name": self.comm, 242 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L51 243 "markers": { 244 "schema": { 245 "name": 0, 246 "startTime": 1, 247 "endTime": 2, 248 "phase": 3, 249 "category": 4, 250 "data": 5, 251 }, 252 "data": [], 253 }, 254 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90 255 "samples": { 256 "schema": { 257 "stack": 0, 258 "time": 1, 259 "responsiveness": 2, 260 }, 261 "data": self.samples 262 }, 263 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156 264 "frameTable": { 265 "schema": { 266 "location": 0, 267 "relevantForJS": 1, 268 "innerWindowID": 2, 269 "implementation": 3, 270 "optimizations": 4, 271 "line": 5, 272 "column": 6, 273 "category": 7, 274 "subcategory": 8, 275 }, 276 "data": self.frameTable, 277 }, 278 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216 279 "stackTable": { 280 "schema": { 281 "prefix": 0, 282 "frame": 1, 283 "category": 2, 284 }, 285 "data": self.stackTable, 286 }, 287 "stringTable": self.stringTable, 288 "registerTime": 0, 289 "unregisterTime": None, 290 "processType": "default", 291 } 292 293 294def _gecko_profile( 295 record_file: str, 296 symfs_dir: Optional[str], 297 kallsyms_file: Optional[str], 298 report_lib_options: ReportLibOptions) -> GeckoProfile: 299 """convert a simpleperf profile to gecko format""" 300 lib = ReportLib() 301 302 lib.ShowIpForUnknownSymbol() 303 if symfs_dir is not None: 304 lib.SetSymfs(symfs_dir) 305 lib.SetRecordFile(record_file) 306 if kallsyms_file is not None: 307 lib.SetKallsymsFile(kallsyms_file) 308 lib.SetReportOptions(report_lib_options) 309 310 arch = lib.GetArch() 311 meta_info = lib.MetaInfo() 312 record_cmd = lib.GetRecordCmd() 313 314 # Map from tid to Thread 315 threadMap: Dict[int, Thread] = {} 316 317 while True: 318 sample = lib.GetNextSample() 319 if sample is None: 320 lib.Close() 321 break 322 event = lib.GetEventOfCurrentSample() 323 symbol = lib.GetSymbolOfCurrentSample() 324 callchain = lib.GetCallChainOfCurrentSample() 325 sample_time_ms = sample.time / 1000000 326 327 stack = ['%s (in %s)' % (symbol.symbol_name, symbol.dso_name)] 328 for i in range(callchain.nr): 329 entry = callchain.entries[i] 330 stack.append('%s (in %s)' % (entry.symbol.symbol_name, entry.symbol.dso_name)) 331 # We want root first, leaf last. 332 stack.reverse() 333 334 # add thread sample 335 thread = threadMap.get(sample.tid) 336 if thread is None: 337 thread = Thread(comm=sample.thread_comm, pid=sample.pid, tid=sample.tid) 338 threadMap[sample.tid] = thread 339 thread._add_sample( 340 comm=sample.thread_comm, 341 stack=stack, 342 # We are being a bit fast and loose here with time here. simpleperf 343 # uses CLOCK_MONOTONIC by default, which doesn't use the normal unix 344 # epoch, but rather some arbitrary time. In practice, this doesn't 345 # matter, the Firefox Profiler normalises all the timestamps to begin at 346 # the minimum time. Consider fixing this in future, if needed, by 347 # setting `simpleperf record --clockid realtime`. 348 time_ms=sample_time_ms) 349 350 threads = [thread._to_json_dict() for thread in threadMap.values()] 351 352 profile_timestamp = meta_info.get('timestamp') 353 end_time_ms = (int(profile_timestamp) * 1000) if profile_timestamp else 0 354 355 # Schema: https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L305 356 gecko_profile_meta = { 357 "interval": 1, 358 "processType": 0, 359 "product": record_cmd, 360 "device": meta_info.get("product_props"), 361 "platform": meta_info.get("android_build_fingerprint"), 362 "stackwalk": 1, 363 "debug": 0, 364 "gcpoison": 0, 365 "asyncstack": 1, 366 # The profile timestamp is actually the end time, not the start time. 367 # This is close enough for our purposes; I mostly just want to know which 368 # day the profile was taken! Consider fixing this in future, if needed, 369 # by setting `simpleperf record --clockid realtime` and taking the minimum 370 # sample time. 371 "startTime": end_time_ms, 372 "shutdownTime": None, 373 "version": 24, 374 "presymbolicated": True, 375 "categories": CATEGORIES, 376 "markerSchema": [], 377 "abi": arch, 378 "oscpu": meta_info.get("android_build_fingerprint"), 379 } 380 381 # Schema: 382 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L377 383 # https://github.com/firefox-devtools/profiler/blob/main/docs-developer/gecko-profile-format.md 384 return { 385 "meta": gecko_profile_meta, 386 "libs": [], 387 "threads": threads, 388 "processes": [], 389 "pausedRanges": [], 390 } 391 392 393def main() -> None: 394 parser = BaseArgumentParser(description=__doc__) 395 parser.add_argument('--symfs', 396 help='Set the path to find binaries with symbols and debug info.') 397 parser.add_argument('--kallsyms', help='Set the path to find kernel symbols.') 398 parser.add_argument('-i', '--record_file', nargs='?', default='perf.data', 399 help='Default is perf.data.') 400 parser.add_report_lib_options() 401 args = parser.parse_args() 402 profile = _gecko_profile( 403 record_file=args.record_file, 404 symfs_dir=args.symfs, 405 kallsyms_file=args.kallsyms, 406 report_lib_options=args.report_lib_options) 407 408 json.dump(profile, sys.stdout, sort_keys=True) 409 410 411if __name__ == '__main__': 412 main() 413