• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3# Copyright (C) 2021 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18"""gecko_profile_generator.py: converts perf.data to Gecko Profile Format,
19    which can be read by https://profiler.firefox.com/.
20
21  Example:
22    ./app_profiler.py
23    ./gecko_profile_generator.py | gzip > gecko-profile.json.gz
24
25  Then open gecko-profile.json.gz in https://profiler.firefox.com/
26"""
27
28import json
29import sys
30
31from dataclasses import dataclass, field
32from simpleperf_report_lib import ReportLib
33from simpleperf_utils import BaseArgumentParser, flatten_arg_list, ReportLibOptions
34from typing import List, Dict, Optional, NamedTuple, Set, Tuple
35
36
37StringID = int
38StackID = int
39FrameID = int
40CategoryID = int
41Milliseconds = float
42GeckoProfile = Dict
43
44
45# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156
46class Frame(NamedTuple):
47    string_id: StringID
48    relevantForJS: bool
49    innerWindowID: int
50    implementation: None
51    optimizations: None
52    line: None
53    column: None
54    category: CategoryID
55    subcategory: int
56
57
58# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216
59class Stack(NamedTuple):
60    prefix_id: Optional[StackID]
61    frame_id: FrameID
62    category_id: CategoryID
63
64
65# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90
66class Sample(NamedTuple):
67    stack_id: Optional[StackID]
68    time_ms: Milliseconds
69    responsiveness: int
70
71
72# Schema: https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/profile.js#L425
73# Colors must be defined in:
74# https://github.com/firefox-devtools/profiler/blob/50124adbfa488adba6e2674a8f2618cf34b59cd2/res/css/categories.css
75CATEGORIES = [
76    {
77        "name": 'User',
78        # Follow Brendan Gregg's Flamegraph convention: yellow for userland
79        # https://github.com/brendangregg/FlameGraph/blob/810687f180f3c4929b5d965f54817a5218c9d89b/flamegraph.pl#L419
80        "color": 'yellow',
81        "subcategories": ['Other']
82    },
83    {
84        "name": 'Kernel',
85        # Follow Brendan Gregg's Flamegraph convention: orange for kernel
86        # https://github.com/brendangregg/FlameGraph/blob/810687f180f3c4929b5d965f54817a5218c9d89b/flamegraph.pl#L417
87        "color": 'orange',
88        "subcategories": ['Other']
89    },
90    {
91        "name": 'Native',
92        # Follow Brendan Gregg's Flamegraph convention: yellow for userland
93        # https://github.com/brendangregg/FlameGraph/blob/810687f180f3c4929b5d965f54817a5218c9d89b/flamegraph.pl#L419
94        "color": 'yellow',
95        "subcategories": ['Other']
96    },
97    {
98        "name": 'DEX',
99        # Follow Brendan Gregg's Flamegraph convention: green for Java/JIT
100        # https://github.com/brendangregg/FlameGraph/blob/810687f180f3c4929b5d965f54817a5218c9d89b/flamegraph.pl#L411
101        "color": 'green',
102        "subcategories": ['Other']
103    },
104    {
105        "name": 'OAT',
106        # Follow Brendan Gregg's Flamegraph convention: green for Java/JIT
107        # https://github.com/brendangregg/FlameGraph/blob/810687f180f3c4929b5d965f54817a5218c9d89b/flamegraph.pl#L411
108        "color": 'green',
109        "subcategories": ['Other']
110    },
111    # Not used by this exporter yet, but some Firefox Profiler code assumes
112    # there is an 'Other' category by searching for a category with
113    # color=grey, so include this.
114    {
115        "name": 'Other',
116        "color": 'grey',
117        "subcategories": ['Other']
118    },
119]
120
121
122@dataclass
123class Thread:
124    """A builder for a profile of a single thread.
125
126    Attributes:
127      comm: Thread command-line (name).
128      pid: process ID of containing process.
129      tid: thread ID.
130      samples: Timeline of profile samples.
131      frameTable: interned stack frame ID -> stack frame.
132      stringTable: interned string ID -> string.
133      stringMap: interned string -> string ID.
134      stackTable: interned stack ID -> stack.
135      stackMap: (stack prefix ID, leaf stack frame ID) -> interned Stack ID.
136      frameMap: Stack Frame string -> interned Frame ID.
137    """
138    comm: str
139    pid: int
140    tid: int
141    samples: List[Sample] = field(default_factory=list)
142    frameTable: List[Frame] = field(default_factory=list)
143    stringTable: List[str] = field(default_factory=list)
144    # TODO: this is redundant with frameTable, could we remove this?
145    stringMap: Dict[str, int] = field(default_factory=dict)
146    stackTable: List[Stack] = field(default_factory=list)
147    stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict)
148    frameMap: Dict[str, int] = field(default_factory=dict)
149
150    def _intern_stack(self, frame_id: int, prefix_id: Optional[int]) -> int:
151        """Gets a matching stack, or saves the new stack. Returns a Stack ID."""
152        key = (prefix_id, frame_id)
153        stack_id = self.stackMap.get(key)
154        if stack_id is not None:
155            return stack_id
156        stack_id = len(self.stackTable)
157        self.stackTable.append(Stack(prefix_id=prefix_id,
158                                     frame_id=frame_id,
159                                     category_id=0))
160        self.stackMap[key] = stack_id
161        return stack_id
162
163    def _intern_string(self, string: str) -> int:
164        """Gets a matching string, or saves the new string. Returns a String ID."""
165        string_id = self.stringMap.get(string)
166        if string_id is not None:
167            return string_id
168        string_id = len(self.stringTable)
169        self.stringTable.append(string)
170        self.stringMap[string] = string_id
171        return string_id
172
173    def _intern_frame(self, frame_str: str) -> int:
174        """Gets a matching stack frame, or saves the new frame. Returns a Frame ID."""
175        frame_id = self.frameMap.get(frame_str)
176        if frame_id is not None:
177            return frame_id
178        frame_id = len(self.frameTable)
179        self.frameMap[frame_str] = frame_id
180        string_id = self._intern_string(frame_str)
181
182        category = 0
183        # Heuristic: kernel code contains "kallsyms" as the library name.
184        if "kallsyms" in frame_str or ".ko" in frame_str:
185            category = 1
186        elif ".so" in frame_str:
187            category = 2
188        elif ".vdex" in frame_str:
189            category = 3
190        elif ".oat" in frame_str:
191            category = 4
192
193        self.frameTable.append(Frame(
194            string_id=string_id,
195            relevantForJS=False,
196            innerWindowID=0,
197            implementation=None,
198            optimizations=None,
199            line=None,
200            column=None,
201            category=category,
202            subcategory=0,
203        ))
204        return frame_id
205
206    def _add_sample(self, comm: str, stack: List[str], time_ms: Milliseconds) -> None:
207        """Add a timestamped stack trace sample to the thread builder.
208
209        Args:
210          comm: command-line (name) of the thread at this sample
211          stack: sampled stack frames. Root first, leaf last.
212          time_ms: timestamp of sample in milliseconds
213        """
214        # Unix threads often don't set their name immediately upon creation.
215        # Use the last name
216        if self.comm != comm:
217            self.comm = comm
218
219        prefix_stack_id = None
220        for frame in stack:
221            frame_id = self._intern_frame(frame)
222            prefix_stack_id = self._intern_stack(frame_id, prefix_stack_id)
223
224        self.samples.append(Sample(stack_id=prefix_stack_id,
225                                   time_ms=time_ms,
226                                   responsiveness=0))
227
228    def _to_json_dict(self) -> Dict:
229        """Converts this Thread to GeckoThread JSON format."""
230        # The samples aren't guaranteed to be in order. Sort them by time.
231        self.samples.sort(key=lambda s: s.time_ms)
232
233        # Gecko profile format is row-oriented data as List[List],
234        # And a schema for interpreting each index.
235        # Schema:
236        # https://github.com/firefox-devtools/profiler/blob/main/docs-developer/gecko-profile-format.md
237        # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L230
238        return {
239            "tid": self.tid,
240            "pid": self.pid,
241            "name": self.comm,
242            # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L51
243            "markers": {
244                "schema": {
245                    "name": 0,
246                    "startTime": 1,
247                    "endTime": 2,
248                    "phase": 3,
249                    "category": 4,
250                    "data": 5,
251                },
252                "data": [],
253            },
254            # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90
255            "samples": {
256                "schema": {
257                    "stack": 0,
258                    "time": 1,
259                    "responsiveness": 2,
260                },
261                "data": self.samples
262            },
263            # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156
264            "frameTable": {
265                "schema": {
266                    "location": 0,
267                    "relevantForJS": 1,
268                    "innerWindowID": 2,
269                    "implementation": 3,
270                    "optimizations": 4,
271                    "line": 5,
272                    "column": 6,
273                    "category": 7,
274                    "subcategory": 8,
275                },
276                "data": self.frameTable,
277            },
278            # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216
279            "stackTable": {
280                "schema": {
281                    "prefix": 0,
282                    "frame": 1,
283                    "category": 2,
284                },
285                "data": self.stackTable,
286            },
287            "stringTable": self.stringTable,
288            "registerTime": 0,
289            "unregisterTime": None,
290            "processType": "default",
291        }
292
293
294def _gecko_profile(
295        record_file: str,
296        symfs_dir: Optional[str],
297        kallsyms_file: Optional[str],
298        report_lib_options: ReportLibOptions) -> GeckoProfile:
299    """convert a simpleperf profile to gecko format"""
300    lib = ReportLib()
301
302    lib.ShowIpForUnknownSymbol()
303    if symfs_dir is not None:
304        lib.SetSymfs(symfs_dir)
305    lib.SetRecordFile(record_file)
306    if kallsyms_file is not None:
307        lib.SetKallsymsFile(kallsyms_file)
308    lib.SetReportOptions(report_lib_options)
309
310    arch = lib.GetArch()
311    meta_info = lib.MetaInfo()
312    record_cmd = lib.GetRecordCmd()
313
314    # Map from tid to Thread
315    threadMap: Dict[int, Thread] = {}
316
317    while True:
318        sample = lib.GetNextSample()
319        if sample is None:
320            lib.Close()
321            break
322        event = lib.GetEventOfCurrentSample()
323        symbol = lib.GetSymbolOfCurrentSample()
324        callchain = lib.GetCallChainOfCurrentSample()
325        sample_time_ms = sample.time / 1000000
326
327        stack = ['%s (in %s)' % (symbol.symbol_name, symbol.dso_name)]
328        for i in range(callchain.nr):
329            entry = callchain.entries[i]
330            stack.append('%s (in %s)' % (entry.symbol.symbol_name, entry.symbol.dso_name))
331        # We want root first, leaf last.
332        stack.reverse()
333
334        # add thread sample
335        thread = threadMap.get(sample.tid)
336        if thread is None:
337            thread = Thread(comm=sample.thread_comm, pid=sample.pid, tid=sample.tid)
338            threadMap[sample.tid] = thread
339        thread._add_sample(
340            comm=sample.thread_comm,
341            stack=stack,
342            # We are being a bit fast and loose here with time here.  simpleperf
343            # uses CLOCK_MONOTONIC by default, which doesn't use the normal unix
344            # epoch, but rather some arbitrary time. In practice, this doesn't
345            # matter, the Firefox Profiler normalises all the timestamps to begin at
346            # the minimum time.  Consider fixing this in future, if needed, by
347            # setting `simpleperf record --clockid realtime`.
348            time_ms=sample_time_ms)
349
350    threads = [thread._to_json_dict() for thread in threadMap.values()]
351
352    profile_timestamp = meta_info.get('timestamp')
353    end_time_ms = (int(profile_timestamp) * 1000) if profile_timestamp else 0
354
355    # Schema: https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L305
356    gecko_profile_meta = {
357        "interval": 1,
358        "processType": 0,
359        "product": record_cmd,
360        "device": meta_info.get("product_props"),
361        "platform": meta_info.get("android_build_fingerprint"),
362        "stackwalk": 1,
363        "debug": 0,
364        "gcpoison": 0,
365        "asyncstack": 1,
366        # The profile timestamp is actually the end time, not the start time.
367        # This is close enough for our purposes; I mostly just want to know which
368        # day the profile was taken! Consider fixing this in future, if needed,
369        # by setting `simpleperf record --clockid realtime` and taking the minimum
370        # sample time.
371        "startTime": end_time_ms,
372        "shutdownTime": None,
373        "version": 24,
374        "presymbolicated": True,
375        "categories": CATEGORIES,
376        "markerSchema": [],
377        "abi": arch,
378        "oscpu": meta_info.get("android_build_fingerprint"),
379    }
380
381    # Schema:
382    # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L377
383    # https://github.com/firefox-devtools/profiler/blob/main/docs-developer/gecko-profile-format.md
384    return {
385        "meta": gecko_profile_meta,
386        "libs": [],
387        "threads": threads,
388        "processes": [],
389        "pausedRanges": [],
390    }
391
392
393def main() -> None:
394    parser = BaseArgumentParser(description=__doc__)
395    parser.add_argument('--symfs',
396                        help='Set the path to find binaries with symbols and debug info.')
397    parser.add_argument('--kallsyms', help='Set the path to find kernel symbols.')
398    parser.add_argument('-i', '--record_file', nargs='?', default='perf.data',
399                        help='Default is perf.data.')
400    parser.add_report_lib_options()
401    args = parser.parse_args()
402    profile = _gecko_profile(
403        record_file=args.record_file,
404        symfs_dir=args.symfs,
405        kallsyms_file=args.kallsyms,
406        report_lib_options=args.report_lib_options)
407
408    json.dump(profile, sys.stdout, sort_keys=True)
409
410
411if __name__ == '__main__':
412    main()
413