1#
2# Copyright (C) 2016 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16
17
18class CallSite(object):
19
20    def __init__(self, method, dso):
21        self.method = method
22        self.dso = dso
23
24
25class Thread(object):
26
27    def __init__(self, tid, pid):
28        self.tid = tid
29        self.pid = pid
30        self.name = ""
31        self.samples = []
32        self.flamegraph = FlameGraphCallSite("root", "", 0)
33        self.num_samples = 0
34        self.num_events = 0
35
36    def add_callchain(self, callchain, symbol, sample):
37        self.name = sample.thread_comm
38        self.num_samples += 1
39        self.num_events += sample.period
40        chain = []
41        for j in range(callchain.nr):
42            entry = callchain.entries[callchain.nr - j - 1]
43            if entry.ip == 0:
44                continue
45            chain.append(CallSite(entry.symbol.symbol_name, entry.symbol.dso_name))
46
47        chain.append(CallSite(symbol.symbol_name, symbol.dso_name))
48        self.flamegraph.add_callchain(chain, sample.period)
49
50
51class Process(object):
52
53    def __init__(self, name, pid):
54        self.name = name
55        self.pid = pid
56        self.threads = {}
57        self.cmd = ""
58        self.props = {}
59        # num_samples is the count of samples recorded in the profiling file.
60        self.num_samples = 0
61        # num_events is the count of events contained in all samples. Each sample contains a
62        # count of events happened since last sample. If we use cpu-cycles event, the count
63        # shows how many cpu-cycles have happened during recording.
64        self.num_events = 0
65
66    def get_thread(self, tid, pid):
67        thread = self.threads.get(tid)
68        if thread is None:
69            thread = self.threads[tid] = Thread(tid, pid)
70        return thread
71
72    def add_sample(self, sample, symbol, callchain):
73        thread = self.get_thread(sample.tid, sample.pid)
74        thread.add_callchain(callchain, symbol, sample)
75        self.num_samples += 1
76        # sample.period is the count of events happened since last sample.
77        self.num_events += sample.period
78
79
80class FlameGraphCallSite(object):
81
82    callsite_counter = 0
83    @classmethod
84    def _get_next_callsite_id(cls):
85        cls.callsite_counter += 1
86        return cls.callsite_counter
87
88    def __init__(self, method, dso, callsite_id):
89        # map from (dso, method) to FlameGraphCallSite. Used to speed up add_callchain().
90        self.child_dict = {}
91        self.children = []
92        self.method = method
93        self.dso = dso
94        self.num_events = 0
95        self.offset = 0  # Offset allows position nodes in different branches.
96        self.id = callsite_id
97
98    def weight(self):
99        return float(self.num_events)
100
101    def add_callchain(self, chain, num_events):
102        self.num_events += num_events
103        current = self
104        for callsite in chain:
105            current = current.get_child(callsite)
106            current.num_events += num_events
107
108    def get_child(self, callsite):
109        key = (callsite.dso, callsite.method)
110        child = self.child_dict.get(key)
111        if child is None:
112            child = self.child_dict[key] = FlameGraphCallSite(callsite.method, callsite.dso,
113                                                              self._get_next_callsite_id())
114        return child
115
116    def trim_callchain(self, min_num_events, max_depth, depth=0):
117        """ Remove call sites with num_events < min_num_events in the subtree.
118            Remaining children are collected in a list.
119        """
120        if depth <= max_depth:
121            for key in self.child_dict:
122                child = self.child_dict[key]
123                if child.num_events >= min_num_events:
124                    child.trim_callchain(min_num_events, max_depth, depth + 1)
125                    self.children.append(child)
126        # Relese child_dict since it will not be used.
127        self.child_dict = None
128
129    def get_max_depth(self):
130        return max([c.get_max_depth() for c in self.children]) + 1 if self.children else 1
131
132    def generate_offset(self, start_offset):
133        self.offset = start_offset
134        child_offset = start_offset
135        for child in self.children:
136            child_offset = child.generate_offset(child_offset)
137        return self.offset + self.num_events
138