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/.
9 # perf record -a -g -F 99 sleep 60
10 # perf script report gecko
14 # perf script gecko -F 99 -a sleep 60
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 *
45 # start_time is intialiazed only once for the all event traces.
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.
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
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
):
80 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216
81 class Stack(NamedTuple
):
82 prefix_id
: Optional
[StackID
]
85 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90
86 class Sample(NamedTuple
):
87 stack_id
: Optional
[StackID
]
93 """A builder for a profile of the thread.
96 comm: Thread command-line (name).
97 pid: process ID of containing process.
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.
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)
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
)
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
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:
145 string_id
= len(self
.stringTable
)
146 self
.stringTable
.append(string
)
147 self
.stringMap
[string
] = 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:
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(
172 category
=symbol_name_to_category
,
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.
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
:
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
,
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.
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
207 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L51
220 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90
230 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156
243 "data": self
.frameTable
,
246 # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216
252 "data": self
.stackTable
,
254 "stringTable": self
.stringTable
,
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:
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
272 start_time
= time_stamp
274 # Parse and append the callchain of the current sample into a stack.
276 if param_dict
['callchain']:
277 for call
in param_dict
['callchain']:
278 if 'sym' not in call
:
280 stack
.append(f
'{call["sym"]["name"]} (in {call["dso"]})')
282 # Reverse the stack, as root come first and the leaf at the end.
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.
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
)
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:
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:
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
= {
324 "startTime": start_time
,
325 "shutdownTime": None,
327 "presymbolicated": True,
328 "categories": CATEGORIES
,
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
)
343 print(f
'[ perf gecko: Captured and wrote into {output_file} ]')
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}')
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
385 "subcategories": ['Other']
389 "color": kernel_color
,
390 "subcategories": ['Other']
394 if __name__
== '__main__':