xref: /linux-6.15/tools/perf/scripts/python/gecko.py (revision 833daec7)
1# firefox-gecko-converter.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 > output.json
11
12import os
13import sys
14import json
15import argparse
16from dataclasses import dataclass, field
17from typing import List, Dict, Optional, NamedTuple, Set, Tuple, Any
18
19# Add the Perf-Trace-Util library to the Python path
20sys.path.append(os.environ['PERF_EXEC_PATH'] + \
21	'/scripts/python/Perf-Trace-Util/lib/Perf/Trace')
22
23from perf_trace_context import *
24from Core import *
25
26StringID = int
27StackID = int
28FrameID = int
29CategoryID = int
30Milliseconds = float
31
32# start_time is intialiazed only once for the all event traces.
33start_time = None
34
35# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/profile.js#L425
36# Follow Brendan Gregg's Flamegraph convention: orange for kernel and yellow for user space by default.
37CATEGORIES = None
38
39# The product name is used by the profiler UI to show the Operating system and Processor.
40PRODUCT = os.popen('uname -op').read().strip()
41
42# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156
43class Frame(NamedTuple):
44	string_id: StringID
45	relevantForJS: bool
46	innerWindowID: int
47	implementation: None
48	optimizations: None
49	line: None
50	column: None
51	category: CategoryID
52	subcategory: int
53
54# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216
55class Stack(NamedTuple):
56	prefix_id: Optional[StackID]
57	frame_id: FrameID
58
59# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90
60class Sample(NamedTuple):
61	stack_id: Optional[StackID]
62	time_ms: Milliseconds
63	responsiveness: int
64
65@dataclass
66class Thread:
67	"""A builder for a profile of the thread.
68
69	Attributes:
70		comm: Thread command-line (name).
71		pid: process ID of containing process.
72		tid: thread ID.
73		samples: Timeline of profile samples.
74		frameTable: interned stack frame ID -> stack frame.
75		stringTable: interned string ID -> string.
76		stringMap: interned string -> string ID.
77		stackTable: interned stack ID -> stack.
78		stackMap: (stack prefix ID, leaf stack frame ID) -> interned Stack ID.
79		frameMap: Stack Frame string -> interned Frame ID.
80		comm: str
81		pid: int
82		tid: int
83		samples: List[Sample] = field(default_factory=list)
84		frameTable: List[Frame] = field(default_factory=list)
85		stringTable: List[str] = field(default_factory=list)
86		stringMap: Dict[str, int] = field(default_factory=dict)
87		stackTable: List[Stack] = field(default_factory=list)
88		stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict)
89		frameMap: Dict[str, int] = field(default_factory=dict)
90	"""
91	comm: str
92	pid: int
93	tid: int
94	samples: List[Sample] = field(default_factory=list)
95	frameTable: List[Frame] = field(default_factory=list)
96	stringTable: List[str] = field(default_factory=list)
97	stringMap: Dict[str, int] = field(default_factory=dict)
98	stackTable: List[Stack] = field(default_factory=list)
99	stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict)
100	frameMap: Dict[str, int] = field(default_factory=dict)
101
102	def _to_json_dict(self) -> Dict:
103		"""Converts current Thread to GeckoThread JSON format."""
104		# Gecko profile format is row-oriented data as List[List],
105		# And a schema for interpreting each index.
106		# Schema:
107		# https://github.com/firefox-devtools/profiler/blob/main/docs-developer/gecko-profile-format.md
108		# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L230
109		return {
110			"tid": self.tid,
111			"pid": self.pid,
112			"name": self.comm,
113			# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L51
114			"markers": {
115				"schema": {
116					"name": 0,
117					"startTime": 1,
118					"endTime": 2,
119					"phase": 3,
120					"category": 4,
121					"data": 5,
122				},
123				"data": [],
124			},
125
126			# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90
127			"samples": {
128				"schema": {
129					"stack": 0,
130					"time": 1,
131					"responsiveness": 2,
132				},
133				"data": self.samples
134			},
135
136			# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156
137			"frameTable": {
138				"schema": {
139					"location": 0,
140					"relevantForJS": 1,
141					"innerWindowID": 2,
142					"implementation": 3,
143					"optimizations": 4,
144					"line": 5,
145					"column": 6,
146					"category": 7,
147					"subcategory": 8,
148				},
149				"data": self.frameTable,
150			},
151
152			# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216
153			"stackTable": {
154				"schema": {
155					"prefix": 0,
156					"frame": 1,
157				},
158				"data": self.stackTable,
159			},
160			"stringTable": self.stringTable,
161			"registerTime": 0,
162			"unregisterTime": None,
163			"processType": "default",
164		}
165
166# Uses perf script python interface to parse each
167# event and store the data in the thread builder.
168def process_event(param_dict: Dict) -> None:
169	global start_time
170	global tid_to_thread
171	time_stamp = (param_dict['sample']['time'] // 1000) / 1000
172	pid = param_dict['sample']['pid']
173	tid = param_dict['sample']['tid']
174	comm = param_dict['comm']
175
176	# Start time is the time of the first sample
177	if not start_time:
178		start_time = time_stamp
179
180# Trace_end runs at the end and will be used to aggregate
181# the data into the final json object and print it out to stdout.
182def trace_end() -> None:
183	# Schema: https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L305
184	gecko_profile_with_meta = {
185		"meta": {
186			"interval": 1,
187			"processType": 0,
188			"product": PRODUCT,
189			"stackwalk": 1,
190			"debug": 0,
191			"gcpoison": 0,
192			"asyncstack": 1,
193			"startTime": start_time,
194			"shutdownTime": None,
195			"version": 24,
196			"presymbolicated": True,
197			"categories": CATEGORIES,
198			"markerSchema": [],
199			},
200		"libs": [],
201		# threads will be implemented in later commits.
202		# "threads": threads,
203		"processes": [],
204		"pausedRanges": [],
205	}
206	json.dump(gecko_profile_with_meta, sys.stdout, indent=2)
207
208def main() -> None:
209	global CATEGORIES
210	parser = argparse.ArgumentParser(description="Convert perf.data to Firefox\'s Gecko Profile format")
211
212	# Add the command-line options
213	# Colors must be defined according to this:
214	# https://github.com/firefox-devtools/profiler/blob/50124adbfa488adba6e2674a8f2618cf34b59cd2/res/css/categories.css
215	parser.add_argument('--user-color', default='yellow', help='Color for the User category')
216	parser.add_argument('--kernel-color', default='orange', help='Color for the Kernel category')
217	# Parse the command-line arguments
218	args = parser.parse_args()
219	# Access the values provided by the user
220	user_color = args.user_color
221	kernel_color = args.kernel_color
222
223	CATEGORIES = [
224		{
225			"name": 'User',
226			"color": user_color,
227			"subcategories": ['Other']
228		},
229		{
230			"name": 'Kernel',
231			"color": kernel_color,
232			"subcategories": ['Other']
233		},
234	]
235
236if __name__ == '__main__':
237    main()
238