1# firefox-gecko-converter.py - Convert perf record output to Firefox's gecko profile format 2# SPDX-License-Identifier: GPL-2.0 3# 4# The script converts perf.data to Gecko Profile Format, 5# which can be read by https://profiler.firefox.com/. 6# 7# Usage: 8# 9# perf record -a -g -F 99 sleep 60 10# perf script report gecko > output.json 11 12import os 13import sys 14import json 15import argparse 16from functools import reduce 17from dataclasses import dataclass, field 18from typing import List, Dict, Optional, NamedTuple, Set, Tuple, Any 19 20# Add the Perf-Trace-Util library to the Python path 21sys.path.append(os.environ['PERF_EXEC_PATH'] + \ 22 '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') 23 24from perf_trace_context import * 25from Core import * 26 27StringID = int 28StackID = int 29FrameID = int 30CategoryID = int 31Milliseconds = float 32 33# start_time is intialiazed only once for the all event traces. 34start_time = None 35 36# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/profile.js#L425 37# Follow Brendan Gregg's Flamegraph convention: orange for kernel and yellow for user space by default. 38CATEGORIES = None 39 40# The product name is used by the profiler UI to show the Operating system and Processor. 41PRODUCT = os.popen('uname -op').read().strip() 42 43# The category index is used by the profiler UI to show the color of the flame graph. 44USER_CATEGORY_INDEX = 0 45KERNEL_CATEGORY_INDEX = 1 46 47# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156 48class Frame(NamedTuple): 49 string_id: StringID 50 relevantForJS: bool 51 innerWindowID: int 52 implementation: None 53 optimizations: None 54 line: None 55 column: None 56 category: CategoryID 57 subcategory: int 58 59# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216 60class Stack(NamedTuple): 61 prefix_id: Optional[StackID] 62 frame_id: FrameID 63 64# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90 65class Sample(NamedTuple): 66 stack_id: Optional[StackID] 67 time_ms: Milliseconds 68 responsiveness: int 69 70@dataclass 71class Thread: 72 """A builder for a profile of the thread. 73 74 Attributes: 75 comm: Thread command-line (name). 76 pid: process ID of containing process. 77 tid: thread ID. 78 samples: Timeline of profile samples. 79 frameTable: interned stack frame ID -> stack frame. 80 stringTable: interned string ID -> string. 81 stringMap: interned string -> string ID. 82 stackTable: interned stack ID -> stack. 83 stackMap: (stack prefix ID, leaf stack frame ID) -> interned Stack ID. 84 frameMap: Stack Frame string -> interned Frame ID. 85 comm: str 86 pid: int 87 tid: int 88 samples: List[Sample] = field(default_factory=list) 89 frameTable: List[Frame] = field(default_factory=list) 90 stringTable: List[str] = field(default_factory=list) 91 stringMap: Dict[str, int] = field(default_factory=dict) 92 stackTable: List[Stack] = field(default_factory=list) 93 stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict) 94 frameMap: Dict[str, int] = field(default_factory=dict) 95 """ 96 comm: str 97 pid: int 98 tid: int 99 samples: List[Sample] = field(default_factory=list) 100 frameTable: List[Frame] = field(default_factory=list) 101 stringTable: List[str] = field(default_factory=list) 102 stringMap: Dict[str, int] = field(default_factory=dict) 103 stackTable: List[Stack] = field(default_factory=list) 104 stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict) 105 frameMap: Dict[str, int] = field(default_factory=dict) 106 107 def _intern_stack(self, frame_id: int, prefix_id: Optional[int]) -> int: 108 """Gets a matching stack, or saves the new stack. Returns a Stack ID.""" 109 key = f"{frame_id}" if prefix_id is None else f"{frame_id},{prefix_id}" 110 # key = (prefix_id, frame_id) 111 stack_id = self.stackMap.get(key) 112 if stack_id is None: 113 # return stack_id 114 stack_id = len(self.stackTable) 115 self.stackTable.append(Stack(prefix_id=prefix_id, frame_id=frame_id)) 116 self.stackMap[key] = stack_id 117 return stack_id 118 119 def _intern_string(self, string: str) -> int: 120 """Gets a matching string, or saves the new string. Returns a String ID.""" 121 string_id = self.stringMap.get(string) 122 if string_id is not None: 123 return string_id 124 string_id = len(self.stringTable) 125 self.stringTable.append(string) 126 self.stringMap[string] = string_id 127 return string_id 128 129 def _intern_frame(self, frame_str: str) -> int: 130 """Gets a matching stack frame, or saves the new frame. Returns a Frame ID.""" 131 frame_id = self.frameMap.get(frame_str) 132 if frame_id is not None: 133 return frame_id 134 frame_id = len(self.frameTable) 135 self.frameMap[frame_str] = frame_id 136 string_id = self._intern_string(frame_str) 137 138 symbol_name_to_category = KERNEL_CATEGORY_INDEX if frame_str.find('kallsyms') != -1 \ 139 or frame_str.find('/vmlinux') != -1 \ 140 or frame_str.endswith('.ko)') \ 141 else USER_CATEGORY_INDEX 142 143 self.frameTable.append(Frame( 144 string_id=string_id, 145 relevantForJS=False, 146 innerWindowID=0, 147 implementation=None, 148 optimizations=None, 149 line=None, 150 column=None, 151 category=symbol_name_to_category, 152 subcategory=None, 153 )) 154 return frame_id 155 156 def _to_json_dict(self) -> Dict: 157 """Converts current Thread to GeckoThread JSON format.""" 158 # Gecko profile format is row-oriented data as List[List], 159 # And a schema for interpreting each index. 160 # Schema: 161 # https://github.com/firefox-devtools/profiler/blob/main/docs-developer/gecko-profile-format.md 162 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L230 163 return { 164 "tid": self.tid, 165 "pid": self.pid, 166 "name": self.comm, 167 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L51 168 "markers": { 169 "schema": { 170 "name": 0, 171 "startTime": 1, 172 "endTime": 2, 173 "phase": 3, 174 "category": 4, 175 "data": 5, 176 }, 177 "data": [], 178 }, 179 180 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90 181 "samples": { 182 "schema": { 183 "stack": 0, 184 "time": 1, 185 "responsiveness": 2, 186 }, 187 "data": self.samples 188 }, 189 190 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156 191 "frameTable": { 192 "schema": { 193 "location": 0, 194 "relevantForJS": 1, 195 "innerWindowID": 2, 196 "implementation": 3, 197 "optimizations": 4, 198 "line": 5, 199 "column": 6, 200 "category": 7, 201 "subcategory": 8, 202 }, 203 "data": self.frameTable, 204 }, 205 206 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216 207 "stackTable": { 208 "schema": { 209 "prefix": 0, 210 "frame": 1, 211 }, 212 "data": self.stackTable, 213 }, 214 "stringTable": self.stringTable, 215 "registerTime": 0, 216 "unregisterTime": None, 217 "processType": "default", 218 } 219 220# Uses perf script python interface to parse each 221# event and store the data in the thread builder. 222def process_event(param_dict: Dict) -> None: 223 global start_time 224 global tid_to_thread 225 time_stamp = (param_dict['sample']['time'] // 1000) / 1000 226 pid = param_dict['sample']['pid'] 227 tid = param_dict['sample']['tid'] 228 comm = param_dict['comm'] 229 230 # Start time is the time of the first sample 231 if not start_time: 232 start_time = time_stamp 233 234# Trace_end runs at the end and will be used to aggregate 235# the data into the final json object and print it out to stdout. 236def trace_end() -> None: 237 # Schema: https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L305 238 gecko_profile_with_meta = { 239 "meta": { 240 "interval": 1, 241 "processType": 0, 242 "product": PRODUCT, 243 "stackwalk": 1, 244 "debug": 0, 245 "gcpoison": 0, 246 "asyncstack": 1, 247 "startTime": start_time, 248 "shutdownTime": None, 249 "version": 24, 250 "presymbolicated": True, 251 "categories": CATEGORIES, 252 "markerSchema": [], 253 }, 254 "libs": [], 255 # threads will be implemented in later commits. 256 # "threads": threads, 257 "processes": [], 258 "pausedRanges": [], 259 } 260 json.dump(gecko_profile_with_meta, sys.stdout, indent=2) 261 262def main() -> None: 263 global CATEGORIES 264 parser = argparse.ArgumentParser(description="Convert perf.data to Firefox\'s Gecko Profile format") 265 266 # Add the command-line options 267 # Colors must be defined according to this: 268 # https://github.com/firefox-devtools/profiler/blob/50124adbfa488adba6e2674a8f2618cf34b59cd2/res/css/categories.css 269 parser.add_argument('--user-color', default='yellow', help='Color for the User category') 270 parser.add_argument('--kernel-color', default='orange', help='Color for the Kernel category') 271 # Parse the command-line arguments 272 args = parser.parse_args() 273 # Access the values provided by the user 274 user_color = args.user_color 275 kernel_color = args.kernel_color 276 277 CATEGORIES = [ 278 { 279 "name": 'User', 280 "color": user_color, 281 "subcategories": ['Other'] 282 }, 283 { 284 "name": 'Kernel', 285 "color": kernel_color, 286 "subcategories": ['Other'] 287 }, 288 ] 289 290if __name__ == '__main__': 291 main() 292