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