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