drm/nouveau: consume the return of large GSP message
[drm/drm-misc.git] / tools / perf / scripts / python / gecko.py
blobbc5a72f94bfab1197451e300585cbae27e3a76fd
1 # gecko.py - Convert perf record output to Firefox's gecko profile format
2 # SPDX-License-Identifier: GPL-2.0
4 # The script converts perf.data to Gecko Profile Format,
5 # which can be read by https://profiler.firefox.com/.
7 # Usage:
9 # perf record -a -g -F 99 sleep 60
10 # perf script report gecko
12 # Combined:
14 # perf script gecko -F 99 -a sleep 60
16 import os
17 import sys
18 import time
19 import json
20 import string
21 import random
22 import argparse
23 import threading
24 import webbrowser
25 import urllib.parse
26 from os import system
27 from functools import reduce
28 from dataclasses import dataclass, field
29 from http.server import HTTPServer, SimpleHTTPRequestHandler, test
30 from typing import List, Dict, Optional, NamedTuple, Set, Tuple, Any
32 # Add the Perf-Trace-Util library to the Python path
33 sys.path.append(os.environ['PERF_EXEC_PATH'] + \
34 '/scripts/python/Perf-Trace-Util/lib/Perf/Trace')
36 from perf_trace_context import *
37 from Core import *
39 StringID = int
40 StackID = int
41 FrameID = int
42 CategoryID = int
43 Milliseconds = float
45 # start_time is intialiazed only once for the all event traces.
46 start_time = None
48 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/profile.js#L425
49 # Follow Brendan Gregg's Flamegraph convention: orange for kernel and yellow for user space by default.
50 CATEGORIES = None
52 # The product name is used by the profiler UI to show the Operating system and Processor.
53 PRODUCT = os.popen('uname -op').read().strip()
55 # store the output file
56 output_file = None
58 # Here key = tid, value = Thread
59 tid_to_thread = dict()
61 # The HTTP server is used to serve the profile to the profiler UI.
62 http_server_thread = None
64 # The category index is used by the profiler UI to show the color of the flame graph.
65 USER_CATEGORY_INDEX = 0
66 KERNEL_CATEGORY_INDEX = 1
68 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156
69 class Frame(NamedTuple):
70 string_id: StringID
71 relevantForJS: bool
72 innerWindowID: int
73 implementation: None
74 optimizations: None
75 line: None
76 column: None
77 category: CategoryID
78 subcategory: int
80 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216
81 class Stack(NamedTuple):
82 prefix_id: Optional[StackID]
83 frame_id: FrameID
85 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90
86 class Sample(NamedTuple):
87 stack_id: Optional[StackID]
88 time_ms: Milliseconds
89 responsiveness: int
91 @dataclass
92 class Thread:
93 """A builder for a profile of the thread.
95 Attributes:
96 comm: Thread command-line (name).
97 pid: process ID of containing process.
98 tid: thread ID.
99 samples: Timeline of profile samples.
100 frameTable: interned stack frame ID -> stack frame.
101 stringTable: interned string ID -> string.
102 stringMap: interned string -> string ID.
103 stackTable: interned stack ID -> stack.
104 stackMap: (stack prefix ID, leaf stack frame ID) -> interned Stack ID.
105 frameMap: Stack Frame string -> interned Frame ID.
106 comm: str
107 pid: int
108 tid: int
109 samples: List[Sample] = field(default_factory=list)
110 frameTable: List[Frame] = field(default_factory=list)
111 stringTable: List[str] = field(default_factory=list)
112 stringMap: Dict[str, int] = field(default_factory=dict)
113 stackTable: List[Stack] = field(default_factory=list)
114 stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict)
115 frameMap: Dict[str, int] = field(default_factory=dict)
117 comm: str
118 pid: int
119 tid: int
120 samples: List[Sample] = field(default_factory=list)
121 frameTable: List[Frame] = field(default_factory=list)
122 stringTable: List[str] = field(default_factory=list)
123 stringMap: Dict[str, int] = field(default_factory=dict)
124 stackTable: List[Stack] = field(default_factory=list)
125 stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict)
126 frameMap: Dict[str, int] = field(default_factory=dict)
128 def _intern_stack(self, frame_id: int, prefix_id: Optional[int]) -> int:
129 """Gets a matching stack, or saves the new stack. Returns a Stack ID."""
130 key = f"{frame_id}" if prefix_id is None else f"{frame_id},{prefix_id}"
131 # key = (prefix_id, frame_id)
132 stack_id = self.stackMap.get(key)
133 if stack_id is None:
134 # return stack_id
135 stack_id = len(self.stackTable)
136 self.stackTable.append(Stack(prefix_id=prefix_id, frame_id=frame_id))
137 self.stackMap[key] = stack_id
138 return stack_id
140 def _intern_string(self, string: str) -> int:
141 """Gets a matching string, or saves the new string. Returns a String ID."""
142 string_id = self.stringMap.get(string)
143 if string_id is not None:
144 return string_id
145 string_id = len(self.stringTable)
146 self.stringTable.append(string)
147 self.stringMap[string] = string_id
148 return string_id
150 def _intern_frame(self, frame_str: str) -> int:
151 """Gets a matching stack frame, or saves the new frame. Returns a Frame ID."""
152 frame_id = self.frameMap.get(frame_str)
153 if frame_id is not None:
154 return frame_id
155 frame_id = len(self.frameTable)
156 self.frameMap[frame_str] = frame_id
157 string_id = self._intern_string(frame_str)
159 symbol_name_to_category = KERNEL_CATEGORY_INDEX if frame_str.find('kallsyms') != -1 \
160 or frame_str.find('/vmlinux') != -1 \
161 or frame_str.endswith('.ko)') \
162 else USER_CATEGORY_INDEX
164 self.frameTable.append(Frame(
165 string_id=string_id,
166 relevantForJS=False,
167 innerWindowID=0,
168 implementation=None,
169 optimizations=None,
170 line=None,
171 column=None,
172 category=symbol_name_to_category,
173 subcategory=None,
175 return frame_id
177 def _add_sample(self, comm: str, stack: List[str], time_ms: Milliseconds) -> None:
178 """Add a timestamped stack trace sample to the thread builder.
179 Args:
180 comm: command-line (name) of the thread at this sample
181 stack: sampled stack frames. Root first, leaf last.
182 time_ms: timestamp of sample in milliseconds.
184 # Ihreads may not set their names right after they are created.
185 # Instead, they might do it later. In such situations, to use the latest name they have set.
186 if self.comm != comm:
187 self.comm = comm
189 prefix_stack_id = reduce(lambda prefix_id, frame: self._intern_stack
190 (self._intern_frame(frame), prefix_id), stack, None)
191 if prefix_stack_id is not None:
192 self.samples.append(Sample(stack_id=prefix_stack_id,
193 time_ms=time_ms,
194 responsiveness=0))
196 def _to_json_dict(self) -> Dict:
197 """Converts current Thread to GeckoThread JSON format."""
198 # Gecko profile format is row-oriented data as List[List],
199 # And a schema for interpreting each index.
200 # Schema:
201 # https://github.com/firefox-devtools/profiler/blob/main/docs-developer/gecko-profile-format.md
202 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L230
203 return {
204 "tid": self.tid,
205 "pid": self.pid,
206 "name": self.comm,
207 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L51
208 "markers": {
209 "schema": {
210 "name": 0,
211 "startTime": 1,
212 "endTime": 2,
213 "phase": 3,
214 "category": 4,
215 "data": 5,
217 "data": [],
220 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90
221 "samples": {
222 "schema": {
223 "stack": 0,
224 "time": 1,
225 "responsiveness": 2,
227 "data": self.samples
230 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156
231 "frameTable": {
232 "schema": {
233 "location": 0,
234 "relevantForJS": 1,
235 "innerWindowID": 2,
236 "implementation": 3,
237 "optimizations": 4,
238 "line": 5,
239 "column": 6,
240 "category": 7,
241 "subcategory": 8,
243 "data": self.frameTable,
246 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216
247 "stackTable": {
248 "schema": {
249 "prefix": 0,
250 "frame": 1,
252 "data": self.stackTable,
254 "stringTable": self.stringTable,
255 "registerTime": 0,
256 "unregisterTime": None,
257 "processType": "default",
260 # Uses perf script python interface to parse each
261 # event and store the data in the thread builder.
262 def process_event(param_dict: Dict) -> None:
263 global start_time
264 global tid_to_thread
265 time_stamp = (param_dict['sample']['time'] // 1000) / 1000
266 pid = param_dict['sample']['pid']
267 tid = param_dict['sample']['tid']
268 comm = param_dict['comm']
270 # Start time is the time of the first sample
271 if not start_time:
272 start_time = time_stamp
274 # Parse and append the callchain of the current sample into a stack.
275 stack = []
276 if param_dict['callchain']:
277 for call in param_dict['callchain']:
278 if 'sym' not in call:
279 continue
280 stack.append(f'{call["sym"]["name"]} (in {call["dso"]})')
281 if len(stack) != 0:
282 # Reverse the stack, as root come first and the leaf at the end.
283 stack = stack[::-1]
285 # During perf record if -g is not used, the callchain is not available.
286 # In that case, the symbol and dso are available in the event parameters.
287 else:
288 func = param_dict['symbol'] if 'symbol' in param_dict else '[unknown]'
289 dso = param_dict['dso'] if 'dso' in param_dict else '[unknown]'
290 stack.append(f'{func} (in {dso})')
292 # Add sample to the specific thread.
293 thread = tid_to_thread.get(tid)
294 if thread is None:
295 thread = Thread(comm=comm, pid=pid, tid=tid)
296 tid_to_thread[tid] = thread
297 thread._add_sample(comm=comm, stack=stack, time_ms=time_stamp)
299 def trace_begin() -> None:
300 global output_file
301 if (output_file is None):
302 print("Staring Firefox Profiler on your default browser...")
303 global http_server_thread
304 http_server_thread = threading.Thread(target=test, args=(CORSRequestHandler, HTTPServer,))
305 http_server_thread.daemon = True
306 http_server_thread.start()
308 # Trace_end runs at the end and will be used to aggregate
309 # the data into the final json object and print it out to stdout.
310 def trace_end() -> None:
311 global output_file
312 threads = [thread._to_json_dict() for thread in tid_to_thread.values()]
314 # Schema: https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L305
315 gecko_profile_with_meta = {
316 "meta": {
317 "interval": 1,
318 "processType": 0,
319 "product": PRODUCT,
320 "stackwalk": 1,
321 "debug": 0,
322 "gcpoison": 0,
323 "asyncstack": 1,
324 "startTime": start_time,
325 "shutdownTime": None,
326 "version": 24,
327 "presymbolicated": True,
328 "categories": CATEGORIES,
329 "markerSchema": [],
331 "libs": [],
332 "threads": threads,
333 "processes": [],
334 "pausedRanges": [],
336 # launch the profiler on local host if not specified --save-only args, otherwise print to file
337 if (output_file is None):
338 output_file = 'gecko_profile.json'
339 with open(output_file, 'w') as f:
340 json.dump(gecko_profile_with_meta, f, indent=2)
341 launchFirefox(output_file)
342 time.sleep(1)
343 print(f'[ perf gecko: Captured and wrote into {output_file} ]')
344 else:
345 print(f'[ perf gecko: Captured and wrote into {output_file} ]')
346 with open(output_file, 'w') as f:
347 json.dump(gecko_profile_with_meta, f, indent=2)
349 # Used to enable Cross-Origin Resource Sharing (CORS) for requests coming from 'https://profiler.firefox.com', allowing it to access resources from this server.
350 class CORSRequestHandler(SimpleHTTPRequestHandler):
351 def end_headers (self):
352 self.send_header('Access-Control-Allow-Origin', 'https://profiler.firefox.com')
353 SimpleHTTPRequestHandler.end_headers(self)
355 # start a local server to serve the gecko_profile.json file to the profiler.firefox.com
356 def launchFirefox(file):
357 safe_string = urllib.parse.quote_plus(f'http://localhost:8000/{file}')
358 url = 'https://profiler.firefox.com/from-url/' + safe_string
359 webbrowser.open(f'{url}')
361 def main() -> None:
362 global output_file
363 global CATEGORIES
364 parser = argparse.ArgumentParser(description="Convert perf.data to Firefox\'s Gecko Profile format which can be uploaded to profiler.firefox.com for visualization")
366 # Add the command-line options
367 # Colors must be defined according to this:
368 # https://github.com/firefox-devtools/profiler/blob/50124adbfa488adba6e2674a8f2618cf34b59cd2/res/css/categories.css
369 parser.add_argument('--user-color', default='yellow', help='Color for the User category', choices=['yellow', 'blue', 'purple', 'green', 'orange', 'red', 'grey', 'magenta'])
370 parser.add_argument('--kernel-color', default='orange', help='Color for the Kernel category', choices=['yellow', 'blue', 'purple', 'green', 'orange', 'red', 'grey', 'magenta'])
371 # If --save-only is specified, the output will be saved to a file instead of opening Firefox's profiler directly.
372 parser.add_argument('--save-only', help='Save the output to a file instead of opening Firefox\'s profiler')
374 # Parse the command-line arguments
375 args = parser.parse_args()
376 # Access the values provided by the user
377 user_color = args.user_color
378 kernel_color = args.kernel_color
379 output_file = args.save_only
381 CATEGORIES = [
383 "name": 'User',
384 "color": user_color,
385 "subcategories": ['Other']
388 "name": 'Kernel',
389 "color": kernel_color,
390 "subcategories": ['Other']
394 if __name__ == '__main__':
395 main()