xref: /linux-6.15/tools/perf/scripts/python/gecko.py (revision 2d889c6a)
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
16258dfd41SAnup 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*2d889c6aSAnup Sharma# Here key = tid, value = Thread
44*2d889c6aSAnup Sharmatid_to_thread = dict()
45*2d889c6aSAnup Sharma
46258dfd41SAnup Sharma# The category index is used by the profiler UI to show the color of the flame graph.
47258dfd41SAnup SharmaUSER_CATEGORY_INDEX = 0
48258dfd41SAnup SharmaKERNEL_CATEGORY_INDEX = 1
49258dfd41SAnup Sharma
505aacd7f0SAnup Sharma# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156
515aacd7f0SAnup Sharmaclass Frame(NamedTuple):
525aacd7f0SAnup Sharma	string_id: StringID
535aacd7f0SAnup Sharma	relevantForJS: bool
545aacd7f0SAnup Sharma	innerWindowID: int
555aacd7f0SAnup Sharma	implementation: None
565aacd7f0SAnup Sharma	optimizations: None
575aacd7f0SAnup Sharma	line: None
585aacd7f0SAnup Sharma	column: None
595aacd7f0SAnup Sharma	category: CategoryID
605aacd7f0SAnup Sharma	subcategory: int
615aacd7f0SAnup Sharma
625aacd7f0SAnup Sharma# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216
635aacd7f0SAnup Sharmaclass Stack(NamedTuple):
645aacd7f0SAnup Sharma	prefix_id: Optional[StackID]
655aacd7f0SAnup Sharma	frame_id: FrameID
665aacd7f0SAnup Sharma
675aacd7f0SAnup Sharma# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90
685aacd7f0SAnup Sharmaclass Sample(NamedTuple):
695aacd7f0SAnup Sharma	stack_id: Optional[StackID]
705aacd7f0SAnup Sharma	time_ms: Milliseconds
715aacd7f0SAnup Sharma	responsiveness: int
725aacd7f0SAnup Sharma
735aacd7f0SAnup Sharma@dataclass
745aacd7f0SAnup Sharmaclass Thread:
755aacd7f0SAnup Sharma	"""A builder for a profile of the thread.
765aacd7f0SAnup Sharma
775aacd7f0SAnup Sharma	Attributes:
785aacd7f0SAnup Sharma		comm: Thread command-line (name).
795aacd7f0SAnup Sharma		pid: process ID of containing process.
805aacd7f0SAnup Sharma		tid: thread ID.
815aacd7f0SAnup Sharma		samples: Timeline of profile samples.
825aacd7f0SAnup Sharma		frameTable: interned stack frame ID -> stack frame.
835aacd7f0SAnup Sharma		stringTable: interned string ID -> string.
845aacd7f0SAnup Sharma		stringMap: interned string -> string ID.
855aacd7f0SAnup Sharma		stackTable: interned stack ID -> stack.
865aacd7f0SAnup Sharma		stackMap: (stack prefix ID, leaf stack frame ID) -> interned Stack ID.
875aacd7f0SAnup Sharma		frameMap: Stack Frame string -> interned Frame ID.
885aacd7f0SAnup Sharma		comm: str
895aacd7f0SAnup Sharma		pid: int
905aacd7f0SAnup Sharma		tid: int
915aacd7f0SAnup Sharma		samples: List[Sample] = field(default_factory=list)
925aacd7f0SAnup Sharma		frameTable: List[Frame] = field(default_factory=list)
935aacd7f0SAnup Sharma		stringTable: List[str] = field(default_factory=list)
945aacd7f0SAnup Sharma		stringMap: Dict[str, int] = field(default_factory=dict)
955aacd7f0SAnup Sharma		stackTable: List[Stack] = field(default_factory=list)
965aacd7f0SAnup Sharma		stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict)
975aacd7f0SAnup Sharma		frameMap: Dict[str, int] = field(default_factory=dict)
985aacd7f0SAnup Sharma	"""
995aacd7f0SAnup Sharma	comm: str
1005aacd7f0SAnup Sharma	pid: int
1015aacd7f0SAnup Sharma	tid: int
1025aacd7f0SAnup Sharma	samples: List[Sample] = field(default_factory=list)
1035aacd7f0SAnup Sharma	frameTable: List[Frame] = field(default_factory=list)
1045aacd7f0SAnup Sharma	stringTable: List[str] = field(default_factory=list)
1055aacd7f0SAnup Sharma	stringMap: Dict[str, int] = field(default_factory=dict)
1065aacd7f0SAnup Sharma	stackTable: List[Stack] = field(default_factory=list)
1075aacd7f0SAnup Sharma	stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict)
1085aacd7f0SAnup Sharma	frameMap: Dict[str, int] = field(default_factory=dict)
1095aacd7f0SAnup Sharma
110258dfd41SAnup Sharma	def _intern_stack(self, frame_id: int, prefix_id: Optional[int]) -> int:
111258dfd41SAnup Sharma		"""Gets a matching stack, or saves the new stack. Returns a Stack ID."""
112258dfd41SAnup Sharma		key = f"{frame_id}" if prefix_id is None else f"{frame_id},{prefix_id}"
113258dfd41SAnup Sharma		# key = (prefix_id, frame_id)
114258dfd41SAnup Sharma		stack_id = self.stackMap.get(key)
115258dfd41SAnup Sharma		if stack_id is None:
116258dfd41SAnup Sharma			# return stack_id
117258dfd41SAnup Sharma			stack_id = len(self.stackTable)
118258dfd41SAnup Sharma			self.stackTable.append(Stack(prefix_id=prefix_id, frame_id=frame_id))
119258dfd41SAnup Sharma			self.stackMap[key] = stack_id
120258dfd41SAnup Sharma		return stack_id
121258dfd41SAnup Sharma
122258dfd41SAnup Sharma	def _intern_string(self, string: str) -> int:
123258dfd41SAnup Sharma		"""Gets a matching string, or saves the new string. Returns a String ID."""
124258dfd41SAnup Sharma		string_id = self.stringMap.get(string)
125258dfd41SAnup Sharma		if string_id is not None:
126258dfd41SAnup Sharma			return string_id
127258dfd41SAnup Sharma		string_id = len(self.stringTable)
128258dfd41SAnup Sharma		self.stringTable.append(string)
129258dfd41SAnup Sharma		self.stringMap[string] = string_id
130258dfd41SAnup Sharma		return string_id
131258dfd41SAnup Sharma
132258dfd41SAnup Sharma	def _intern_frame(self, frame_str: str) -> int:
133258dfd41SAnup Sharma		"""Gets a matching stack frame, or saves the new frame. Returns a Frame ID."""
134258dfd41SAnup Sharma		frame_id = self.frameMap.get(frame_str)
135258dfd41SAnup Sharma		if frame_id is not None:
136258dfd41SAnup Sharma			return frame_id
137258dfd41SAnup Sharma		frame_id = len(self.frameTable)
138258dfd41SAnup Sharma		self.frameMap[frame_str] = frame_id
139258dfd41SAnup Sharma		string_id = self._intern_string(frame_str)
140258dfd41SAnup Sharma
141258dfd41SAnup Sharma		symbol_name_to_category = KERNEL_CATEGORY_INDEX if frame_str.find('kallsyms') != -1 \
142258dfd41SAnup Sharma		or frame_str.find('/vmlinux') != -1 \
143258dfd41SAnup Sharma		or frame_str.endswith('.ko)') \
144258dfd41SAnup Sharma		else USER_CATEGORY_INDEX
145258dfd41SAnup Sharma
146258dfd41SAnup Sharma		self.frameTable.append(Frame(
147258dfd41SAnup Sharma			string_id=string_id,
148258dfd41SAnup Sharma			relevantForJS=False,
149258dfd41SAnup Sharma			innerWindowID=0,
150258dfd41SAnup Sharma			implementation=None,
151258dfd41SAnup Sharma			optimizations=None,
152258dfd41SAnup Sharma			line=None,
153258dfd41SAnup Sharma			column=None,
154258dfd41SAnup Sharma			category=symbol_name_to_category,
155258dfd41SAnup Sharma			subcategory=None,
156258dfd41SAnup Sharma		))
157258dfd41SAnup Sharma		return frame_id
158258dfd41SAnup Sharma
159*2d889c6aSAnup Sharma	def _add_sample(self, comm: str, stack: List[str], time_ms: Milliseconds) -> None:
160*2d889c6aSAnup Sharma		"""Add a timestamped stack trace sample to the thread builder.
161*2d889c6aSAnup Sharma		Args:
162*2d889c6aSAnup Sharma			comm: command-line (name) of the thread at this sample
163*2d889c6aSAnup Sharma			stack: sampled stack frames. Root first, leaf last.
164*2d889c6aSAnup Sharma			time_ms: timestamp of sample in milliseconds.
165*2d889c6aSAnup Sharma		"""
166*2d889c6aSAnup Sharma		# Ihreads may not set their names right after they are created.
167*2d889c6aSAnup Sharma		# Instead, they might do it later. In such situations, to use the latest name they have set.
168*2d889c6aSAnup Sharma		if self.comm != comm:
169*2d889c6aSAnup Sharma			self.comm = comm
170*2d889c6aSAnup Sharma
171*2d889c6aSAnup Sharma		prefix_stack_id = reduce(lambda prefix_id, frame: self._intern_stack
172*2d889c6aSAnup Sharma						(self._intern_frame(frame), prefix_id), stack, None)
173*2d889c6aSAnup Sharma		if prefix_stack_id is not None:
174*2d889c6aSAnup Sharma			self.samples.append(Sample(stack_id=prefix_stack_id,
175*2d889c6aSAnup Sharma									time_ms=time_ms,
176*2d889c6aSAnup Sharma									responsiveness=0))
177*2d889c6aSAnup Sharma
1785aacd7f0SAnup Sharma	def _to_json_dict(self) -> Dict:
1795aacd7f0SAnup Sharma		"""Converts current Thread to GeckoThread JSON format."""
1805aacd7f0SAnup Sharma		# Gecko profile format is row-oriented data as List[List],
1815aacd7f0SAnup Sharma		# And a schema for interpreting each index.
1825aacd7f0SAnup Sharma		# Schema:
1835aacd7f0SAnup Sharma		# https://github.com/firefox-devtools/profiler/blob/main/docs-developer/gecko-profile-format.md
1845aacd7f0SAnup Sharma		# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L230
1855aacd7f0SAnup Sharma		return {
1865aacd7f0SAnup Sharma			"tid": self.tid,
1875aacd7f0SAnup Sharma			"pid": self.pid,
1885aacd7f0SAnup Sharma			"name": self.comm,
1895aacd7f0SAnup Sharma			# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L51
1905aacd7f0SAnup Sharma			"markers": {
1915aacd7f0SAnup Sharma				"schema": {
1925aacd7f0SAnup Sharma					"name": 0,
1935aacd7f0SAnup Sharma					"startTime": 1,
1945aacd7f0SAnup Sharma					"endTime": 2,
1955aacd7f0SAnup Sharma					"phase": 3,
1965aacd7f0SAnup Sharma					"category": 4,
1975aacd7f0SAnup Sharma					"data": 5,
1985aacd7f0SAnup Sharma				},
1995aacd7f0SAnup Sharma				"data": [],
2005aacd7f0SAnup Sharma			},
2015aacd7f0SAnup Sharma
2025aacd7f0SAnup Sharma			# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90
2035aacd7f0SAnup Sharma			"samples": {
2045aacd7f0SAnup Sharma				"schema": {
2055aacd7f0SAnup Sharma					"stack": 0,
2065aacd7f0SAnup Sharma					"time": 1,
2075aacd7f0SAnup Sharma					"responsiveness": 2,
2085aacd7f0SAnup Sharma				},
2095aacd7f0SAnup Sharma				"data": self.samples
2105aacd7f0SAnup Sharma			},
2115aacd7f0SAnup Sharma
2125aacd7f0SAnup Sharma			# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156
2135aacd7f0SAnup Sharma			"frameTable": {
2145aacd7f0SAnup Sharma				"schema": {
2155aacd7f0SAnup Sharma					"location": 0,
2165aacd7f0SAnup Sharma					"relevantForJS": 1,
2175aacd7f0SAnup Sharma					"innerWindowID": 2,
2185aacd7f0SAnup Sharma					"implementation": 3,
2195aacd7f0SAnup Sharma					"optimizations": 4,
2205aacd7f0SAnup Sharma					"line": 5,
2215aacd7f0SAnup Sharma					"column": 6,
2225aacd7f0SAnup Sharma					"category": 7,
2235aacd7f0SAnup Sharma					"subcategory": 8,
2245aacd7f0SAnup Sharma				},
2255aacd7f0SAnup Sharma				"data": self.frameTable,
2265aacd7f0SAnup Sharma			},
2275aacd7f0SAnup Sharma
2285aacd7f0SAnup Sharma			# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216
2295aacd7f0SAnup Sharma			"stackTable": {
2305aacd7f0SAnup Sharma				"schema": {
2315aacd7f0SAnup Sharma					"prefix": 0,
2325aacd7f0SAnup Sharma					"frame": 1,
2335aacd7f0SAnup Sharma				},
2345aacd7f0SAnup Sharma				"data": self.stackTable,
2355aacd7f0SAnup Sharma			},
2365aacd7f0SAnup Sharma			"stringTable": self.stringTable,
2375aacd7f0SAnup Sharma			"registerTime": 0,
2385aacd7f0SAnup Sharma			"unregisterTime": None,
2395aacd7f0SAnup Sharma			"processType": "default",
2405aacd7f0SAnup Sharma		}
2415aacd7f0SAnup Sharma
2421699d3efSAnup Sharma# Uses perf script python interface to parse each
2431699d3efSAnup Sharma# event and store the data in the thread builder.
2441699d3efSAnup Sharmadef process_event(param_dict: Dict) -> None:
2450a02e44cSAnup Sharma	global start_time
2460a02e44cSAnup Sharma	global tid_to_thread
2470a02e44cSAnup Sharma	time_stamp = (param_dict['sample']['time'] // 1000) / 1000
2480a02e44cSAnup Sharma	pid = param_dict['sample']['pid']
2490a02e44cSAnup Sharma	tid = param_dict['sample']['tid']
2500a02e44cSAnup Sharma	comm = param_dict['comm']
2510a02e44cSAnup Sharma
2520a02e44cSAnup Sharma	# Start time is the time of the first sample
2530a02e44cSAnup Sharma	if not start_time:
2540a02e44cSAnup Sharma		start_time = time_stamp
2551699d3efSAnup Sharma
256*2d889c6aSAnup Sharma	# Parse and append the callchain of the current sample into a stack.
257*2d889c6aSAnup Sharma	stack = []
258*2d889c6aSAnup Sharma	if param_dict['callchain']:
259*2d889c6aSAnup Sharma		for call in param_dict['callchain']:
260*2d889c6aSAnup Sharma			if 'sym' not in call:
261*2d889c6aSAnup Sharma				continue
262*2d889c6aSAnup Sharma			stack.append(f'{call["sym"]["name"]} (in {call["dso"]})')
263*2d889c6aSAnup Sharma		if len(stack) != 0:
264*2d889c6aSAnup Sharma			# Reverse the stack, as root come first and the leaf at the end.
265*2d889c6aSAnup Sharma			stack = stack[::-1]
266*2d889c6aSAnup Sharma
267*2d889c6aSAnup Sharma	# During perf record if -g is not used, the callchain is not available.
268*2d889c6aSAnup Sharma	# In that case, the symbol and dso are available in the event parameters.
269*2d889c6aSAnup Sharma	else:
270*2d889c6aSAnup Sharma		func = param_dict['symbol'] if 'symbol' in param_dict else '[unknown]'
271*2d889c6aSAnup Sharma		dso = param_dict['dso'] if 'dso' in param_dict else '[unknown]'
272*2d889c6aSAnup Sharma		stack.append(f'{func} (in {dso})')
273*2d889c6aSAnup Sharma
274*2d889c6aSAnup Sharma	# Add sample to the specific thread.
275*2d889c6aSAnup Sharma	thread = tid_to_thread.get(tid)
276*2d889c6aSAnup Sharma	if thread is None:
277*2d889c6aSAnup Sharma		thread = Thread(comm=comm, pid=pid, tid=tid)
278*2d889c6aSAnup Sharma		tid_to_thread[tid] = thread
279*2d889c6aSAnup Sharma	thread._add_sample(comm=comm, stack=stack, time_ms=time_stamp)
280*2d889c6aSAnup Sharma
2811699d3efSAnup Sharma# Trace_end runs at the end and will be used to aggregate
2821699d3efSAnup Sharma# the data into the final json object and print it out to stdout.
2831699d3efSAnup Sharmadef trace_end() -> None:
284*2d889c6aSAnup Sharma	threads = [thread._to_json_dict() for thread in tid_to_thread.values()]
285*2d889c6aSAnup Sharma
286833daec7SAnup Sharma	# Schema: https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L305
287833daec7SAnup Sharma	gecko_profile_with_meta = {
288833daec7SAnup Sharma		"meta": {
289833daec7SAnup Sharma			"interval": 1,
290833daec7SAnup Sharma			"processType": 0,
291833daec7SAnup Sharma			"product": PRODUCT,
292833daec7SAnup Sharma			"stackwalk": 1,
293833daec7SAnup Sharma			"debug": 0,
294833daec7SAnup Sharma			"gcpoison": 0,
295833daec7SAnup Sharma			"asyncstack": 1,
296833daec7SAnup Sharma			"startTime": start_time,
297833daec7SAnup Sharma			"shutdownTime": None,
298833daec7SAnup Sharma			"version": 24,
299833daec7SAnup Sharma			"presymbolicated": True,
300833daec7SAnup Sharma			"categories": CATEGORIES,
301833daec7SAnup Sharma			"markerSchema": [],
302833daec7SAnup Sharma			},
303833daec7SAnup Sharma		"libs": [],
304*2d889c6aSAnup Sharma		"threads": threads,
305833daec7SAnup Sharma		"processes": [],
306833daec7SAnup Sharma		"pausedRanges": [],
307833daec7SAnup Sharma	}
308833daec7SAnup Sharma	json.dump(gecko_profile_with_meta, sys.stdout, indent=2)
309833daec7SAnup Sharma
310833daec7SAnup Sharmadef main() -> None:
311833daec7SAnup Sharma	global CATEGORIES
312833daec7SAnup Sharma	parser = argparse.ArgumentParser(description="Convert perf.data to Firefox\'s Gecko Profile format")
313833daec7SAnup Sharma
314833daec7SAnup Sharma	# Add the command-line options
315833daec7SAnup Sharma	# Colors must be defined according to this:
316833daec7SAnup Sharma	# https://github.com/firefox-devtools/profiler/blob/50124adbfa488adba6e2674a8f2618cf34b59cd2/res/css/categories.css
317833daec7SAnup Sharma	parser.add_argument('--user-color', default='yellow', help='Color for the User category')
318833daec7SAnup Sharma	parser.add_argument('--kernel-color', default='orange', help='Color for the Kernel category')
319833daec7SAnup Sharma	# Parse the command-line arguments
320833daec7SAnup Sharma	args = parser.parse_args()
321833daec7SAnup Sharma	# Access the values provided by the user
322833daec7SAnup Sharma	user_color = args.user_color
323833daec7SAnup Sharma	kernel_color = args.kernel_color
324833daec7SAnup Sharma
325833daec7SAnup Sharma	CATEGORIES = [
326833daec7SAnup Sharma		{
327833daec7SAnup Sharma			"name": 'User',
328833daec7SAnup Sharma			"color": user_color,
329833daec7SAnup Sharma			"subcategories": ['Other']
330833daec7SAnup Sharma		},
331833daec7SAnup Sharma		{
332833daec7SAnup Sharma			"name": 'Kernel',
333833daec7SAnup Sharma			"color": kernel_color,
334833daec7SAnup Sharma			"subcategories": ['Other']
335833daec7SAnup Sharma		},
336833daec7SAnup Sharma	]
337833daec7SAnup Sharma
338833daec7SAnup Sharmaif __name__ == '__main__':
339833daec7SAnup Sharma    main()
340