~ [ source navigation ] ~ [ diff markup ] ~ [ identifier search ] ~

TOMOYO Linux Cross Reference
Linux/tools/perf/scripts/python/gecko.py

Version: ~ [ linux-6.12-rc7 ] ~ [ linux-6.11.7 ] ~ [ linux-6.10.14 ] ~ [ linux-6.9.12 ] ~ [ linux-6.8.12 ] ~ [ linux-6.7.12 ] ~ [ linux-6.6.60 ] ~ [ linux-6.5.13 ] ~ [ linux-6.4.16 ] ~ [ linux-6.3.13 ] ~ [ linux-6.2.16 ] ~ [ linux-6.1.116 ] ~ [ linux-6.0.19 ] ~ [ linux-5.19.17 ] ~ [ linux-5.18.19 ] ~ [ linux-5.17.15 ] ~ [ linux-5.16.20 ] ~ [ linux-5.15.171 ] ~ [ linux-5.14.21 ] ~ [ linux-5.13.19 ] ~ [ linux-5.12.19 ] ~ [ linux-5.11.22 ] ~ [ linux-5.10.229 ] ~ [ linux-5.9.16 ] ~ [ linux-5.8.18 ] ~ [ linux-5.7.19 ] ~ [ linux-5.6.19 ] ~ [ linux-5.5.19 ] ~ [ linux-5.4.285 ] ~ [ linux-5.3.18 ] ~ [ linux-5.2.21 ] ~ [ linux-5.1.21 ] ~ [ linux-5.0.21 ] ~ [ linux-4.20.17 ] ~ [ linux-4.19.323 ] ~ [ linux-4.18.20 ] ~ [ linux-4.17.19 ] ~ [ linux-4.16.18 ] ~ [ linux-4.15.18 ] ~ [ linux-4.14.336 ] ~ [ linux-4.13.16 ] ~ [ linux-4.12.14 ] ~ [ linux-4.11.12 ] ~ [ linux-4.10.17 ] ~ [ linux-4.9.337 ] ~ [ linux-4.4.302 ] ~ [ linux-3.10.108 ] ~ [ linux-2.6.32.71 ] ~ [ linux-2.6.0 ] ~ [ linux-2.4.37.11 ] ~ [ unix-v6-master ] ~ [ ccs-tools-1.8.12 ] ~ [ policy-sample ] ~
Architecture: ~ [ i386 ] ~ [ alpha ] ~ [ m68k ] ~ [ mips ] ~ [ ppc ] ~ [ sparc ] ~ [ sparc64 ] ~

  1 # gecko.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
 11 #
 12 # Combined:
 13 #
 14 #     perf script gecko -F 99 -a sleep 60
 15 
 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
 31 
 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')
 35 
 36 from perf_trace_context import *
 37 from Core import *
 38 
 39 StringID = int
 40 StackID = int
 41 FrameID = int
 42 CategoryID = int
 43 Milliseconds = float
 44 
 45 # start_time is intialiazed only once for the all event traces.
 46 start_time = None
 47 
 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
 51 
 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()
 54 
 55 # store the output file
 56 output_file = None
 57 
 58 # Here key = tid, value = Thread
 59 tid_to_thread = dict()
 60 
 61 # The HTTP server is used to serve the profile to the profiler UI.
 62 http_server_thread = None
 63 
 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
 67 
 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
 79 
 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
 84 
 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
 90 
 91 @dataclass
 92 class Thread:
 93         """A builder for a profile of the thread.
 94 
 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)
116         """
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)
127 
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
139 
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
149 
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)
158 
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
163 
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,
174                 ))
175                 return frame_id
176 
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.
183                 """
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
188 
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))
195 
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,
216                                 },
217                                 "data": [],
218                         },
219 
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,
226                                 },
227                                 "data": self.samples
228                         },
229 
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,
242                                 },
243                                 "data": self.frameTable,
244                         },
245 
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,
251                                 },
252                                 "data": self.stackTable,
253                         },
254                         "stringTable": self.stringTable,
255                         "registerTime": 0,
256                         "unregisterTime": None,
257                         "processType": "default",
258                 }
259 
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']
269 
270         # Start time is the time of the first sample
271         if not start_time:
272                 start_time = time_stamp
273 
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]
284 
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})')
291 
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)
298 
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()
307 
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()]
313 
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": [],
330                         },
331                 "libs": [],
332                 "threads": threads,
333                 "processes": [],
334                 "pausedRanges": [],
335         }
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)
348 
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)
354 
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}')
360 
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")
365 
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')
373 
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
380 
381         CATEGORIES = [
382                 {
383                         "name": 'User',
384                         "color": user_color,
385                         "subcategories": ['Other']
386                 },
387                 {
388                         "name": 'Kernel',
389                         "color": kernel_color,
390                         "subcategories": ['Other']
391                 },
392         ]
393 
394 if __name__ == '__main__':
395         main()

~ [ source navigation ] ~ [ diff markup ] ~ [ identifier search ] ~

kernel.org | git.kernel.org | LWN.net | Project Home | SVN repository | Mail admin

Linux® is a registered trademark of Linus Torvalds in the United States and other countries.
TOMOYO® is a registered trademark of NTT DATA CORPORATION.

sflogo.php