# 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/>. from __future__ import with_statement import os import string import re import sys import tarfile from time import clock from collections import defaultdict from functools import reduce from .samples import * from .process_tree import ProcessTree if sys.version_info >= (3, 0): long = int # Parsing produces as its end result a 'Trace' class Trace: def __init__(self, writer, paths, options): self.processes = {} self.start = {} self.end = {} self.min = None self.max = None self.headers = None self.disk_stats = None self.ps_stats = None self.taskstats = None self.cpu_stats = None self.cmdline = None self.kernel = None self.kernel_tree = None self.filename = None self.parent_map = None self.mem_stats = None if len(paths): parse_paths (writer, self, paths) if not self.valid(): raise ParseError("empty state: '%s' does not contain a valid bootchart" % ", ".join(paths)) if options.full_time: self.min = min(self.start.keys()) self.max = max(self.end.keys()) return # Turn that parsed information into something more useful # link processes into a tree of pointers, calculate statistics self.compile(writer) # Crop the chart to the end of the first idle period after the given # process if options.crop_after: idle = self.crop (writer, options.crop_after) else: idle = None # Annotate other times as the first start point of given process lists self.times = [ idle ] if options.annotate: for procnames in options.annotate: names = [x[:15] for x in procnames.split(",")] for proc in self.ps_stats.process_map.values(): if proc.cmd in names: self.times.append(proc.start_time) break else: self.times.append(None) self.proc_tree = ProcessTree(writer, self.kernel, self.ps_stats, self.ps_stats.sample_period, self.headers.get("profile.process"), options.prune, idle, self.taskstats, self.parent_map is not None) if self.kernel is not None: self.kernel_tree = ProcessTree(writer, self.kernel, None, 0, self.headers.get("profile.process"), False, None, None, True) def valid(self): return len(self.processes) != 0 return self.headers != None and self.disk_stats != None and \ self.ps_stats != None and self.cpu_stats != None def add_process(self, process, start, end): self.processes[process] = [start, end] if start not in self.start: self.start[start] = [] if process not in self.start[start]: self.start[start].append(process) if end not in self.end: self.end[end] = [] if process not in self.end[end]: self.end[end].append(process) def compile(self, writer): def find_parent_id_for(pid): if pid is 0: return 0 ppid = self.parent_map.get(pid) if ppid: # many of these double forks are so short lived # that we have no samples, or process info for them # so climb the parent hierarcy to find one if int (ppid * 1000) not in self.ps_stats.process_map: # print "Pid '%d' short lived with no process" % ppid ppid = find_parent_id_for (ppid) # else: # print "Pid '%d' has an entry" % ppid else: # print "Pid '%d' missing from pid map" % pid return 0 return ppid # merge in the cmdline data if self.cmdline is not None: for proc in self.ps_stats.process_map.values(): rpid = int (proc.pid // 1000) if rpid in self.cmdline: cmd = self.cmdline[rpid] proc.exe = cmd['exe'] proc.args = cmd['args'] # else: # print "proc %d '%s' not in cmdline" % (rpid, proc.exe) # re-parent any stray orphans if we can if self.parent_map is not None: for process in self.ps_stats.process_map.values(): ppid = find_parent_id_for (int(process.pid // 1000)) if ppid: process.ppid = ppid * 1000 # stitch the tree together with pointers for process in self.ps_stats.process_map.values(): process.set_parent (self.ps_stats.process_map) # count on fingers variously for process in self.ps_stats.process_map.values(): process.calc_stats (self.ps_stats.sample_period) def crop(self, writer, crop_after): def is_idle_at(util, start, j): k = j + 1 while k < len(util) and util[k][0] < start + 300: k += 1 k = min(k, len(util)-1) if util[j][1] >= 0.25: return False avgload = sum(u[1] for u in util[j:k+1]) / (k-j+1) if avgload < 0.25: return True else: return False def is_idle(util, start): for j in range(0, len(util)): if util[j][0] < start: continue return is_idle_at(util, start, j) else: return False names = [x[:15] for x in crop_after.split(",")] for proc in self.ps_stats.process_map.values(): if proc.cmd in names or proc.exe in names: writer.info("selected proc '%s' from list (start %d)" % (proc.cmd, proc.start_time)) break if proc is None: writer.warn("no selected crop proc '%s' in list" % crop_after) cpu_util = [(sample.time, sample.user + sample.sys + sample.io) for sample in self.cpu_stats] disk_util = [(sample.time, sample.util) for sample in self.disk_stats] idle = None for i in range(0, len(cpu_util)): if cpu_util[i][0] < proc.start_time: continue if is_idle_at(cpu_util, cpu_util[i][0], i) \ and is_idle(disk_util, cpu_util[i][0]): idle = cpu_util[i][0] break if idle is None: writer.warn ("not idle after proc '%s'" % crop_after) return None crop_at = idle + 300 writer.info ("cropping at time %d" % crop_at) while len (self.cpu_stats) \ and self.cpu_stats[-1].time > crop_at: self.cpu_stats.pop() while len (self.disk_stats) \ and self.disk_stats[-1].time > crop_at: self.disk_stats.pop() self.ps_stats.end_time = crop_at cropped_map