diff options
Diffstat (limited to 'scripts/pybootchartgui/pybootchartgui/process_tree.py')
-rw-r--r-- | scripts/pybootchartgui/pybootchartgui/process_tree.py | 270 |
1 files changed, 270 insertions, 0 deletions
diff --git a/scripts/pybootchartgui/pybootchartgui/process_tree.py b/scripts/pybootchartgui/pybootchartgui/process_tree.py new file mode 100644 index 0000000000..bde29ebda8 --- /dev/null +++ b/scripts/pybootchartgui/pybootchartgui/process_tree.py | |||
@@ -0,0 +1,270 @@ | |||
1 | class ProcessTree: | ||
2 | """ProcessTree encapsulates a process tree. The tree is built from log files | ||
3 | retrieved during the boot process. When building the process tree, it is | ||
4 | pruned and merged in order to be able to visualize it in a comprehensible | ||
5 | manner. | ||
6 | |||
7 | The following pruning techniques are used: | ||
8 | |||
9 | * idle processes that keep running during the last process sample | ||
10 | (which is a heuristic for a background processes) are removed, | ||
11 | * short-lived processes (i.e. processes that only live for the | ||
12 | duration of two samples or less) are removed, | ||
13 | * the processes used by the boot logger are removed, | ||
14 | * exploders (i.e. processes that are known to spawn huge meaningless | ||
15 | process subtrees) have their subtrees merged together, | ||
16 | * siblings (i.e. processes with the same command line living | ||
17 | concurrently -- thread heuristic) are merged together, | ||
18 | * process runs (unary trees with processes sharing the command line) | ||
19 | are merged together. | ||
20 | |||
21 | """ | ||
22 | LOGGER_PROC = 'bootchartd' | ||
23 | EXPLODER_PROCESSES = set(['hwup']) | ||
24 | |||
25 | def __init__(self, psstats, monitoredApp, prune, for_testing = False): | ||
26 | self.process_tree = [] | ||
27 | self.psstats = psstats | ||
28 | self.process_list = sorted(psstats.process_list, key = lambda p: p.pid) | ||
29 | self.sample_period = psstats.sample_period | ||
30 | |||
31 | self.build() | ||
32 | self.update_ppids_for_daemons(self.process_list) | ||
33 | |||
34 | self.start_time = self.get_start_time(self.process_tree) | ||
35 | self.end_time = self.get_end_time(self.process_tree) | ||
36 | self.duration = self.end_time - self.start_time | ||
37 | |||
38 | if for_testing: | ||
39 | return | ||
40 | |||
41 | # print 'proc_tree before prune: num_proc=%i, duration=%i' % (self.num_nodes(self.process_list), self.duration) | ||
42 | |||
43 | removed = self.merge_logger(self.process_tree, self.LOGGER_PROC, monitoredApp, False) | ||
44 | print "Merged %i logger processes" % removed | ||
45 | |||
46 | if prune: | ||
47 | removed = self.prune(self.process_tree, None) | ||
48 | print "Pruned %i processes" % removed | ||
49 | removed = self.merge_exploders(self.process_tree, self.EXPLODER_PROCESSES) | ||
50 | print "Pruned %i exploders" % removed | ||
51 | removed = self.merge_siblings(self.process_tree) | ||
52 | print "Pruned %i threads" % removed | ||
53 | removed = self.merge_runs(self.process_tree) | ||
54 | print "Pruned %i runs" % removed | ||
55 | |||
56 | self.sort(self.process_tree) | ||
57 | |||
58 | self.start_time = self.get_start_time(self.process_tree) | ||
59 | self.end_time = self.get_end_time(self.process_tree) | ||
60 | self.duration = self.end_time - self.start_time | ||
61 | |||
62 | self.num_proc = self.num_nodes(self.process_tree) | ||
63 | |||
64 | def build(self): | ||
65 | """Build the process tree from the list of top samples.""" | ||
66 | self.process_tree = [] | ||
67 | for proc in self.process_list: | ||
68 | if not proc.parent: | ||
69 | self.process_tree.append(proc) | ||
70 | else: | ||
71 | proc.parent.child_list.append(proc) | ||
72 | |||
73 | def sort(self, process_subtree): | ||
74 | """Sort process tree.""" | ||
75 | for p in process_subtree: | ||
76 | p.child_list.sort(key = lambda p: p.pid) | ||
77 | self.sort(p.child_list) | ||
78 | |||
79 | def num_nodes(self, process_list): | ||
80 | "Counts the number of nodes in the specified process tree.""" | ||
81 | nodes = 0 | ||
82 | for proc in process_list: | ||
83 | nodes = nodes + self.num_nodes(proc.child_list) | ||
84 | return nodes + len(process_list) | ||
85 | |||
86 | def get_start_time(self, process_subtree): | ||
87 | """Returns the start time of the process subtree. This is the start | ||
88 | time of the earliest process. | ||
89 | |||
90 | """ | ||
91 | if not process_subtree: | ||
92 | return 100000000; | ||
93 | return min( [min(proc.start_time, self.get_start_time(proc.child_list)) for proc in process_subtree] ) | ||
94 | |||
95 | def get_end_time(self, process_subtree): | ||
96 | """Returns the end time of the process subtree. This is the end time | ||
97 | of the last collected sample. | ||
98 | |||
99 | """ | ||
100 | if not process_subtree: | ||
101 | return -100000000; | ||
102 | return max( [max(proc.start_time + proc.duration, self.get_end_time(proc.child_list)) for proc in process_subtree] ) | ||
103 | |||
104 | def get_max_pid(self, process_subtree): | ||
105 | """Returns the max PID found in the process tree.""" | ||
106 | if not process_subtree: | ||
107 | return -100000000; | ||
108 | return max( [max(proc.pid, self.get_max_pid(proc.child_list)) for proc in process_subtree] ) | ||
109 | |||
110 | def update_ppids_for_daemons(self, process_list): | ||
111 | """Fedora hack: when loading the system services from rc, runuser(1) | ||
112 | is used. This sets the PPID of all daemons to 1, skewing | ||
113 | the process tree. Try to detect this and set the PPID of | ||
114 | these processes the PID of rc. | ||
115 | |||
116 | """ | ||
117 | rcstartpid = -1 | ||
118 | rcendpid = -1 | ||
119 | rcproc = None | ||
120 | for p in process_list: | ||
121 | if p.cmd == "rc" and p.ppid == 1: | ||
122 | rcproc = p | ||
123 | rcstartpid = p.pid | ||
124 | rcendpid = self.get_max_pid(p.child_list) | ||
125 | if rcstartpid != -1 and rcendpid != -1: | ||
126 | for p in process_list: | ||
127 | if p.pid > rcstartpid and p.pid < rcendpid and p.ppid == 1: | ||
128 | p.ppid = rcstartpid | ||
129 | p.parent = rcproc | ||
130 | for p in process_list: | ||
131 | p.child_list = [] | ||
132 | self.build() | ||
133 | |||
134 | def prune(self, process_subtree, parent): | ||
135 | """Prunes the process tree by removing idle processes and processes | ||
136 | that only live for the duration of a single top sample. Sibling | ||
137 | processes with the same command line (i.e. threads) are merged | ||
138 | together. This filters out sleepy background processes, short-lived | ||
139 | processes and bootcharts' analysis tools. | ||
140 | """ | ||
141 | def is_idle_background_process_without_children(p): | ||
142 | process_end = p.start_time + p.duration | ||
143 | return not p.active and \ | ||
144 | process_end >= self.start_time + self.duration and \ | ||
145 | p.start_time > self.start_time and \ | ||
146 | p.duration > 0.9 * self.duration and \ | ||
147 | self.num_nodes(p.child_list) == 0 | ||
148 | |||
149 | num_removed = 0 | ||
150 | idx = 0 | ||
151 | while idx < len(process_subtree): | ||
152 | p = process_subtree[idx] | ||
153 | if parent != None or len(p.child_list) == 0: | ||
154 | |||
155 | prune = False | ||
156 | if is_idle_background_process_without_children(p): | ||
157 | prune = True | ||
158 | elif p.duration <= 2 * self.sample_period: | ||
159 | # short-lived process | ||
160 | prune = True | ||
161 | |||
162 | if prune: | ||
163 | process_subtree.pop(idx) | ||
164 | for c in p.child_list: | ||
165 | process_subtree.insert(idx, c) | ||
166 | num_removed += 1 | ||
167 | continue | ||
168 | else: | ||
169 | num_removed += self.prune(p.child_list, p) | ||
170 | else: | ||
171 | num_removed += self.prune(p.child_list, p) | ||
172 | idx += 1 | ||
173 | |||
174 | return num_removed | ||
175 | |||
176 | def merge_logger(self, process_subtree, logger_proc, monitored_app, app_tree): | ||
177 | """Merges the logger's process subtree. The logger will typically | ||
178 | spawn lots of sleep and cat processes, thus polluting the | ||
179 | process tree. | ||
180 | |||
181 | """ | ||
182 | num_removed = 0 | ||
183 | for p in process_subtree: | ||
184 | is_app_tree = app_tree | ||
185 | if logger_proc == p.cmd and not app_tree: | ||
186 | is_app_tree = True | ||
187 | num_removed += self.merge_logger(p.child_list, logger_proc, monitored_app, is_app_tree) | ||
188 | # don't remove the logger itself | ||
189 | continue | ||
190 | |||
191 | if app_tree and monitored_app != None and monitored_app == p.cmd: | ||
192 | is_app_tree = False | ||
193 | |||
194 | if is_app_tree: | ||
195 | for child in p.child_list: | ||
196 | self.__merge_processes(p, child) | ||
197 | num_removed += 1 | ||
198 | p.child_list = [] | ||
199 | else: | ||
200 | num_removed += self.merge_logger(p.child_list, logger_proc, monitored_app, is_app_tree) | ||
201 | return num_removed | ||
202 | |||
203 | def merge_exploders(self, process_subtree, processes): | ||
204 | """Merges specific process subtrees (used for processes which usually | ||
205 | spawn huge meaningless process trees). | ||
206 | |||
207 | """ | ||
208 | num_removed = 0 | ||
209 | for p in process_subtree: | ||
210 | if processes in processes and len(p.child_list) > 0: | ||
211 | subtreemap = self.getProcessMap(p.child_list) | ||
212 | for child in subtreemap.values(): | ||
213 | self.__merge_processes(p, child) | ||
214 | num_removed += len(subtreemap) | ||
215 | p.child_list = [] | ||
216 | p.cmd += " (+)" | ||
217 | else: | ||
218 | num_removed += self.merge_exploders(p.child_list, processes) | ||
219 | return num_removed | ||
220 | |||
221 | def merge_siblings(self,process_subtree): | ||
222 | """Merges thread processes. Sibling processes with the same command | ||
223 | line are merged together. | ||
224 | |||
225 | """ | ||
226 | num_removed = 0 | ||
227 | idx = 0 | ||
228 | while idx < len(process_subtree)-1: | ||
229 | p = process_subtree[idx] | ||
230 | nextp = process_subtree[idx+1] | ||
231 | if nextp.cmd == p.cmd: | ||
232 | process_subtree.pop(idx+1) | ||
233 | idx -= 1 | ||
234 | num_removed += 1 | ||
235 | p.child_list.extend(nextp.child_list) | ||
236 | self.__merge_processes(p, nextp) | ||
237 | num_removed += self.merge_siblings(p.child_list) | ||
238 | idx += 1 | ||
239 | if len(process_subtree) > 0: | ||
240 | p = process_subtree[-1] | ||
241 | num_removed += self.merge_siblings(p.child_list) | ||
242 | return num_removed | ||
243 | |||
244 | def merge_runs(self, process_subtree): | ||
245 | """Merges process runs. Single child processes which share the same | ||
246 | command line with the parent are merged. | ||
247 | |||
248 | """ | ||
249 | num_removed = 0 | ||
250 | idx = 0 | ||
251 | while idx < len(process_subtree): | ||
252 | p = process_subtree[idx] | ||
253 | if len(p.child_list) == 1 and p.child_list[0].cmd == p.cmd: | ||
254 | child = p.child_list[0] | ||
255 | p.child_list = list(child.child_list) | ||
256 | self.__merge_processes(p, child) | ||
257 | num_removed += 1 | ||
258 | continue | ||
259 | num_removed += self.merge_runs(p.child_list) | ||
260 | idx += 1 | ||
261 | return num_removed | ||
262 | |||
263 | def __merge_processes(self, p1, p2): | ||
264 | """Merges two process samples.""" | ||
265 | p1.samples.extend(p2.samples) | ||
266 | p1time = p1.start_time | ||
267 | p2time = p2.start_time | ||
268 | p1.start_time = min(p1time, p2time) | ||
269 | pendtime = max(p1time + p1.duration, p2time + p2.duration) | ||
270 | p1.duration = pendtime - p1.start_time | ||