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 16258dfd41SAnup 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*2d889c6aSAnup Sharma# Here key = tid, value = Thread 44*2d889c6aSAnup Sharmatid_to_thread = dict() 45*2d889c6aSAnup Sharma 46258dfd41SAnup Sharma# The category index is used by the profiler UI to show the color of the flame graph. 47258dfd41SAnup SharmaUSER_CATEGORY_INDEX = 0 48258dfd41SAnup SharmaKERNEL_CATEGORY_INDEX = 1 49258dfd41SAnup Sharma 505aacd7f0SAnup Sharma# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156 515aacd7f0SAnup Sharmaclass Frame(NamedTuple): 525aacd7f0SAnup Sharma string_id: StringID 535aacd7f0SAnup Sharma relevantForJS: bool 545aacd7f0SAnup Sharma innerWindowID: int 555aacd7f0SAnup Sharma implementation: None 565aacd7f0SAnup Sharma optimizations: None 575aacd7f0SAnup Sharma line: None 585aacd7f0SAnup Sharma column: None 595aacd7f0SAnup Sharma category: CategoryID 605aacd7f0SAnup Sharma subcategory: int 615aacd7f0SAnup Sharma 625aacd7f0SAnup Sharma# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216 635aacd7f0SAnup Sharmaclass Stack(NamedTuple): 645aacd7f0SAnup Sharma prefix_id: Optional[StackID] 655aacd7f0SAnup Sharma frame_id: FrameID 665aacd7f0SAnup Sharma 675aacd7f0SAnup Sharma# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90 685aacd7f0SAnup Sharmaclass Sample(NamedTuple): 695aacd7f0SAnup Sharma stack_id: Optional[StackID] 705aacd7f0SAnup Sharma time_ms: Milliseconds 715aacd7f0SAnup Sharma responsiveness: int 725aacd7f0SAnup Sharma 735aacd7f0SAnup Sharma@dataclass 745aacd7f0SAnup Sharmaclass Thread: 755aacd7f0SAnup Sharma """A builder for a profile of the thread. 765aacd7f0SAnup Sharma 775aacd7f0SAnup Sharma Attributes: 785aacd7f0SAnup Sharma comm: Thread command-line (name). 795aacd7f0SAnup Sharma pid: process ID of containing process. 805aacd7f0SAnup Sharma tid: thread ID. 815aacd7f0SAnup Sharma samples: Timeline of profile samples. 825aacd7f0SAnup Sharma frameTable: interned stack frame ID -> stack frame. 835aacd7f0SAnup Sharma stringTable: interned string ID -> string. 845aacd7f0SAnup Sharma stringMap: interned string -> string ID. 855aacd7f0SAnup Sharma stackTable: interned stack ID -> stack. 865aacd7f0SAnup Sharma stackMap: (stack prefix ID, leaf stack frame ID) -> interned Stack ID. 875aacd7f0SAnup Sharma frameMap: Stack Frame string -> interned Frame ID. 885aacd7f0SAnup Sharma comm: str 895aacd7f0SAnup Sharma pid: int 905aacd7f0SAnup Sharma tid: int 915aacd7f0SAnup Sharma samples: List[Sample] = field(default_factory=list) 925aacd7f0SAnup Sharma frameTable: List[Frame] = field(default_factory=list) 935aacd7f0SAnup Sharma stringTable: List[str] = field(default_factory=list) 945aacd7f0SAnup Sharma stringMap: Dict[str, int] = field(default_factory=dict) 955aacd7f0SAnup Sharma stackTable: List[Stack] = field(default_factory=list) 965aacd7f0SAnup Sharma stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict) 975aacd7f0SAnup Sharma frameMap: Dict[str, int] = field(default_factory=dict) 985aacd7f0SAnup Sharma """ 995aacd7f0SAnup Sharma comm: str 1005aacd7f0SAnup Sharma pid: int 1015aacd7f0SAnup Sharma tid: int 1025aacd7f0SAnup Sharma samples: List[Sample] = field(default_factory=list) 1035aacd7f0SAnup Sharma frameTable: List[Frame] = field(default_factory=list) 1045aacd7f0SAnup Sharma stringTable: List[str] = field(default_factory=list) 1055aacd7f0SAnup Sharma stringMap: Dict[str, int] = field(default_factory=dict) 1065aacd7f0SAnup Sharma stackTable: List[Stack] = field(default_factory=list) 1075aacd7f0SAnup Sharma stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict) 1085aacd7f0SAnup Sharma frameMap: Dict[str, int] = field(default_factory=dict) 1095aacd7f0SAnup Sharma 110258dfd41SAnup Sharma def _intern_stack(self, frame_id: int, prefix_id: Optional[int]) -> int: 111258dfd41SAnup Sharma """Gets a matching stack, or saves the new stack. Returns a Stack ID.""" 112258dfd41SAnup Sharma key = f"{frame_id}" if prefix_id is None else f"{frame_id},{prefix_id}" 113258dfd41SAnup Sharma # key = (prefix_id, frame_id) 114258dfd41SAnup Sharma stack_id = self.stackMap.get(key) 115258dfd41SAnup Sharma if stack_id is None: 116258dfd41SAnup Sharma # return stack_id 117258dfd41SAnup Sharma stack_id = len(self.stackTable) 118258dfd41SAnup Sharma self.stackTable.append(Stack(prefix_id=prefix_id, frame_id=frame_id)) 119258dfd41SAnup Sharma self.stackMap[key] = stack_id 120258dfd41SAnup Sharma return stack_id 121258dfd41SAnup Sharma 122258dfd41SAnup Sharma def _intern_string(self, string: str) -> int: 123258dfd41SAnup Sharma """Gets a matching string, or saves the new string. Returns a String ID.""" 124258dfd41SAnup Sharma string_id = self.stringMap.get(string) 125258dfd41SAnup Sharma if string_id is not None: 126258dfd41SAnup Sharma return string_id 127258dfd41SAnup Sharma string_id = len(self.stringTable) 128258dfd41SAnup Sharma self.stringTable.append(string) 129258dfd41SAnup Sharma self.stringMap[string] = string_id 130258dfd41SAnup Sharma return string_id 131258dfd41SAnup Sharma 132258dfd41SAnup Sharma def _intern_frame(self, frame_str: str) -> int: 133258dfd41SAnup Sharma """Gets a matching stack frame, or saves the new frame. Returns a Frame ID.""" 134258dfd41SAnup Sharma frame_id = self.frameMap.get(frame_str) 135258dfd41SAnup Sharma if frame_id is not None: 136258dfd41SAnup Sharma return frame_id 137258dfd41SAnup Sharma frame_id = len(self.frameTable) 138258dfd41SAnup Sharma self.frameMap[frame_str] = frame_id 139258dfd41SAnup Sharma string_id = self._intern_string(frame_str) 140258dfd41SAnup Sharma 141258dfd41SAnup Sharma symbol_name_to_category = KERNEL_CATEGORY_INDEX if frame_str.find('kallsyms') != -1 \ 142258dfd41SAnup Sharma or frame_str.find('/vmlinux') != -1 \ 143258dfd41SAnup Sharma or frame_str.endswith('.ko)') \ 144258dfd41SAnup Sharma else USER_CATEGORY_INDEX 145258dfd41SAnup Sharma 146258dfd41SAnup Sharma self.frameTable.append(Frame( 147258dfd41SAnup Sharma string_id=string_id, 148258dfd41SAnup Sharma relevantForJS=False, 149258dfd41SAnup Sharma innerWindowID=0, 150258dfd41SAnup Sharma implementation=None, 151258dfd41SAnup Sharma optimizations=None, 152258dfd41SAnup Sharma line=None, 153258dfd41SAnup Sharma column=None, 154258dfd41SAnup Sharma category=symbol_name_to_category, 155258dfd41SAnup Sharma subcategory=None, 156258dfd41SAnup Sharma )) 157258dfd41SAnup Sharma return frame_id 158258dfd41SAnup Sharma 159*2d889c6aSAnup Sharma def _add_sample(self, comm: str, stack: List[str], time_ms: Milliseconds) -> None: 160*2d889c6aSAnup Sharma """Add a timestamped stack trace sample to the thread builder. 161*2d889c6aSAnup Sharma Args: 162*2d889c6aSAnup Sharma comm: command-line (name) of the thread at this sample 163*2d889c6aSAnup Sharma stack: sampled stack frames. Root first, leaf last. 164*2d889c6aSAnup Sharma time_ms: timestamp of sample in milliseconds. 165*2d889c6aSAnup Sharma """ 166*2d889c6aSAnup Sharma # Ihreads may not set their names right after they are created. 167*2d889c6aSAnup Sharma # Instead, they might do it later. In such situations, to use the latest name they have set. 168*2d889c6aSAnup Sharma if self.comm != comm: 169*2d889c6aSAnup Sharma self.comm = comm 170*2d889c6aSAnup Sharma 171*2d889c6aSAnup Sharma prefix_stack_id = reduce(lambda prefix_id, frame: self._intern_stack 172*2d889c6aSAnup Sharma (self._intern_frame(frame), prefix_id), stack, None) 173*2d889c6aSAnup Sharma if prefix_stack_id is not None: 174*2d889c6aSAnup Sharma self.samples.append(Sample(stack_id=prefix_stack_id, 175*2d889c6aSAnup Sharma time_ms=time_ms, 176*2d889c6aSAnup Sharma responsiveness=0)) 177*2d889c6aSAnup Sharma 1785aacd7f0SAnup Sharma def _to_json_dict(self) -> Dict: 1795aacd7f0SAnup Sharma """Converts current Thread to GeckoThread JSON format.""" 1805aacd7f0SAnup Sharma # Gecko profile format is row-oriented data as List[List], 1815aacd7f0SAnup Sharma # And a schema for interpreting each index. 1825aacd7f0SAnup Sharma # Schema: 1835aacd7f0SAnup Sharma # https://github.com/firefox-devtools/profiler/blob/main/docs-developer/gecko-profile-format.md 1845aacd7f0SAnup Sharma # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L230 1855aacd7f0SAnup Sharma return { 1865aacd7f0SAnup Sharma "tid": self.tid, 1875aacd7f0SAnup Sharma "pid": self.pid, 1885aacd7f0SAnup Sharma "name": self.comm, 1895aacd7f0SAnup Sharma # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L51 1905aacd7f0SAnup Sharma "markers": { 1915aacd7f0SAnup Sharma "schema": { 1925aacd7f0SAnup Sharma "name": 0, 1935aacd7f0SAnup Sharma "startTime": 1, 1945aacd7f0SAnup Sharma "endTime": 2, 1955aacd7f0SAnup Sharma "phase": 3, 1965aacd7f0SAnup Sharma "category": 4, 1975aacd7f0SAnup Sharma "data": 5, 1985aacd7f0SAnup Sharma }, 1995aacd7f0SAnup Sharma "data": [], 2005aacd7f0SAnup Sharma }, 2015aacd7f0SAnup Sharma 2025aacd7f0SAnup Sharma # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90 2035aacd7f0SAnup Sharma "samples": { 2045aacd7f0SAnup Sharma "schema": { 2055aacd7f0SAnup Sharma "stack": 0, 2065aacd7f0SAnup Sharma "time": 1, 2075aacd7f0SAnup Sharma "responsiveness": 2, 2085aacd7f0SAnup Sharma }, 2095aacd7f0SAnup Sharma "data": self.samples 2105aacd7f0SAnup Sharma }, 2115aacd7f0SAnup Sharma 2125aacd7f0SAnup Sharma # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156 2135aacd7f0SAnup Sharma "frameTable": { 2145aacd7f0SAnup Sharma "schema": { 2155aacd7f0SAnup Sharma "location": 0, 2165aacd7f0SAnup Sharma "relevantForJS": 1, 2175aacd7f0SAnup Sharma "innerWindowID": 2, 2185aacd7f0SAnup Sharma "implementation": 3, 2195aacd7f0SAnup Sharma "optimizations": 4, 2205aacd7f0SAnup Sharma "line": 5, 2215aacd7f0SAnup Sharma "column": 6, 2225aacd7f0SAnup Sharma "category": 7, 2235aacd7f0SAnup Sharma "subcategory": 8, 2245aacd7f0SAnup Sharma }, 2255aacd7f0SAnup Sharma "data": self.frameTable, 2265aacd7f0SAnup Sharma }, 2275aacd7f0SAnup Sharma 2285aacd7f0SAnup Sharma # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216 2295aacd7f0SAnup Sharma "stackTable": { 2305aacd7f0SAnup Sharma "schema": { 2315aacd7f0SAnup Sharma "prefix": 0, 2325aacd7f0SAnup Sharma "frame": 1, 2335aacd7f0SAnup Sharma }, 2345aacd7f0SAnup Sharma "data": self.stackTable, 2355aacd7f0SAnup Sharma }, 2365aacd7f0SAnup Sharma "stringTable": self.stringTable, 2375aacd7f0SAnup Sharma "registerTime": 0, 2385aacd7f0SAnup Sharma "unregisterTime": None, 2395aacd7f0SAnup Sharma "processType": "default", 2405aacd7f0SAnup Sharma } 2415aacd7f0SAnup Sharma 2421699d3efSAnup Sharma# Uses perf script python interface to parse each 2431699d3efSAnup Sharma# event and store the data in the thread builder. 2441699d3efSAnup Sharmadef process_event(param_dict: Dict) -> None: 2450a02e44cSAnup Sharma global start_time 2460a02e44cSAnup Sharma global tid_to_thread 2470a02e44cSAnup Sharma time_stamp = (param_dict['sample']['time'] // 1000) / 1000 2480a02e44cSAnup Sharma pid = param_dict['sample']['pid'] 2490a02e44cSAnup Sharma tid = param_dict['sample']['tid'] 2500a02e44cSAnup Sharma comm = param_dict['comm'] 2510a02e44cSAnup Sharma 2520a02e44cSAnup Sharma # Start time is the time of the first sample 2530a02e44cSAnup Sharma if not start_time: 2540a02e44cSAnup Sharma start_time = time_stamp 2551699d3efSAnup Sharma 256*2d889c6aSAnup Sharma # Parse and append the callchain of the current sample into a stack. 257*2d889c6aSAnup Sharma stack = [] 258*2d889c6aSAnup Sharma if param_dict['callchain']: 259*2d889c6aSAnup Sharma for call in param_dict['callchain']: 260*2d889c6aSAnup Sharma if 'sym' not in call: 261*2d889c6aSAnup Sharma continue 262*2d889c6aSAnup Sharma stack.append(f'{call["sym"]["name"]} (in {call["dso"]})') 263*2d889c6aSAnup Sharma if len(stack) != 0: 264*2d889c6aSAnup Sharma # Reverse the stack, as root come first and the leaf at the end. 265*2d889c6aSAnup Sharma stack = stack[::-1] 266*2d889c6aSAnup Sharma 267*2d889c6aSAnup Sharma # During perf record if -g is not used, the callchain is not available. 268*2d889c6aSAnup Sharma # In that case, the symbol and dso are available in the event parameters. 269*2d889c6aSAnup Sharma else: 270*2d889c6aSAnup Sharma func = param_dict['symbol'] if 'symbol' in param_dict else '[unknown]' 271*2d889c6aSAnup Sharma dso = param_dict['dso'] if 'dso' in param_dict else '[unknown]' 272*2d889c6aSAnup Sharma stack.append(f'{func} (in {dso})') 273*2d889c6aSAnup Sharma 274*2d889c6aSAnup Sharma # Add sample to the specific thread. 275*2d889c6aSAnup Sharma thread = tid_to_thread.get(tid) 276*2d889c6aSAnup Sharma if thread is None: 277*2d889c6aSAnup Sharma thread = Thread(comm=comm, pid=pid, tid=tid) 278*2d889c6aSAnup Sharma tid_to_thread[tid] = thread 279*2d889c6aSAnup Sharma thread._add_sample(comm=comm, stack=stack, time_ms=time_stamp) 280*2d889c6aSAnup Sharma 2811699d3efSAnup Sharma# Trace_end runs at the end and will be used to aggregate 2821699d3efSAnup Sharma# the data into the final json object and print it out to stdout. 2831699d3efSAnup Sharmadef trace_end() -> None: 284*2d889c6aSAnup Sharma threads = [thread._to_json_dict() for thread in tid_to_thread.values()] 285*2d889c6aSAnup Sharma 286833daec7SAnup Sharma # Schema: https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L305 287833daec7SAnup Sharma gecko_profile_with_meta = { 288833daec7SAnup Sharma "meta": { 289833daec7SAnup Sharma "interval": 1, 290833daec7SAnup Sharma "processType": 0, 291833daec7SAnup Sharma "product": PRODUCT, 292833daec7SAnup Sharma "stackwalk": 1, 293833daec7SAnup Sharma "debug": 0, 294833daec7SAnup Sharma "gcpoison": 0, 295833daec7SAnup Sharma "asyncstack": 1, 296833daec7SAnup Sharma "startTime": start_time, 297833daec7SAnup Sharma "shutdownTime": None, 298833daec7SAnup Sharma "version": 24, 299833daec7SAnup Sharma "presymbolicated": True, 300833daec7SAnup Sharma "categories": CATEGORIES, 301833daec7SAnup Sharma "markerSchema": [], 302833daec7SAnup Sharma }, 303833daec7SAnup Sharma "libs": [], 304*2d889c6aSAnup Sharma "threads": threads, 305833daec7SAnup Sharma "processes": [], 306833daec7SAnup Sharma "pausedRanges": [], 307833daec7SAnup Sharma } 308833daec7SAnup Sharma json.dump(gecko_profile_with_meta, sys.stdout, indent=2) 309833daec7SAnup Sharma 310833daec7SAnup Sharmadef main() -> None: 311833daec7SAnup Sharma global CATEGORIES 312833daec7SAnup Sharma parser = argparse.ArgumentParser(description="Convert perf.data to Firefox\'s Gecko Profile format") 313833daec7SAnup Sharma 314833daec7SAnup Sharma # Add the command-line options 315833daec7SAnup Sharma # Colors must be defined according to this: 316833daec7SAnup Sharma # https://github.com/firefox-devtools/profiler/blob/50124adbfa488adba6e2674a8f2618cf34b59cd2/res/css/categories.css 317833daec7SAnup Sharma parser.add_argument('--user-color', default='yellow', help='Color for the User category') 318833daec7SAnup Sharma parser.add_argument('--kernel-color', default='orange', help='Color for the Kernel category') 319833daec7SAnup Sharma # Parse the command-line arguments 320833daec7SAnup Sharma args = parser.parse_args() 321833daec7SAnup Sharma # Access the values provided by the user 322833daec7SAnup Sharma user_color = args.user_color 323833daec7SAnup Sharma kernel_color = args.kernel_color 324833daec7SAnup Sharma 325833daec7SAnup Sharma CATEGORIES = [ 326833daec7SAnup Sharma { 327833daec7SAnup Sharma "name": 'User', 328833daec7SAnup Sharma "color": user_color, 329833daec7SAnup Sharma "subcategories": ['Other'] 330833daec7SAnup Sharma }, 331833daec7SAnup Sharma { 332833daec7SAnup Sharma "name": 'Kernel', 333833daec7SAnup Sharma "color": kernel_color, 334833daec7SAnup Sharma "subcategories": ['Other'] 335833daec7SAnup Sharma }, 336833daec7SAnup Sharma ] 337833daec7SAnup Sharma 338833daec7SAnup Sharmaif __name__ == '__main__': 339833daec7SAnup Sharma main() 340