| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395 |
- # gecko.py - Convert perf record output to Firefox's gecko profile format
- # SPDX-License-Identifier: GPL-2.0
- #
- # The script converts perf.data to Gecko Profile Format,
- # which can be read by https://profiler.firefox.com/.
- #
- # Usage:
- #
- # perf record -a -g -F 99 sleep 60
- # perf script report gecko
- #
- # Combined:
- #
- # perf script gecko -F 99 -a sleep 60
- import os
- import sys
- import time
- import json
- import string
- import random
- import argparse
- import threading
- import webbrowser
- import urllib.parse
- from os import system
- from functools import reduce
- from dataclasses import dataclass, field
- from http.server import HTTPServer, SimpleHTTPRequestHandler, test
- from typing import List, Dict, Optional, NamedTuple, Set, Tuple, Any
- # Add the Perf-Trace-Util library to the Python path
- sys.path.append(os.environ['PERF_EXEC_PATH'] + \
- '/scripts/python/Perf-Trace-Util/lib/Perf/Trace')
- from perf_trace_context import *
- from Core import *
- StringID = int
- StackID = int
- FrameID = int
- CategoryID = int
- Milliseconds = float
- # start_time is intialiazed only once for the all event traces.
- start_time = None
- # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/profile.js#L425
- # Follow Brendan Gregg's Flamegraph convention: orange for kernel and yellow for user space by default.
- CATEGORIES = None
- # The product name is used by the profiler UI to show the Operating system and Processor.
- PRODUCT = os.popen('uname -op').read().strip()
- # store the output file
- output_file = None
- # Here key = tid, value = Thread
- tid_to_thread = dict()
- # The HTTP server is used to serve the profile to the profiler UI.
- http_server_thread = None
- # The category index is used by the profiler UI to show the color of the flame graph.
- USER_CATEGORY_INDEX = 0
- KERNEL_CATEGORY_INDEX = 1
- # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156
- class Frame(NamedTuple):
- string_id: StringID
- relevantForJS: bool
- innerWindowID: int
- implementation: None
- optimizations: None
- line: None
- column: None
- category: CategoryID
- subcategory: int
- # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216
- class Stack(NamedTuple):
- prefix_id: Optional[StackID]
- frame_id: FrameID
- # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90
- class Sample(NamedTuple):
- stack_id: Optional[StackID]
- time_ms: Milliseconds
- responsiveness: int
- @dataclass
- class Thread:
- """A builder for a profile of the thread.
- Attributes:
- comm: Thread command-line (name).
- pid: process ID of containing process.
- tid: thread ID.
- samples: Timeline of profile samples.
- frameTable: interned stack frame ID -> stack frame.
- stringTable: interned string ID -> string.
- stringMap: interned string -> string ID.
- stackTable: interned stack ID -> stack.
- stackMap: (stack prefix ID, leaf stack frame ID) -> interned Stack ID.
- frameMap: Stack Frame string -> interned Frame ID.
- comm: str
- pid: int
- tid: int
- samples: List[Sample] = field(default_factory=list)
- frameTable: List[Frame] = field(default_factory=list)
- stringTable: List[str] = field(default_factory=list)
- stringMap: Dict[str, int] = field(default_factory=dict)
- stackTable: List[Stack] = field(default_factory=list)
- stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict)
- frameMap: Dict[str, int] = field(default_factory=dict)
- """
- comm: str
- pid: int
- tid: int
- samples: List[Sample] = field(default_factory=list)
- frameTable: List[Frame] = field(default_factory=list)
- stringTable: List[str] = field(default_factory=list)
- stringMap: Dict[str, int] = field(default_factory=dict)
- stackTable: List[Stack] = field(default_factory=list)
- stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict)
- frameMap: Dict[str, int] = field(default_factory=dict)
- def _intern_stack(self, frame_id: int, prefix_id: Optional[int]) -> int:
- """Gets a matching stack, or saves the new stack. Returns a Stack ID."""
- key = f"{frame_id}" if prefix_id is None else f"{frame_id},{prefix_id}"
- # key = (prefix_id, frame_id)
- stack_id = self.stackMap.get(key)
- if stack_id is None:
- # return stack_id
- stack_id = len(self.stackTable)
- self.stackTable.append(Stack(prefix_id=prefix_id, frame_id=frame_id))
- self.stackMap[key] = stack_id
- return stack_id
- def _intern_string(self, string: str) -> int:
- """Gets a matching string, or saves the new string. Returns a String ID."""
- string_id = self.stringMap.get(string)
- if string_id is not None:
- return string_id
- string_id = len(self.stringTable)
- self.stringTable.append(string)
- self.stringMap[string] = string_id
- return string_id
- def _intern_frame(self, frame_str: str) -> int:
- """Gets a matching stack frame, or saves the new frame. Returns a Frame ID."""
- frame_id = self.frameMap.get(frame_str)
- if frame_id is not None:
- return frame_id
- frame_id = len(self.frameTable)
- self.frameMap[frame_str] = frame_id
- string_id = self._intern_string(frame_str)
- symbol_name_to_category = KERNEL_CATEGORY_INDEX if frame_str.find('kallsyms') != -1 \
- or frame_str.find('/vmlinux') != -1 \
- or frame_str.endswith('.ko)') \
- else USER_CATEGORY_INDEX
- self.frameTable.append(Frame(
- string_id=string_id,
- relevantForJS=False,
- innerWindowID=0,
- implementation=None,
- optimizations=None,
- line=None,
- column=None,
- category=symbol_name_to_category,
- subcategory=None,
- ))
- return frame_id
- def _add_sample(self, comm: str, stack: List[str], time_ms: Milliseconds) -> None:
- """Add a timestamped stack trace sample to the thread builder.
- Args:
- comm: command-line (name) of the thread at this sample
- stack: sampled stack frames. Root first, leaf last.
- time_ms: timestamp of sample in milliseconds.
- """
- # Ihreads may not set their names right after they are created.
- # Instead, they might do it later. In such situations, to use the latest name they have set.
- if self.comm != comm:
- self.comm = comm
- prefix_stack_id = reduce(lambda prefix_id, frame: self._intern_stack
- (self._intern_frame(frame), prefix_id), stack, None)
- if prefix_stack_id is not None:
- self.samples.append(Sample(stack_id=prefix_stack_id,
- time_ms=time_ms,
- responsiveness=0))
- def _to_json_dict(self) -> Dict:
- """Converts current Thread to GeckoThread JSON format."""
- # Gecko profile format is row-oriented data as List[List],
- # And a schema for interpreting each index.
- # Schema:
- # https://github.com/firefox-devtools/profiler/blob/main/docs-developer/gecko-profile-format.md
- # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L230
- return {
- "tid": self.tid,
- "pid": self.pid,
- "name": self.comm,
- # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L51
- "markers": {
- "schema": {
- "name": 0,
- "startTime": 1,
- "endTime": 2,
- "phase": 3,
- "category": 4,
- "data": 5,
- },
- "data": [],
- },
- # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90
- "samples": {
- "schema": {
- "stack": 0,
- "time": 1,
- "responsiveness": 2,
- },
- "data": self.samples
- },
- # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156
- "frameTable": {
- "schema": {
- "location": 0,
- "relevantForJS": 1,
- "innerWindowID": 2,
- "implementation": 3,
- "optimizations": 4,
- "line": 5,
- "column": 6,
- "category": 7,
- "subcategory": 8,
- },
- "data": self.frameTable,
- },
- # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216
- "stackTable": {
- "schema": {
- "prefix": 0,
- "frame": 1,
- },
- "data": self.stackTable,
- },
- "stringTable": self.stringTable,
- "registerTime": 0,
- "unregisterTime": None,
- "processType": "default",
- }
- # Uses perf script python interface to parse each
- # event and store the data in the thread builder.
- def process_event(param_dict: Dict) -> None:
- global start_time
- global tid_to_thread
- time_stamp = (param_dict['sample']['time'] // 1000) / 1000
- pid = param_dict['sample']['pid']
- tid = param_dict['sample']['tid']
- comm = param_dict['comm']
- # Start time is the time of the first sample
- if not start_time:
- start_time = time_stamp
- # Parse and append the callchain of the current sample into a stack.
- stack = []
- if param_dict['callchain']:
- for call in param_dict['callchain']:
- if 'sym' not in call:
- continue
- stack.append(f'{call["sym"]["name"]} (in {call["dso"]})')
- if len(stack) != 0:
- # Reverse the stack, as root come first and the leaf at the end.
- stack = stack[::-1]
- # During perf record if -g is not used, the callchain is not available.
- # In that case, the symbol and dso are available in the event parameters.
- else:
- func = param_dict['symbol'] if 'symbol' in param_dict else '[unknown]'
- dso = param_dict['dso'] if 'dso' in param_dict else '[unknown]'
- stack.append(f'{func} (in {dso})')
- # Add sample to the specific thread.
- thread = tid_to_thread.get(tid)
- if thread is None:
- thread = Thread(comm=comm, pid=pid, tid=tid)
- tid_to_thread[tid] = thread
- thread._add_sample(comm=comm, stack=stack, time_ms=time_stamp)
- def trace_begin() -> None:
- global output_file
- if (output_file is None):
- print("Staring Firefox Profiler on your default browser...")
- global http_server_thread
- http_server_thread = threading.Thread(target=test, args=(CORSRequestHandler, HTTPServer,))
- http_server_thread.daemon = True
- http_server_thread.start()
- # Trace_end runs at the end and will be used to aggregate
- # the data into the final json object and print it out to stdout.
- def trace_end() -> None:
- global output_file
- threads = [thread._to_json_dict() for thread in tid_to_thread.values()]
- # Schema: https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L305
- gecko_profile_with_meta = {
- "meta": {
- "interval": 1,
- "processType": 0,
- "product": PRODUCT,
- "stackwalk": 1,
- "debug": 0,
- "gcpoison": 0,
- "asyncstack": 1,
- "startTime": start_time,
- "shutdownTime": None,
- "version": 24,
- "presymbolicated": True,
- "categories": CATEGORIES,
- "markerSchema": [],
- },
- "libs": [],
- "threads": threads,
- "processes": [],
- "pausedRanges": [],
- }
- # launch the profiler on local host if not specified --save-only args, otherwise print to file
- if (output_file is None):
- output_file = 'gecko_profile.json'
- with open(output_file, 'w') as f:
- json.dump(gecko_profile_with_meta, f, indent=2)
- launchFirefox(output_file)
- time.sleep(1)
- print(f'[ perf gecko: Captured and wrote into {output_file} ]')
- else:
- print(f'[ perf gecko: Captured and wrote into {output_file} ]')
- with open(output_file, 'w') as f:
- json.dump(gecko_profile_with_meta, f, indent=2)
- # Used to enable Cross-Origin Resource Sharing (CORS) for requests coming from 'https://profiler.firefox.com', allowing it to access resources from this server.
- class CORSRequestHandler(SimpleHTTPRequestHandler):
- def end_headers (self):
- self.send_header('Access-Control-Allow-Origin', 'https://profiler.firefox.com')
- SimpleHTTPRequestHandler.end_headers(self)
- # start a local server to serve the gecko_profile.json file to the profiler.firefox.com
- def launchFirefox(file):
- safe_string = urllib.parse.quote_plus(f'http://localhost:8000/{file}')
- url = 'https://profiler.firefox.com/from-url/' + safe_string
- webbrowser.open(f'{url}')
- def main() -> None:
- global output_file
- global CATEGORIES
- parser = argparse.ArgumentParser(description="Convert perf.data to Firefox\'s Gecko Profile format which can be uploaded to profiler.firefox.com for visualization")
- # Add the command-line options
- # Colors must be defined according to this:
- # https://github.com/firefox-devtools/profiler/blob/50124adbfa488adba6e2674a8f2618cf34b59cd2/res/css/categories.css
- parser.add_argument('--user-color', default='yellow', help='Color for the User category', choices=['yellow', 'blue', 'purple', 'green', 'orange', 'red', 'grey', 'magenta'])
- parser.add_argument('--kernel-color', default='orange', help='Color for the Kernel category', choices=['yellow', 'blue', 'purple', 'green', 'orange', 'red', 'grey', 'magenta'])
- # If --save-only is specified, the output will be saved to a file instead of opening Firefox's profiler directly.
- parser.add_argument('--save-only', help='Save the output to a file instead of opening Firefox\'s profiler')
- # Parse the command-line arguments
- args = parser.parse_args()
- # Access the values provided by the user
- user_color = args.user_color
- kernel_color = args.kernel_color
- output_file = args.save_only
- CATEGORIES = [
- {
- "name": 'User',
- "color": user_color,
- "subcategories": ['Other']
- },
- {
- "name": 'Kernel',
- "color": kernel_color,
- "subcategories": ['Other']
- },
- ]
- if __name__ == '__main__':
- main()
|