11699d3efSAnup Sharma# firefox-gecko-converter.py - Convert perf record output to Firefox's gecko profile format 21699d3efSAnup Sharma# SPDX-License-Identifier: GPL-2.0 31699d3efSAnup Sharma# 41699d3efSAnup Sharma# The script converts perf.data to Gecko Profile Format, 51699d3efSAnup Sharma# which can be read by https://profiler.firefox.com/. 61699d3efSAnup Sharma# 71699d3efSAnup Sharma# Usage: 81699d3efSAnup Sharma# 91699d3efSAnup Sharma# perf record -a -g -F 99 sleep 60 101699d3efSAnup Sharma# perf script report gecko > output.json 111699d3efSAnup Sharma 121699d3efSAnup Sharmaimport os 131699d3efSAnup Sharmaimport sys 14833daec7SAnup Sharmaimport json 15833daec7SAnup Sharmaimport argparse 16*258dfd41SAnup Sharmafrom functools import reduce 175aacd7f0SAnup Sharmafrom dataclasses import dataclass, field 185aacd7f0SAnup Sharmafrom typing import List, Dict, Optional, NamedTuple, Set, Tuple, Any 191699d3efSAnup Sharma 201699d3efSAnup Sharma# Add the Perf-Trace-Util library to the Python path 211699d3efSAnup Sharmasys.path.append(os.environ['PERF_EXEC_PATH'] + \ 221699d3efSAnup Sharma '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') 231699d3efSAnup Sharma 241699d3efSAnup Sharmafrom perf_trace_context import * 251699d3efSAnup Sharmafrom Core import * 261699d3efSAnup Sharma 275aacd7f0SAnup SharmaStringID = int 285aacd7f0SAnup SharmaStackID = int 295aacd7f0SAnup SharmaFrameID = int 305aacd7f0SAnup SharmaCategoryID = int 315aacd7f0SAnup SharmaMilliseconds = float 325aacd7f0SAnup Sharma 330a02e44cSAnup Sharma# start_time is intialiazed only once for the all event traces. 340a02e44cSAnup Sharmastart_time = None 350a02e44cSAnup Sharma 36833daec7SAnup Sharma# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/profile.js#L425 37833daec7SAnup Sharma# Follow Brendan Gregg's Flamegraph convention: orange for kernel and yellow for user space by default. 38833daec7SAnup SharmaCATEGORIES = None 39833daec7SAnup Sharma 40833daec7SAnup Sharma# The product name is used by the profiler UI to show the Operating system and Processor. 41833daec7SAnup SharmaPRODUCT = os.popen('uname -op').read().strip() 42833daec7SAnup Sharma 43*258dfd41SAnup Sharma# The category index is used by the profiler UI to show the color of the flame graph. 44*258dfd41SAnup SharmaUSER_CATEGORY_INDEX = 0 45*258dfd41SAnup SharmaKERNEL_CATEGORY_INDEX = 1 46*258dfd41SAnup Sharma 475aacd7f0SAnup Sharma# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156 485aacd7f0SAnup Sharmaclass Frame(NamedTuple): 495aacd7f0SAnup Sharma string_id: StringID 505aacd7f0SAnup Sharma relevantForJS: bool 515aacd7f0SAnup Sharma innerWindowID: int 525aacd7f0SAnup Sharma implementation: None 535aacd7f0SAnup Sharma optimizations: None 545aacd7f0SAnup Sharma line: None 555aacd7f0SAnup Sharma column: None 565aacd7f0SAnup Sharma category: CategoryID 575aacd7f0SAnup Sharma subcategory: int 585aacd7f0SAnup Sharma 595aacd7f0SAnup Sharma# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216 605aacd7f0SAnup Sharmaclass Stack(NamedTuple): 615aacd7f0SAnup Sharma prefix_id: Optional[StackID] 625aacd7f0SAnup Sharma frame_id: FrameID 635aacd7f0SAnup Sharma 645aacd7f0SAnup Sharma# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90 655aacd7f0SAnup Sharmaclass Sample(NamedTuple): 665aacd7f0SAnup Sharma stack_id: Optional[StackID] 675aacd7f0SAnup Sharma time_ms: Milliseconds 685aacd7f0SAnup Sharma responsiveness: int 695aacd7f0SAnup Sharma 705aacd7f0SAnup Sharma@dataclass 715aacd7f0SAnup Sharmaclass Thread: 725aacd7f0SAnup Sharma """A builder for a profile of the thread. 735aacd7f0SAnup Sharma 745aacd7f0SAnup Sharma Attributes: 755aacd7f0SAnup Sharma comm: Thread command-line (name). 765aacd7f0SAnup Sharma pid: process ID of containing process. 775aacd7f0SAnup Sharma tid: thread ID. 785aacd7f0SAnup Sharma samples: Timeline of profile samples. 795aacd7f0SAnup Sharma frameTable: interned stack frame ID -> stack frame. 805aacd7f0SAnup Sharma stringTable: interned string ID -> string. 815aacd7f0SAnup Sharma stringMap: interned string -> string ID. 825aacd7f0SAnup Sharma stackTable: interned stack ID -> stack. 835aacd7f0SAnup Sharma stackMap: (stack prefix ID, leaf stack frame ID) -> interned Stack ID. 845aacd7f0SAnup Sharma frameMap: Stack Frame string -> interned Frame ID. 855aacd7f0SAnup Sharma comm: str 865aacd7f0SAnup Sharma pid: int 875aacd7f0SAnup Sharma tid: int 885aacd7f0SAnup Sharma samples: List[Sample] = field(default_factory=list) 895aacd7f0SAnup Sharma frameTable: List[Frame] = field(default_factory=list) 905aacd7f0SAnup Sharma stringTable: List[str] = field(default_factory=list) 915aacd7f0SAnup Sharma stringMap: Dict[str, int] = field(default_factory=dict) 925aacd7f0SAnup Sharma stackTable: List[Stack] = field(default_factory=list) 935aacd7f0SAnup Sharma stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict) 945aacd7f0SAnup Sharma frameMap: Dict[str, int] = field(default_factory=dict) 955aacd7f0SAnup Sharma """ 965aacd7f0SAnup Sharma comm: str 975aacd7f0SAnup Sharma pid: int 985aacd7f0SAnup Sharma tid: int 995aacd7f0SAnup Sharma samples: List[Sample] = field(default_factory=list) 1005aacd7f0SAnup Sharma frameTable: List[Frame] = field(default_factory=list) 1015aacd7f0SAnup Sharma stringTable: List[str] = field(default_factory=list) 1025aacd7f0SAnup Sharma stringMap: Dict[str, int] = field(default_factory=dict) 1035aacd7f0SAnup Sharma stackTable: List[Stack] = field(default_factory=list) 1045aacd7f0SAnup Sharma stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict) 1055aacd7f0SAnup Sharma frameMap: Dict[str, int] = field(default_factory=dict) 1065aacd7f0SAnup Sharma 107*258dfd41SAnup Sharma def _intern_stack(self, frame_id: int, prefix_id: Optional[int]) -> int: 108*258dfd41SAnup Sharma """Gets a matching stack, or saves the new stack. Returns a Stack ID.""" 109*258dfd41SAnup Sharma key = f"{frame_id}" if prefix_id is None else f"{frame_id},{prefix_id}" 110*258dfd41SAnup Sharma # key = (prefix_id, frame_id) 111*258dfd41SAnup Sharma stack_id = self.stackMap.get(key) 112*258dfd41SAnup Sharma if stack_id is None: 113*258dfd41SAnup Sharma # return stack_id 114*258dfd41SAnup Sharma stack_id = len(self.stackTable) 115*258dfd41SAnup Sharma self.stackTable.append(Stack(prefix_id=prefix_id, frame_id=frame_id)) 116*258dfd41SAnup Sharma self.stackMap[key] = stack_id 117*258dfd41SAnup Sharma return stack_id 118*258dfd41SAnup Sharma 119*258dfd41SAnup Sharma def _intern_string(self, string: str) -> int: 120*258dfd41SAnup Sharma """Gets a matching string, or saves the new string. Returns a String ID.""" 121*258dfd41SAnup Sharma string_id = self.stringMap.get(string) 122*258dfd41SAnup Sharma if string_id is not None: 123*258dfd41SAnup Sharma return string_id 124*258dfd41SAnup Sharma string_id = len(self.stringTable) 125*258dfd41SAnup Sharma self.stringTable.append(string) 126*258dfd41SAnup Sharma self.stringMap[string] = string_id 127*258dfd41SAnup Sharma return string_id 128*258dfd41SAnup Sharma 129*258dfd41SAnup Sharma def _intern_frame(self, frame_str: str) -> int: 130*258dfd41SAnup Sharma """Gets a matching stack frame, or saves the new frame. Returns a Frame ID.""" 131*258dfd41SAnup Sharma frame_id = self.frameMap.get(frame_str) 132*258dfd41SAnup Sharma if frame_id is not None: 133*258dfd41SAnup Sharma return frame_id 134*258dfd41SAnup Sharma frame_id = len(self.frameTable) 135*258dfd41SAnup Sharma self.frameMap[frame_str] = frame_id 136*258dfd41SAnup Sharma string_id = self._intern_string(frame_str) 137*258dfd41SAnup Sharma 138*258dfd41SAnup Sharma symbol_name_to_category = KERNEL_CATEGORY_INDEX if frame_str.find('kallsyms') != -1 \ 139*258dfd41SAnup Sharma or frame_str.find('/vmlinux') != -1 \ 140*258dfd41SAnup Sharma or frame_str.endswith('.ko)') \ 141*258dfd41SAnup Sharma else USER_CATEGORY_INDEX 142*258dfd41SAnup Sharma 143*258dfd41SAnup Sharma self.frameTable.append(Frame( 144*258dfd41SAnup Sharma string_id=string_id, 145*258dfd41SAnup Sharma relevantForJS=False, 146*258dfd41SAnup Sharma innerWindowID=0, 147*258dfd41SAnup Sharma implementation=None, 148*258dfd41SAnup Sharma optimizations=None, 149*258dfd41SAnup Sharma line=None, 150*258dfd41SAnup Sharma column=None, 151*258dfd41SAnup Sharma category=symbol_name_to_category, 152*258dfd41SAnup Sharma subcategory=None, 153*258dfd41SAnup Sharma )) 154*258dfd41SAnup Sharma return frame_id 155*258dfd41SAnup Sharma 1565aacd7f0SAnup Sharma def _to_json_dict(self) -> Dict: 1575aacd7f0SAnup Sharma """Converts current Thread to GeckoThread JSON format.""" 1585aacd7f0SAnup Sharma # Gecko profile format is row-oriented data as List[List], 1595aacd7f0SAnup Sharma # And a schema for interpreting each index. 1605aacd7f0SAnup Sharma # Schema: 1615aacd7f0SAnup Sharma # https://github.com/firefox-devtools/profiler/blob/main/docs-developer/gecko-profile-format.md 1625aacd7f0SAnup Sharma # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L230 1635aacd7f0SAnup Sharma return { 1645aacd7f0SAnup Sharma "tid": self.tid, 1655aacd7f0SAnup Sharma "pid": self.pid, 1665aacd7f0SAnup Sharma "name": self.comm, 1675aacd7f0SAnup Sharma # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L51 1685aacd7f0SAnup Sharma "markers": { 1695aacd7f0SAnup Sharma "schema": { 1705aacd7f0SAnup Sharma "name": 0, 1715aacd7f0SAnup Sharma "startTime": 1, 1725aacd7f0SAnup Sharma "endTime": 2, 1735aacd7f0SAnup Sharma "phase": 3, 1745aacd7f0SAnup Sharma "category": 4, 1755aacd7f0SAnup Sharma "data": 5, 1765aacd7f0SAnup Sharma }, 1775aacd7f0SAnup Sharma "data": [], 1785aacd7f0SAnup Sharma }, 1795aacd7f0SAnup Sharma 1805aacd7f0SAnup Sharma # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90 1815aacd7f0SAnup Sharma "samples": { 1825aacd7f0SAnup Sharma "schema": { 1835aacd7f0SAnup Sharma "stack": 0, 1845aacd7f0SAnup Sharma "time": 1, 1855aacd7f0SAnup Sharma "responsiveness": 2, 1865aacd7f0SAnup Sharma }, 1875aacd7f0SAnup Sharma "data": self.samples 1885aacd7f0SAnup Sharma }, 1895aacd7f0SAnup Sharma 1905aacd7f0SAnup Sharma # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156 1915aacd7f0SAnup Sharma "frameTable": { 1925aacd7f0SAnup Sharma "schema": { 1935aacd7f0SAnup Sharma "location": 0, 1945aacd7f0SAnup Sharma "relevantForJS": 1, 1955aacd7f0SAnup Sharma "innerWindowID": 2, 1965aacd7f0SAnup Sharma "implementation": 3, 1975aacd7f0SAnup Sharma "optimizations": 4, 1985aacd7f0SAnup Sharma "line": 5, 1995aacd7f0SAnup Sharma "column": 6, 2005aacd7f0SAnup Sharma "category": 7, 2015aacd7f0SAnup Sharma "subcategory": 8, 2025aacd7f0SAnup Sharma }, 2035aacd7f0SAnup Sharma "data": self.frameTable, 2045aacd7f0SAnup Sharma }, 2055aacd7f0SAnup Sharma 2065aacd7f0SAnup Sharma # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216 2075aacd7f0SAnup Sharma "stackTable": { 2085aacd7f0SAnup Sharma "schema": { 2095aacd7f0SAnup Sharma "prefix": 0, 2105aacd7f0SAnup Sharma "frame": 1, 2115aacd7f0SAnup Sharma }, 2125aacd7f0SAnup Sharma "data": self.stackTable, 2135aacd7f0SAnup Sharma }, 2145aacd7f0SAnup Sharma "stringTable": self.stringTable, 2155aacd7f0SAnup Sharma "registerTime": 0, 2165aacd7f0SAnup Sharma "unregisterTime": None, 2175aacd7f0SAnup Sharma "processType": "default", 2185aacd7f0SAnup Sharma } 2195aacd7f0SAnup Sharma 2201699d3efSAnup Sharma# Uses perf script python interface to parse each 2211699d3efSAnup Sharma# event and store the data in the thread builder. 2221699d3efSAnup Sharmadef process_event(param_dict: Dict) -> None: 2230a02e44cSAnup Sharma global start_time 2240a02e44cSAnup Sharma global tid_to_thread 2250a02e44cSAnup Sharma time_stamp = (param_dict['sample']['time'] // 1000) / 1000 2260a02e44cSAnup Sharma pid = param_dict['sample']['pid'] 2270a02e44cSAnup Sharma tid = param_dict['sample']['tid'] 2280a02e44cSAnup Sharma comm = param_dict['comm'] 2290a02e44cSAnup Sharma 2300a02e44cSAnup Sharma # Start time is the time of the first sample 2310a02e44cSAnup Sharma if not start_time: 2320a02e44cSAnup Sharma start_time = time_stamp 2331699d3efSAnup Sharma 2341699d3efSAnup Sharma# Trace_end runs at the end and will be used to aggregate 2351699d3efSAnup Sharma# the data into the final json object and print it out to stdout. 2361699d3efSAnup Sharmadef trace_end() -> None: 237833daec7SAnup Sharma # Schema: https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L305 238833daec7SAnup Sharma gecko_profile_with_meta = { 239833daec7SAnup Sharma "meta": { 240833daec7SAnup Sharma "interval": 1, 241833daec7SAnup Sharma "processType": 0, 242833daec7SAnup Sharma "product": PRODUCT, 243833daec7SAnup Sharma "stackwalk": 1, 244833daec7SAnup Sharma "debug": 0, 245833daec7SAnup Sharma "gcpoison": 0, 246833daec7SAnup Sharma "asyncstack": 1, 247833daec7SAnup Sharma "startTime": start_time, 248833daec7SAnup Sharma "shutdownTime": None, 249833daec7SAnup Sharma "version": 24, 250833daec7SAnup Sharma "presymbolicated": True, 251833daec7SAnup Sharma "categories": CATEGORIES, 252833daec7SAnup Sharma "markerSchema": [], 253833daec7SAnup Sharma }, 254833daec7SAnup Sharma "libs": [], 255833daec7SAnup Sharma # threads will be implemented in later commits. 256833daec7SAnup Sharma # "threads": threads, 257833daec7SAnup Sharma "processes": [], 258833daec7SAnup Sharma "pausedRanges": [], 259833daec7SAnup Sharma } 260833daec7SAnup Sharma json.dump(gecko_profile_with_meta, sys.stdout, indent=2) 261833daec7SAnup Sharma 262833daec7SAnup Sharmadef main() -> None: 263833daec7SAnup Sharma global CATEGORIES 264833daec7SAnup Sharma parser = argparse.ArgumentParser(description="Convert perf.data to Firefox\'s Gecko Profile format") 265833daec7SAnup Sharma 266833daec7SAnup Sharma # Add the command-line options 267833daec7SAnup Sharma # Colors must be defined according to this: 268833daec7SAnup Sharma # https://github.com/firefox-devtools/profiler/blob/50124adbfa488adba6e2674a8f2618cf34b59cd2/res/css/categories.css 269833daec7SAnup Sharma parser.add_argument('--user-color', default='yellow', help='Color for the User category') 270833daec7SAnup Sharma parser.add_argument('--kernel-color', default='orange', help='Color for the Kernel category') 271833daec7SAnup Sharma # Parse the command-line arguments 272833daec7SAnup Sharma args = parser.parse_args() 273833daec7SAnup Sharma # Access the values provided by the user 274833daec7SAnup Sharma user_color = args.user_color 275833daec7SAnup Sharma kernel_color = args.kernel_color 276833daec7SAnup Sharma 277833daec7SAnup Sharma CATEGORIES = [ 278833daec7SAnup Sharma { 279833daec7SAnup Sharma "name": 'User', 280833daec7SAnup Sharma "color": user_color, 281833daec7SAnup Sharma "subcategories": ['Other'] 282833daec7SAnup Sharma }, 283833daec7SAnup Sharma { 284833daec7SAnup Sharma "name": 'Kernel', 285833daec7SAnup Sharma "color": kernel_color, 286833daec7SAnup Sharma "subcategories": ['Other'] 287833daec7SAnup Sharma }, 288833daec7SAnup Sharma ] 289833daec7SAnup Sharma 290833daec7SAnup Sharmaif __name__ == '__main__': 291833daec7SAnup Sharma main() 292