xref: /linux-6.15/tools/perf/scripts/python/gecko.py (revision 258dfd41)
11699d3efSAnup Sharma# firefox-gecko-converter.py - Convert perf record output to Firefox's gecko profile format
21699d3efSAnup Sharma# SPDX-License-Identifier: GPL-2.0
31699d3efSAnup Sharma#
41699d3efSAnup Sharma# The script converts perf.data to Gecko Profile Format,
51699d3efSAnup Sharma# which can be read by https://profiler.firefox.com/.
61699d3efSAnup Sharma#
71699d3efSAnup Sharma# Usage:
81699d3efSAnup Sharma#
91699d3efSAnup Sharma#     perf record -a -g -F 99 sleep 60
101699d3efSAnup Sharma#     perf script report gecko > output.json
111699d3efSAnup Sharma
121699d3efSAnup Sharmaimport os
131699d3efSAnup Sharmaimport sys
14833daec7SAnup Sharmaimport json
15833daec7SAnup Sharmaimport argparse
16*258dfd41SAnup Sharmafrom functools import reduce
175aacd7f0SAnup Sharmafrom dataclasses import dataclass, field
185aacd7f0SAnup Sharmafrom typing import List, Dict, Optional, NamedTuple, Set, Tuple, Any
191699d3efSAnup Sharma
201699d3efSAnup Sharma# Add the Perf-Trace-Util library to the Python path
211699d3efSAnup Sharmasys.path.append(os.environ['PERF_EXEC_PATH'] + \
221699d3efSAnup Sharma	'/scripts/python/Perf-Trace-Util/lib/Perf/Trace')
231699d3efSAnup Sharma
241699d3efSAnup Sharmafrom perf_trace_context import *
251699d3efSAnup Sharmafrom Core import *
261699d3efSAnup Sharma
275aacd7f0SAnup SharmaStringID = int
285aacd7f0SAnup SharmaStackID = int
295aacd7f0SAnup SharmaFrameID = int
305aacd7f0SAnup SharmaCategoryID = int
315aacd7f0SAnup SharmaMilliseconds = float
325aacd7f0SAnup Sharma
330a02e44cSAnup Sharma# start_time is intialiazed only once for the all event traces.
340a02e44cSAnup Sharmastart_time = None
350a02e44cSAnup Sharma
36833daec7SAnup Sharma# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/profile.js#L425
37833daec7SAnup Sharma# Follow Brendan Gregg's Flamegraph convention: orange for kernel and yellow for user space by default.
38833daec7SAnup SharmaCATEGORIES = None
39833daec7SAnup Sharma
40833daec7SAnup Sharma# The product name is used by the profiler UI to show the Operating system and Processor.
41833daec7SAnup SharmaPRODUCT = os.popen('uname -op').read().strip()
42833daec7SAnup Sharma
43*258dfd41SAnup Sharma# The category index is used by the profiler UI to show the color of the flame graph.
44*258dfd41SAnup SharmaUSER_CATEGORY_INDEX = 0
45*258dfd41SAnup SharmaKERNEL_CATEGORY_INDEX = 1
46*258dfd41SAnup Sharma
475aacd7f0SAnup Sharma# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156
485aacd7f0SAnup Sharmaclass Frame(NamedTuple):
495aacd7f0SAnup Sharma	string_id: StringID
505aacd7f0SAnup Sharma	relevantForJS: bool
515aacd7f0SAnup Sharma	innerWindowID: int
525aacd7f0SAnup Sharma	implementation: None
535aacd7f0SAnup Sharma	optimizations: None
545aacd7f0SAnup Sharma	line: None
555aacd7f0SAnup Sharma	column: None
565aacd7f0SAnup Sharma	category: CategoryID
575aacd7f0SAnup Sharma	subcategory: int
585aacd7f0SAnup Sharma
595aacd7f0SAnup Sharma# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216
605aacd7f0SAnup Sharmaclass Stack(NamedTuple):
615aacd7f0SAnup Sharma	prefix_id: Optional[StackID]
625aacd7f0SAnup Sharma	frame_id: FrameID
635aacd7f0SAnup Sharma
645aacd7f0SAnup Sharma# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90
655aacd7f0SAnup Sharmaclass Sample(NamedTuple):
665aacd7f0SAnup Sharma	stack_id: Optional[StackID]
675aacd7f0SAnup Sharma	time_ms: Milliseconds
685aacd7f0SAnup Sharma	responsiveness: int
695aacd7f0SAnup Sharma
705aacd7f0SAnup Sharma@dataclass
715aacd7f0SAnup Sharmaclass Thread:
725aacd7f0SAnup Sharma	"""A builder for a profile of the thread.
735aacd7f0SAnup Sharma
745aacd7f0SAnup Sharma	Attributes:
755aacd7f0SAnup Sharma		comm: Thread command-line (name).
765aacd7f0SAnup Sharma		pid: process ID of containing process.
775aacd7f0SAnup Sharma		tid: thread ID.
785aacd7f0SAnup Sharma		samples: Timeline of profile samples.
795aacd7f0SAnup Sharma		frameTable: interned stack frame ID -> stack frame.
805aacd7f0SAnup Sharma		stringTable: interned string ID -> string.
815aacd7f0SAnup Sharma		stringMap: interned string -> string ID.
825aacd7f0SAnup Sharma		stackTable: interned stack ID -> stack.
835aacd7f0SAnup Sharma		stackMap: (stack prefix ID, leaf stack frame ID) -> interned Stack ID.
845aacd7f0SAnup Sharma		frameMap: Stack Frame string -> interned Frame ID.
855aacd7f0SAnup Sharma		comm: str
865aacd7f0SAnup Sharma		pid: int
875aacd7f0SAnup Sharma		tid: int
885aacd7f0SAnup Sharma		samples: List[Sample] = field(default_factory=list)
895aacd7f0SAnup Sharma		frameTable: List[Frame] = field(default_factory=list)
905aacd7f0SAnup Sharma		stringTable: List[str] = field(default_factory=list)
915aacd7f0SAnup Sharma		stringMap: Dict[str, int] = field(default_factory=dict)
925aacd7f0SAnup Sharma		stackTable: List[Stack] = field(default_factory=list)
935aacd7f0SAnup Sharma		stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict)
945aacd7f0SAnup Sharma		frameMap: Dict[str, int] = field(default_factory=dict)
955aacd7f0SAnup Sharma	"""
965aacd7f0SAnup Sharma	comm: str
975aacd7f0SAnup Sharma	pid: int
985aacd7f0SAnup Sharma	tid: int
995aacd7f0SAnup Sharma	samples: List[Sample] = field(default_factory=list)
1005aacd7f0SAnup Sharma	frameTable: List[Frame] = field(default_factory=list)
1015aacd7f0SAnup Sharma	stringTable: List[str] = field(default_factory=list)
1025aacd7f0SAnup Sharma	stringMap: Dict[str, int] = field(default_factory=dict)
1035aacd7f0SAnup Sharma	stackTable: List[Stack] = field(default_factory=list)
1045aacd7f0SAnup Sharma	stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict)
1055aacd7f0SAnup Sharma	frameMap: Dict[str, int] = field(default_factory=dict)
1065aacd7f0SAnup Sharma
107*258dfd41SAnup Sharma	def _intern_stack(self, frame_id: int, prefix_id: Optional[int]) -> int:
108*258dfd41SAnup Sharma		"""Gets a matching stack, or saves the new stack. Returns a Stack ID."""
109*258dfd41SAnup Sharma		key = f"{frame_id}" if prefix_id is None else f"{frame_id},{prefix_id}"
110*258dfd41SAnup Sharma		# key = (prefix_id, frame_id)
111*258dfd41SAnup Sharma		stack_id = self.stackMap.get(key)
112*258dfd41SAnup Sharma		if stack_id is None:
113*258dfd41SAnup Sharma			# return stack_id
114*258dfd41SAnup Sharma			stack_id = len(self.stackTable)
115*258dfd41SAnup Sharma			self.stackTable.append(Stack(prefix_id=prefix_id, frame_id=frame_id))
116*258dfd41SAnup Sharma			self.stackMap[key] = stack_id
117*258dfd41SAnup Sharma		return stack_id
118*258dfd41SAnup Sharma
119*258dfd41SAnup Sharma	def _intern_string(self, string: str) -> int:
120*258dfd41SAnup Sharma		"""Gets a matching string, or saves the new string. Returns a String ID."""
121*258dfd41SAnup Sharma		string_id = self.stringMap.get(string)
122*258dfd41SAnup Sharma		if string_id is not None:
123*258dfd41SAnup Sharma			return string_id
124*258dfd41SAnup Sharma		string_id = len(self.stringTable)
125*258dfd41SAnup Sharma		self.stringTable.append(string)
126*258dfd41SAnup Sharma		self.stringMap[string] = string_id
127*258dfd41SAnup Sharma		return string_id
128*258dfd41SAnup Sharma
129*258dfd41SAnup Sharma	def _intern_frame(self, frame_str: str) -> int:
130*258dfd41SAnup Sharma		"""Gets a matching stack frame, or saves the new frame. Returns a Frame ID."""
131*258dfd41SAnup Sharma		frame_id = self.frameMap.get(frame_str)
132*258dfd41SAnup Sharma		if frame_id is not None:
133*258dfd41SAnup Sharma			return frame_id
134*258dfd41SAnup Sharma		frame_id = len(self.frameTable)
135*258dfd41SAnup Sharma		self.frameMap[frame_str] = frame_id
136*258dfd41SAnup Sharma		string_id = self._intern_string(frame_str)
137*258dfd41SAnup Sharma
138*258dfd41SAnup Sharma		symbol_name_to_category = KERNEL_CATEGORY_INDEX if frame_str.find('kallsyms') != -1 \
139*258dfd41SAnup Sharma		or frame_str.find('/vmlinux') != -1 \
140*258dfd41SAnup Sharma		or frame_str.endswith('.ko)') \
141*258dfd41SAnup Sharma		else USER_CATEGORY_INDEX
142*258dfd41SAnup Sharma
143*258dfd41SAnup Sharma		self.frameTable.append(Frame(
144*258dfd41SAnup Sharma			string_id=string_id,
145*258dfd41SAnup Sharma			relevantForJS=False,
146*258dfd41SAnup Sharma			innerWindowID=0,
147*258dfd41SAnup Sharma			implementation=None,
148*258dfd41SAnup Sharma			optimizations=None,
149*258dfd41SAnup Sharma			line=None,
150*258dfd41SAnup Sharma			column=None,
151*258dfd41SAnup Sharma			category=symbol_name_to_category,
152*258dfd41SAnup Sharma			subcategory=None,
153*258dfd41SAnup Sharma		))
154*258dfd41SAnup Sharma		return frame_id
155*258dfd41SAnup Sharma
1565aacd7f0SAnup Sharma	def _to_json_dict(self) -> Dict:
1575aacd7f0SAnup Sharma		"""Converts current Thread to GeckoThread JSON format."""
1585aacd7f0SAnup Sharma		# Gecko profile format is row-oriented data as List[List],
1595aacd7f0SAnup Sharma		# And a schema for interpreting each index.
1605aacd7f0SAnup Sharma		# Schema:
1615aacd7f0SAnup Sharma		# https://github.com/firefox-devtools/profiler/blob/main/docs-developer/gecko-profile-format.md
1625aacd7f0SAnup Sharma		# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L230
1635aacd7f0SAnup Sharma		return {
1645aacd7f0SAnup Sharma			"tid": self.tid,
1655aacd7f0SAnup Sharma			"pid": self.pid,
1665aacd7f0SAnup Sharma			"name": self.comm,
1675aacd7f0SAnup Sharma			# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L51
1685aacd7f0SAnup Sharma			"markers": {
1695aacd7f0SAnup Sharma				"schema": {
1705aacd7f0SAnup Sharma					"name": 0,
1715aacd7f0SAnup Sharma					"startTime": 1,
1725aacd7f0SAnup Sharma					"endTime": 2,
1735aacd7f0SAnup Sharma					"phase": 3,
1745aacd7f0SAnup Sharma					"category": 4,
1755aacd7f0SAnup Sharma					"data": 5,
1765aacd7f0SAnup Sharma				},
1775aacd7f0SAnup Sharma				"data": [],
1785aacd7f0SAnup Sharma			},
1795aacd7f0SAnup Sharma
1805aacd7f0SAnup Sharma			# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90
1815aacd7f0SAnup Sharma			"samples": {
1825aacd7f0SAnup Sharma				"schema": {
1835aacd7f0SAnup Sharma					"stack": 0,
1845aacd7f0SAnup Sharma					"time": 1,
1855aacd7f0SAnup Sharma					"responsiveness": 2,
1865aacd7f0SAnup Sharma				},
1875aacd7f0SAnup Sharma				"data": self.samples
1885aacd7f0SAnup Sharma			},
1895aacd7f0SAnup Sharma
1905aacd7f0SAnup Sharma			# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156
1915aacd7f0SAnup Sharma			"frameTable": {
1925aacd7f0SAnup Sharma				"schema": {
1935aacd7f0SAnup Sharma					"location": 0,
1945aacd7f0SAnup Sharma					"relevantForJS": 1,
1955aacd7f0SAnup Sharma					"innerWindowID": 2,
1965aacd7f0SAnup Sharma					"implementation": 3,
1975aacd7f0SAnup Sharma					"optimizations": 4,
1985aacd7f0SAnup Sharma					"line": 5,
1995aacd7f0SAnup Sharma					"column": 6,
2005aacd7f0SAnup Sharma					"category": 7,
2015aacd7f0SAnup Sharma					"subcategory": 8,
2025aacd7f0SAnup Sharma				},
2035aacd7f0SAnup Sharma				"data": self.frameTable,
2045aacd7f0SAnup Sharma			},
2055aacd7f0SAnup Sharma
2065aacd7f0SAnup Sharma			# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216
2075aacd7f0SAnup Sharma			"stackTable": {
2085aacd7f0SAnup Sharma				"schema": {
2095aacd7f0SAnup Sharma					"prefix": 0,
2105aacd7f0SAnup Sharma					"frame": 1,
2115aacd7f0SAnup Sharma				},
2125aacd7f0SAnup Sharma				"data": self.stackTable,
2135aacd7f0SAnup Sharma			},
2145aacd7f0SAnup Sharma			"stringTable": self.stringTable,
2155aacd7f0SAnup Sharma			"registerTime": 0,
2165aacd7f0SAnup Sharma			"unregisterTime": None,
2175aacd7f0SAnup Sharma			"processType": "default",
2185aacd7f0SAnup Sharma		}
2195aacd7f0SAnup Sharma
2201699d3efSAnup Sharma# Uses perf script python interface to parse each
2211699d3efSAnup Sharma# event and store the data in the thread builder.
2221699d3efSAnup Sharmadef process_event(param_dict: Dict) -> None:
2230a02e44cSAnup Sharma	global start_time
2240a02e44cSAnup Sharma	global tid_to_thread
2250a02e44cSAnup Sharma	time_stamp = (param_dict['sample']['time'] // 1000) / 1000
2260a02e44cSAnup Sharma	pid = param_dict['sample']['pid']
2270a02e44cSAnup Sharma	tid = param_dict['sample']['tid']
2280a02e44cSAnup Sharma	comm = param_dict['comm']
2290a02e44cSAnup Sharma
2300a02e44cSAnup Sharma	# Start time is the time of the first sample
2310a02e44cSAnup Sharma	if not start_time:
2320a02e44cSAnup Sharma		start_time = time_stamp
2331699d3efSAnup Sharma
2341699d3efSAnup Sharma# Trace_end runs at the end and will be used to aggregate
2351699d3efSAnup Sharma# the data into the final json object and print it out to stdout.
2361699d3efSAnup Sharmadef trace_end() -> None:
237833daec7SAnup Sharma	# Schema: https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L305
238833daec7SAnup Sharma	gecko_profile_with_meta = {
239833daec7SAnup Sharma		"meta": {
240833daec7SAnup Sharma			"interval": 1,
241833daec7SAnup Sharma			"processType": 0,
242833daec7SAnup Sharma			"product": PRODUCT,
243833daec7SAnup Sharma			"stackwalk": 1,
244833daec7SAnup Sharma			"debug": 0,
245833daec7SAnup Sharma			"gcpoison": 0,
246833daec7SAnup Sharma			"asyncstack": 1,
247833daec7SAnup Sharma			"startTime": start_time,
248833daec7SAnup Sharma			"shutdownTime": None,
249833daec7SAnup Sharma			"version": 24,
250833daec7SAnup Sharma			"presymbolicated": True,
251833daec7SAnup Sharma			"categories": CATEGORIES,
252833daec7SAnup Sharma			"markerSchema": [],
253833daec7SAnup Sharma			},
254833daec7SAnup Sharma		"libs": [],
255833daec7SAnup Sharma		# threads will be implemented in later commits.
256833daec7SAnup Sharma		# "threads": threads,
257833daec7SAnup Sharma		"processes": [],
258833daec7SAnup Sharma		"pausedRanges": [],
259833daec7SAnup Sharma	}
260833daec7SAnup Sharma	json.dump(gecko_profile_with_meta, sys.stdout, indent=2)
261833daec7SAnup Sharma
262833daec7SAnup Sharmadef main() -> None:
263833daec7SAnup Sharma	global CATEGORIES
264833daec7SAnup Sharma	parser = argparse.ArgumentParser(description="Convert perf.data to Firefox\'s Gecko Profile format")
265833daec7SAnup Sharma
266833daec7SAnup Sharma	# Add the command-line options
267833daec7SAnup Sharma	# Colors must be defined according to this:
268833daec7SAnup Sharma	# https://github.com/firefox-devtools/profiler/blob/50124adbfa488adba6e2674a8f2618cf34b59cd2/res/css/categories.css
269833daec7SAnup Sharma	parser.add_argument('--user-color', default='yellow', help='Color for the User category')
270833daec7SAnup Sharma	parser.add_argument('--kernel-color', default='orange', help='Color for the Kernel category')
271833daec7SAnup Sharma	# Parse the command-line arguments
272833daec7SAnup Sharma	args = parser.parse_args()
273833daec7SAnup Sharma	# Access the values provided by the user
274833daec7SAnup Sharma	user_color = args.user_color
275833daec7SAnup Sharma	kernel_color = args.kernel_color
276833daec7SAnup Sharma
277833daec7SAnup Sharma	CATEGORIES = [
278833daec7SAnup Sharma		{
279833daec7SAnup Sharma			"name": 'User',
280833daec7SAnup Sharma			"color": user_color,
281833daec7SAnup Sharma			"subcategories": ['Other']
282833daec7SAnup Sharma		},
283833daec7SAnup Sharma		{
284833daec7SAnup Sharma			"name": 'Kernel',
285833daec7SAnup Sharma			"color": kernel_color,
286833daec7SAnup Sharma			"subcategories": ['Other']
287833daec7SAnup Sharma		},
288833daec7SAnup Sharma	]
289833daec7SAnup Sharma
290833daec7SAnup Sharmaif __name__ == '__main__':
291833daec7SAnup Sharma    main()
292