| # This file is part of pybootchartgui. |
| |
| # pybootchartgui is free software: you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation, either version 3 of the License, or |
| # (at your option) any later version. |
| |
| # pybootchartgui is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| # GNU General Public License for more details. |
| |
| # You should have received a copy of the GNU General Public License |
| # along with pybootchartgui. If not, see <http://www.gnu.org/licenses/>. |
| |
| class ProcessTree: |
| """ProcessTree encapsulates a process tree. The tree is built from log files |
| retrieved during the boot process. When building the process tree, it is |
| pruned and merged in order to be able to visualize it in a comprehensible |
| manner. |
| |
| The following pruning techniques are used: |
| |
| * idle processes that keep running during the last process sample |
| (which is a heuristic for a background processes) are removed, |
| * short-lived processes (i.e. processes that only live for the |
| duration of two samples or less) are removed, |
| * the processes used by the boot logger are removed, |
| * exploders (i.e. processes that are known to spawn huge meaningless |
| process subtrees) have their subtrees merged together, |
| * siblings (i.e. processes with the same command line living |
| concurrently -- thread heuristic) are merged together, |
| * process runs (unary trees with processes sharing the command line) |
| are merged together. |
| |
| """ |
| LOGGER_PROC = 'bootchart-colle' |
| EXPLODER_PROCESSES = set(['hwup']) |
| |
| def __init__(self, writer, kernel, psstats, sample_period, |
| monitoredApp, prune, idle, taskstats, |
| accurate_parentage, for_testing = False): |
| self.writer = writer |
| self.process_tree = [] |
| self.taskstats = taskstats |
| if psstats is None: |
| process_list = kernel |
| elif kernel is None: |
| process_list = psstats.process_map.values() |
| else: |
| process_list = list(kernel) + list(psstats.process_map.values()) |
| self.process_list = sorted(process_list, key = lambda p: p.pid) |
| self.sample_period = sample_period |
| |
| self.build() |
| if not accurate_parentage: |
| self.update_ppids_for_daemons(self.process_list) |
| |
| self.start_time = self.get_start_time(self.process_tree) |
| self.end_time = self.get_end_time(self.process_tree) |
| self.duration = self.end_time - self.start_time |
| self.idle = idle |
| |
| if for_testing: |
| return |
| |
| removed = self.merge_logger(self.process_tree, self.LOGGER_PROC, monitoredApp, False) |
| writer.status("merged %i logger processes" % removed) |
| |
| if prune: |
| p_processes = self.prune(self.process_tree, None) |
| p_exploders = self.merge_exploders(self.process_tree, self.EXPLODER_PROCESSES) |
| p_threads = self.merge_siblings(self.process_tree) |
| p_runs = self.merge_runs(self.process_tree) |
| writer.status("pruned %i process, %i exploders, %i threads, and %i runs" % (p_processes, p_exploders, p_threads, p_runs)) |
| |
| self.sort(self.process_tree) |
| |
| self.start_time = self.get_start_time(self.process_tree) |
| self.end_time = self.get_end_time(self.process_tree) |
| self.duration = self.end_time - self.start_time |
| |
| self.num_proc = self.num_nodes(self.process_tree) |
| |
| def build(self): |
| """Build the process tree from the list of top samples.""" |
| self.process_tree = [] |
| for proc in self.process_list: |
| if not proc.parent: |
| self.process_tree.append(proc) |
| else: |
| proc.parent.child_list.append(proc) |
| |
| def sort(self, process_subtree): |
| """Sort process tree.""" |
| for p in process_subtree: |
| p.child_list.sort(key = lambda p: p.pid) |
| self.sort(p.child_list) |
| |
| def num_nodes(self, process_list): |
| "Counts the number of nodes in the specified process tree.""" |
| nodes = 0 |
| for proc in process_list: |
| nodes = nodes + self.num_nodes(proc.child_list) |
| return nodes + len(process_list) |
| |
| def get_start_time(self, process_subtree): |
| """Returns the start time of the process subtree. This is the start |
| time of the earliest process. |
| |
| """ |
| if not process_subtree: |
| return 100000000 |
| return min( [min(proc.start_time, self.get_start_time(proc.child_list)) for proc in process_subtree] ) |
| |
| def get_end_time(self, process_subtree): |
| """Returns the end time of the process subtree. This is the end time |
| of the last collected sample. |
| |
| """ |
| if not process_subtree: |
| return -100000000 |
| return max( [max(proc.start_time + proc.duration, self.get_end_time(proc.child_list)) for proc in process_subtree] ) |
| |
| def get_max_pid(self, process_subtree): |
| """Returns the max PID found in the process tree.""" |
| if not process_subtree: |
| return -100000000 |
| return max( [max(proc.pid, self.get_max_pid(proc.child_list)) for proc in process_subtree] ) |
| |
| def update_ppids_for_daemons(self, process_list): |
| """Fedora hack: when loading the system services from rc, runuser(1) |
| is used. This sets the PPID of all daemons to 1, skewing |
| the process tree. Try to detect this and set the PPID of |
| these processes the PID of rc. |
| |
| """ |
| rcstartpid = -1 |
| rcendpid = -1 |
| rcproc = None |
| for p in process_list: |
| if p.cmd == "rc" and p.ppid // 1000 == 1: |
| rcproc = p |
| rcstartpid = p.pid |
| rcendpid = self.get_max_pid(p.child_list) |
| if rcstartpid != -1 and rcendpid != -1: |
| for p in process_list: |
| if p.pid > rcstartpid and p.pid < rcendpid and p.ppid // 1000 == 1: |
| p.ppid = rcstartpid |
| p.parent = rcproc |
| for p in process_list: |
| p.child_list = [] |
| self.build() |
| |
| def prune(self, process_subtree, parent): |
| """Prunes the process tree by removing idle processes and processes |
| that only live for the duration of a single top sample. Sibling |
| processes with the same command line (i.e. threads) are merged |
| together. This filters out sleepy background processes, short-lived |
| processes and bootcharts' analysis tools. |
| """ |
| def is_idle_background_process_without_children(p): |
| process_end = p.start_time + p.duration |
| return not p.active and \ |
| process_end >= self.start_time + self.duration and \ |
| p.start_time > self.start_time and \ |
| p.duration > 0.9 * self.duration and \ |
| self.num_nodes(p.child_list) == 0 |
| |
| num_removed = 0 |
| idx = 0 |
| while idx < len(process_subtree): |
| p = process_subtree[idx] |
| if parent != None or len(p.child_list) == 0: |
| |
| prune = False |
| if is_idle_background_process_without_children(p): |
| prune = True |
| elif p.duration <= 2 * self.sample_period: |
| # short-lived process |
| prune = True |
| |
| if prune: |
| process_subtree.pop(idx) |
| for c in p.child_list: |
| process_subtree.insert(idx, c) |
| num_removed += 1 |
| continue |
| else: |
| num_removed += self.prune(p.child_list, p) |
| else: |
| num_removed += self.prune(p.child_list, p) |
| idx += 1 |
| |
| return num_removed |
| |
| def merge_logger(self, process_subtree, logger_proc, monitored_app, app_tree): |
| """Merges the logger's process subtree. The logger will typically |
| spawn lots of sleep and cat processes, thus polluting the |
| process tree. |
| |
| """ |
| num_removed = 0 |
| for p in process_subtree: |
| is_app_tree = app_tree |
| if logger_proc == p.cmd and not app_tree: |
| is_app_tree = True |
| num_removed += self.merge_logger(p.child_list, logger_proc, monitored_app, is_app_tree) |
| # don't remove the logger itself |
| continue |
| |
| if app_tree and monitored_app != None and monitored_app == p.cmd: |
| is_app_tree = False |
| |
| if is_app_tree: |
| for child in p.child_list: |
| self.merge_processes(p, child) |
| num_removed += 1 |
| p.child_list = [] |
| else: |
| num_removed += self.merge_logger(p.child_list, logger_proc, monitored_app, is_app_tree) |
| return num_removed |
| |
| def merge_exploders(self, process_subtree, processes): |
| """Merges specific process subtrees (used for processes which usually |
| spawn huge meaningless process trees). |
| |
| """ |
| num_removed = 0 |
| for p in process_subtree: |
| if processes in processes and len(p.child_list) > 0: |
| subtreemap = self.getProcessMap(p.child_list) |
| for child in subtreemap.values(): |
| self.merge_processes(p, child) |
| num_removed += len(subtreemap) |
| p.child_list = [] |
| p.cmd += " (+)" |
| else: |
| num_removed += self.merge_exploders(p.child_list, processes) |
| return num_removed |
| |
| def merge_siblings(self, process_subtree): |
| """Merges thread processes. Sibling processes with the same command |
| line are merged together. |
| |
| """ |
| num_removed = 0 |
| idx = 0 |
| while idx < len(process_subtree)-1: |
| p = process_subtree[idx] |
| nextp = process_subtree[idx+1] |
| if nextp.cmd == p.cmd: |
| process_subtree.pop(idx+1) |
| idx -= 1 |
| num_removed += 1 |
| p.child_list.extend(nextp.child_list) |
| self.merge_processes(p, nextp) |
| num_removed += self.merge_siblings(p.child_list) |
| idx += 1 |
| if len(process_subtree) > 0: |
| p = process_subtree[-1] |
| num_removed += self.merge_siblings(p.child_list) |
| return num_removed |
| |
| def merge_runs(self, process_subtree): |
| """Merges process runs. Single child processes which share the same |
| command line with the parent are merged. |
| |
| """ |
| num_removed = 0 |
| idx = 0 |
| while idx < len(process_subtree): |
| p = process_subtree[idx] |
| if len(p.child_list) == 1 and p.child_list[0].cmd == p.cmd: |
| child = p.child_list[0] |
| p.child_list = list(child.child_list) |
| self.merge_processes(p, child) |
| num_removed += 1 |
| continue |
| num_removed += self.merge_runs(p.child_list) |
| idx += 1 |
| return num_removed |
| |
| def merge_processes(self, p1, p2): |
| """Merges two process' samples.""" |
| p1.samples.extend(p2.samples) |
| p1.samples.sort( key = lambda p: p.time ) |
| p1time = p1.start_time |
| p2time = p2.start_time |
| p1.start_time = min(p1time, p2time) |
| pendtime = max(p1time + p1.duration, p2time + p2.duration) |
| p1.duration = pendtime - p1.start_time |