pybootchartgui: add the original code

This is from:
http://pybootchartgui.googlecode.com/files/pybootchartgui-r124.tar.gz

Will modify it to make the build profiling in pictures.

Remove the examples since they would not work any more, and they cost
much disk space.

[YOCTO #2403]

(From OE-Core rev: 1f0791109e1aed715f02945834d6d7fdb9a411b4)

Signed-off-by: Robert Yang <liezhi.yang@windriver.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
This commit is contained in:
Robert Yang
2012-06-06 13:52:43 +08:00
committed by Richard Purdie
parent bc19f8bc9c
commit 3d78bc19c5
14 changed files with 2207 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
import cairo
import draw
def render(res, format, filename):
handlers = {
"png": (lambda w,h: cairo.ImageSurface(cairo.FORMAT_ARGB32,w,h), lambda sfc: sfc.write_to_png(filename)),
"pdf": (lambda w,h: cairo.PDFSurface(filename, w, h), lambda sfc: 0),
"svg": (lambda w,h: cairo.SVGSurface(filename, w, h), lambda sfc: 0)
}
if not(handlers.has_key(format)):
print "Unknown format '%s'." % format
return 10
make_surface, write_surface = handlers[format]
w,h = draw.extents(*res)
w = max(w, draw.MIN_IMG_W)
surface = make_surface(w,h)
ctx = cairo.Context(surface)
draw.render(ctx, *res)
write_surface(surface)

View File

@@ -0,0 +1,355 @@
import cairo
import math
import re
# Process tree background color.
BACK_COLOR = (1.0, 1.0, 1.0, 1.0)
WHITE = (1.0, 1.0, 1.0, 1.0)
# Process tree border color.
BORDER_COLOR = (0.63, 0.63, 0.63, 1.0)
# Second tick line color.
TICK_COLOR = (0.92, 0.92, 0.92, 1.0)
# 5-second tick line color.
TICK_COLOR_BOLD = (0.86, 0.86, 0.86, 1.0)
# Text color.
TEXT_COLOR = (0.0, 0.0, 0.0, 1.0)
# Font family
FONT_NAME = "Bitstream Vera Sans"
# Title text font.
TITLE_FONT_SIZE = 18
# Default text font.
TEXT_FONT_SIZE = 12
# Axis label font.
AXIS_FONT_SIZE = 11
# Legend font.
LEGEND_FONT_SIZE = 12
# CPU load chart color.
CPU_COLOR = (0.40, 0.55, 0.70, 1.0)
# IO wait chart color.
IO_COLOR = (0.76, 0.48, 0.48, 0.5)
# Disk throughput color.
DISK_TPUT_COLOR = (0.20, 0.71, 0.20, 1.0)
# CPU load chart color.
FILE_OPEN_COLOR = (0.20, 0.71, 0.71, 1.0)
# Process border color.
PROC_BORDER_COLOR = (0.71, 0.71, 0.71, 1.0)
# Waiting process color.
PROC_COLOR_D = (0.76, 0.48, 0.48, 0.125)
# Running process color.
PROC_COLOR_R = CPU_COLOR
# Sleeping process color.
PROC_COLOR_S = (0.94, 0.94, 0.94, 1.0)
# Stopped process color.
PROC_COLOR_T = (0.94, 0.50, 0.50, 1.0)
# Zombie process color.
PROC_COLOR_Z = (0.71, 0.71, 0.71, 1.0)
# Dead process color.
PROC_COLOR_X = (0.71, 0.71, 0.71, 0.125)
# Paging process color.
PROC_COLOR_W = (0.71, 0.71, 0.71, 0.125)
# Process label color.
PROC_TEXT_COLOR = (0.19, 0.19, 0.19, 1.0)
# Process label font.
PROC_TEXT_FONT_SIZE = 12
# Signature color.
SIG_COLOR = (0.0, 0.0, 0.0, 0.3125)
# Signature font.
SIG_FONT_SIZE = 14
# Signature text.
SIGNATURE = "http://code.google.com/p/pybootchartgui"
# Process dependency line color.
DEP_COLOR = (0.75, 0.75, 0.75, 1.0)
# Process dependency line stroke.
DEP_STROKE = 1.0
# Process description date format.
DESC_TIME_FORMAT = "mm:ss.SSS"
# Process states
STATE_UNDEFINED = 0
STATE_RUNNING = 1
STATE_SLEEPING = 2
STATE_WAITING = 3
STATE_STOPPED = 4
STATE_ZOMBIE = 5
STATE_COLORS = [(0,0,0,0), PROC_COLOR_R, PROC_COLOR_S, PROC_COLOR_D, PROC_COLOR_T, PROC_COLOR_Z, PROC_COLOR_X, PROC_COLOR_W]
# Convert ps process state to an int
def get_proc_state(flag):
return "RSDTZXW".index(flag) + 1
def draw_text(ctx, text, color, x, y):
ctx.set_source_rgba(*color)
ctx.move_to(x, y)
ctx.show_text(text)
def draw_fill_rect(ctx, color, rect):
ctx.set_source_rgba(*color)
ctx.rectangle(*rect)
ctx.fill()
def draw_rect(ctx, color, rect):
ctx.set_source_rgba(*color)
ctx.rectangle(*rect)
ctx.stroke()
def draw_legend_box(ctx, label, fill_color, x, y, s):
draw_fill_rect(ctx, fill_color, (x, y - s, s, s))
draw_rect(ctx, PROC_BORDER_COLOR, (x, y - s, s, s))
draw_text(ctx, label, TEXT_COLOR, x + s + 5, y)
def draw_legend_line(ctx, label, fill_color, x, y, s):
draw_fill_rect(ctx, fill_color, (x, y - s/2, s + 1, 3))
ctx.arc(x + (s + 1)/2.0, y - (s - 3)/2.0, 2.5, 0, 2.0 * math.pi)
ctx.fill()
draw_text(ctx, label, TEXT_COLOR, x + s + 5, y)
def draw_label_in_box(ctx, color, label, x, y, w, maxx):
label_w = ctx.text_extents(label)[2]
label_x = x + w / 2 - label_w / 2
if label_w + 10 > w:
label_x = x + w + 5
if label_x + label_w > maxx:
label_x = x - label_w - 5
draw_text(ctx, label, color, label_x, y)
def draw_5sec_labels(ctx, rect, sec_w):
ctx.set_font_size(AXIS_FONT_SIZE)
for i in range(0, rect[2] + 1, sec_w):
if ((i / sec_w) % 5 == 0) :
label = "%ds" % (i / sec_w)
label_w = ctx.text_extents(label)[2]
draw_text(ctx, label, TEXT_COLOR, rect[0] + i - label_w/2, rect[1] - 2)
def draw_box_ticks(ctx, rect, sec_w):
draw_rect(ctx, BORDER_COLOR, tuple(rect))
ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
for i in range(sec_w, rect[2] + 1, sec_w):
if ((i / sec_w) % 5 == 0) :
ctx.set_source_rgba(*TICK_COLOR_BOLD)
else :
ctx.set_source_rgba(*TICK_COLOR)
ctx.move_to(rect[0] + i, rect[1] + 1)
ctx.line_to(rect[0] + i, rect[1] + rect[3] - 1)
ctx.stroke()
ctx.set_line_cap(cairo.LINE_CAP_BUTT)
def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree):
ctx.set_line_width(0.5)
x_shift = proc_tree.start_time
x_scale = proc_tree.duration
def transform_point_coords(point, x_base, y_base, xscale, yscale, x_trans, y_trans):
x = (point[0] - x_base) * xscale + x_trans
y = (point[1] - y_base) * -yscale + y_trans + bar_h
return x, y
xscale = float(chart_bounds[2]) / max(x for (x,y) in data)
yscale = float(chart_bounds[3]) / max(y for (x,y) in data)
first = transform_point_coords(data[0], x_shift, 0, xscale, yscale, chart_bounds[0], chart_bounds[1])
last = transform_point_coords(data[-1], x_shift, 0, xscale, yscale, chart_bounds[0], chart_bounds[1])
ctx.set_source_rgba(*color)
ctx.move_to(*first)
for point in data:
x, y = transform_point_coords(point, x_shift, 0, xscale, yscale, chart_bounds[0], chart_bounds[1])
ctx.line_to(x, y)
if fill:
ctx.stroke_preserve()
ctx.line_to(last[0], chart_bounds[1]+bar_h)
ctx.line_to(first[0], chart_bounds[1]+bar_h)
ctx.line_to(first[0], first[1])
ctx.fill()
else:
ctx.stroke()
ctx.set_line_width(1.0)
header_h = 280
bar_h = 55
# offsets
off_x, off_y = 10, 10
sec_w = 25 # the width of a second
proc_h = 16 # the height of a process
leg_s = 10
MIN_IMG_W = 800
def extents(headers, cpu_stats, disk_stats, proc_tree):
w = (proc_tree.duration * sec_w / 100) + 2*off_x
h = proc_h * proc_tree.num_proc + header_h + 2*off_y
return (w,h)
#
# Render the chart.
#
def render(ctx, headers, cpu_stats, disk_stats, proc_tree):
(w, h) = extents(headers, cpu_stats, disk_stats, proc_tree)
ctx.set_line_width(1.0)
ctx.select_font_face(FONT_NAME)
draw_fill_rect(ctx, WHITE, (0, 0, max(w, MIN_IMG_W), h))
w -= 2*off_x
# draw the title and headers
curr_y = draw_header(ctx, headers, off_x, proc_tree.duration)
# render bar legend
ctx.set_font_size(LEGEND_FONT_SIZE)
draw_legend_box(ctx, "CPU (user+sys)", CPU_COLOR, off_x, curr_y+20, leg_s)
draw_legend_box(ctx, "I/O (wait)", IO_COLOR, off_x + 120, curr_y+20, leg_s)
# render I/O wait
chart_rect = (off_x, curr_y+30, w, bar_h)
draw_box_ticks(ctx, chart_rect, sec_w)
draw_chart(ctx, IO_COLOR, True, chart_rect, [(sample.time, sample.user + sample.sys + sample.io) for sample in cpu_stats], proc_tree)
# render CPU load
draw_chart(ctx, CPU_COLOR, True, chart_rect, [(sample.time, sample.user + sample.sys) for sample in cpu_stats], proc_tree)
curr_y = curr_y + 30 + bar_h
# render second chart
draw_legend_line(ctx, "Disk throughput", DISK_TPUT_COLOR, off_x, curr_y+20, leg_s)
draw_legend_box(ctx, "Disk utilization", IO_COLOR, off_x + 120, curr_y+20, leg_s)
# render I/O utilization
chart_rect = (off_x, curr_y+30, w, bar_h)
draw_box_ticks(ctx, chart_rect, sec_w)
draw_chart(ctx, IO_COLOR, True, chart_rect, [(sample.time, sample.util) for sample in disk_stats], proc_tree)
# render disk throughput
max_sample = max(disk_stats, key=lambda s: s.tput)
draw_chart(ctx, DISK_TPUT_COLOR, False, chart_rect, [(sample.time, sample.tput) for sample in disk_stats], proc_tree)
pos_x = off_x + ((max_sample.time - proc_tree.start_time) * w / proc_tree.duration)
shift_x, shift_y = -20, 20
if (pos_x < off_x + 245):
shift_x, shift_y = 5, 40
label = "%dMB/s" % round((max_sample.tput) / 1024.0)
draw_text(ctx, label, DISK_TPUT_COLOR, pos_x + shift_x, curr_y + shift_y)
# draw process boxes
draw_process_bar_chart(ctx, proc_tree, curr_y + bar_h, w, h)
ctx.set_font_size(SIG_FONT_SIZE)
draw_text(ctx, SIGNATURE, SIG_COLOR, off_x + 5, h - off_y - 5)
def draw_process_bar_chart(ctx, proc_tree, curr_y, w, h):
draw_legend_box(ctx, "Running (%cpu)", PROC_COLOR_R, off_x , curr_y + 45, leg_s)
draw_legend_box(ctx, "Unint.sleep (I/O)", PROC_COLOR_D, off_x+120, curr_y + 45, leg_s)
draw_legend_box(ctx, "Sleeping", PROC_COLOR_S, off_x+240, curr_y + 45, leg_s)
draw_legend_box(ctx, "Zombie", PROC_COLOR_Z, off_x+360, curr_y + 45, leg_s)
chart_rect = [off_x, curr_y+60, w, h - 2 * off_y - (curr_y+60) + proc_h]
ctx.set_font_size(PROC_TEXT_FONT_SIZE)
draw_box_ticks(ctx, chart_rect, sec_w)
draw_5sec_labels(ctx, chart_rect, sec_w)
y = curr_y+60
for root in proc_tree.process_tree:
draw_processes_recursively(ctx, root, proc_tree, y, proc_h, chart_rect)
y = y + proc_h * proc_tree.num_nodes([root])
def draw_header(ctx, headers, off_x, duration):
dur = duration / 100.0
toshow = [
('system.uname', 'uname', lambda s: s),
('system.release', 'release', lambda s: s),
('system.cpu', 'CPU', lambda s: re.sub('model name\s*:\s*', '', s, 1)),
('system.kernel.options', 'kernel options', lambda s: s),
('pseudo.header', 'time', lambda s: '%02d:%05.2f' % (math.floor(dur/60), dur - 60 * math.floor(dur/60)))
]
header_y = ctx.font_extents()[2] + 10
ctx.set_font_size(TITLE_FONT_SIZE)
draw_text(ctx, headers['title'], TEXT_COLOR, off_x, header_y)
ctx.set_font_size(TEXT_FONT_SIZE)
for (headerkey, headertitle, mangle) in toshow:
header_y += ctx.font_extents()[2]
txt = headertitle + ': ' + mangle(headers.get(headerkey))
draw_text(ctx, txt, TEXT_COLOR, off_x, header_y)
return header_y
def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect) :
x = rect[0] + ((proc.start_time - proc_tree.start_time) * rect[2] / proc_tree.duration)
w = ((proc.duration) * rect[2] / proc_tree.duration)
draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect)
draw_rect(ctx, PROC_BORDER_COLOR, (x, y, w, proc_h))
draw_label_in_box(ctx, PROC_TEXT_COLOR, proc.cmd, x, y + proc_h - 4, w, rect[0] + rect[2])
next_y = y + proc_h
for child in proc.child_list:
child_x, child_y = draw_processes_recursively(ctx, child, proc_tree, next_y, proc_h, rect)
draw_process_connecting_lines(ctx, x, y, child_x, child_y, proc_h)
next_y = next_y + proc_h * proc_tree.num_nodes([child])
return x, y
def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect):
draw_fill_rect(ctx, PROC_COLOR_S, (x, y, w, proc_h))
last_tx = -1
for sample in proc.samples :
tx = rect[0] + round(((sample.time - proc_tree.start_time) * rect[2] / proc_tree.duration))
tw = round(proc_tree.sample_period * rect[2] / float(proc_tree.duration))
if last_tx != -1 and abs(last_tx - tx) <= tw:
tw -= last_tx - tx
tx = last_tx
last_tx = tx + tw
state = get_proc_state( sample.state )
color = STATE_COLORS[state]
if state == STATE_RUNNING:
alpha = sample.cpu_sample.user + sample.cpu_sample.sys
color = tuple(list(PROC_COLOR_R[0:3]) + [alpha])
elif state == STATE_SLEEPING:
continue
draw_fill_rect(ctx, color, (tx, y, tw, proc_h))
def draw_process_connecting_lines(ctx, px, py, x, y, proc_h):
ctx.set_source_rgba(*DEP_COLOR)
ctx.set_dash([2,2])
if abs(px - x) < 3:
dep_off_x = 3
dep_off_y = proc_h / 4
ctx.move_to(x, y + proc_h / 2)
ctx.line_to(px - dep_off_x, y + proc_h / 2)
ctx.line_to(px - dep_off_x, py - dep_off_y)
ctx.line_to(px, py - dep_off_y)
else:
ctx.move_to(x, y + proc_h / 2)
ctx.line_to(px, y + proc_h / 2)
ctx.line_to(px, py)
ctx.stroke()
ctx.set_dash([])

View File

@@ -0,0 +1,273 @@
import gobject
import gtk
import gtk.gdk
import gtk.keysyms
import draw
class PyBootchartWidget(gtk.DrawingArea):
__gsignals__ = {
'expose-event': 'override',
'clicked' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gtk.gdk.Event)),
'position-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT, gobject.TYPE_INT)),
'set-scroll-adjustments' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gtk.Adjustment, gtk.Adjustment))
}
def __init__(self, res):
gtk.DrawingArea.__init__(self)
self.res = res
self.set_flags(gtk.CAN_FOCUS)
self.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
self.connect("button-press-event", self.on_area_button_press)
self.connect("button-release-event", self.on_area_button_release)
self.add_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
self.connect("motion-notify-event", self.on_area_motion_notify)
self.connect("scroll-event", self.on_area_scroll_event)
self.connect('key-press-event', self.on_key_press_event)
self.connect('set-scroll-adjustments', self.on_set_scroll_adjustments)
self.connect("size-allocate", self.on_allocation_size_changed)
self.connect("position-changed", self.on_position_changed)
self.zoom_ratio = 1.0
self.x, self.y = 0.0, 0.0
self.chart_width, self.chart_height = draw.extents(*res)
self.hadj = None
self.vadj = None
def do_expose_event(self, event):
cr = self.window.cairo_create()
# set a clip region for the expose event
cr.rectangle(
event.area.x, event.area.y,
event.area.width, event.area.height
)
cr.clip()
self.draw(cr, self.get_allocation())
return False
def draw(self, cr, rect):
cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
cr.paint()
cr.scale(self.zoom_ratio, self.zoom_ratio)
cr.translate(-self.x, -self.y)
draw.render(cr, *self.res)
def position_changed(self):
self.emit("position-changed", self.x, self.y)
ZOOM_INCREMENT = 1.25
def zoom_image(self, zoom_ratio):
self.zoom_ratio = zoom_ratio
self._set_scroll_adjustments(self.hadj, self.vadj)
self.queue_draw()
def zoom_to_rect(self, rect):
zoom_ratio = float(rect.width)/float(self.chart_width)
self.zoom_image(zoom_ratio)
self.x = 0
self.position_changed()
def on_zoom_in(self, action):
self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
def on_zoom_out(self, action):
self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
def on_zoom_fit(self, action):
self.zoom_to_rect(self.get_allocation())
def on_zoom_100(self, action):
self.zoom_image(1.0)
POS_INCREMENT = 100
def on_key_press_event(self, widget, event):
if event.keyval == gtk.keysyms.Left:
self.x -= self.POS_INCREMENT/self.zoom_ratio
elif event.keyval == gtk.keysyms.Right:
self.x += self.POS_INCREMENT/self.zoom_ratio
elif event.keyval == gtk.keysyms.Up:
self.y -= self.POS_INCREMENT/self.zoom_ratio
elif event.keyval == gtk.keysyms.Down:
self.y += self.POS_INCREMENT/self.zoom_ratio
elif event.keyval == gtk.keysyms.Page_Up:
self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
elif event.keyval == gtk.keysyms.Page_Down:
self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
else:
return False
self.queue_draw()
self.position_changed()
return True
def on_area_button_press(self, area, event):
if event.button == 2 or event.button == 1:
area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
self.prevmousex = event.x
self.prevmousey = event.y
if event.type not in (gtk.gdk.BUTTON_PRESS, gtk.gdk.BUTTON_RELEASE):
return False
return False
def on_area_button_release(self, area, event):
if event.button == 2 or event.button == 1:
area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
self.prevmousex = None
self.prevmousey = None
return True
return False
def on_area_scroll_event(self, area, event):
if event.direction == gtk.gdk.SCROLL_UP:
self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
return True
if event.direction == gtk.gdk.SCROLL_DOWN:
self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
return True
return False
def on_area_motion_notify(self, area, event):
state = event.state
if state & gtk.gdk.BUTTON2_MASK or state & gtk.gdk.BUTTON1_MASK:
x, y = int(event.x), int(event.y)
# pan the image
self.x += (self.prevmousex - x)/self.zoom_ratio
self.y += (self.prevmousey - y)/self.zoom_ratio
self.queue_draw()
self.prevmousex = x
self.prevmousey = y
self.position_changed()
return True
def on_set_scroll_adjustments(self, area, hadj, vadj):
self._set_scroll_adjustments(hadj, vadj)
def on_allocation_size_changed(self, widget, allocation):
self.hadj.page_size = allocation.width
self.hadj.page_increment = allocation.width * 0.9
self.vadj.page_size = allocation.height
self.vadj.page_increment = allocation.height * 0.9
def _set_scroll_adjustments(self, hadj, vadj):
if hadj == None:
hadj = gtk.Adjustment(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
if vadj == None:
vadj = gtk.Adjustment(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
if self.hadj != None and hadj != self.hadj:
self.hadj.disconnect(self.hadj_changed_signal_id)
if self.vadj != None and vadj != self.vadj:
self.vadj.disconnect(self.vadj_changed_signal_id)
if hadj != None:
self.hadj = hadj
self._set_adj_upper(self.hadj, self.zoom_ratio * self.chart_width)
self.hadj_changed_signal_id = self.hadj.connect('value-changed', self.on_adjustments_changed)
if vadj != None:
self.vadj = vadj
self._set_adj_upper(self.vadj, self.zoom_ratio * self.chart_height)
self.vadj_changed_signal_id = self.vadj.connect('value-changed', self.on_adjustments_changed)
def _set_adj_upper(self, adj, upper):
changed = False
value_changed = False
if adj.upper != upper:
adj.upper = upper
changed = True
max_value = max(0.0, upper - adj.page_size)
if adj.value > max_value:
adj.value = max_value
value_changed = True
if changed:
adj.changed()
if value_changed:
adj.value_changed()
def on_adjustments_changed(self, adj):
self.x = self.hadj.value / self.zoom_ratio
self.y = self.vadj.value / self.zoom_ratio
self.queue_draw()
def on_position_changed(self, widget, x, y):
self.hadj.value = x * self.zoom_ratio
self.vadj.value = y * self.zoom_ratio
PyBootchartWidget.set_set_scroll_adjustments_signal('set-scroll-adjustments')
class PyBootchartWindow(gtk.Window):
ui = '''
<ui>
<toolbar name="ToolBar">
<toolitem action="ZoomIn"/>
<toolitem action="ZoomOut"/>
<toolitem action="ZoomFit"/>
<toolitem action="Zoom100"/>
</toolbar>
</ui>
'''
def __init__(self, res):
gtk.Window.__init__(self)
window = self
window.set_title('Bootchart')
window.set_default_size(512, 512)
vbox = gtk.VBox()
window.add(vbox)
self.widget = PyBootchartWidget(res)
# Create a UIManager instance
uimanager = self.uimanager = gtk.UIManager()
# Add the accelerator group to the toplevel window
accelgroup = uimanager.get_accel_group()
window.add_accel_group(accelgroup)
# Create an ActionGroup
actiongroup = gtk.ActionGroup('Actions')
self.actiongroup = actiongroup
# Create actions
actiongroup.add_actions((
('ZoomIn', gtk.STOCK_ZOOM_IN, None, None, None, self.widget.on_zoom_in),
('ZoomOut', gtk.STOCK_ZOOM_OUT, None, None, None, self.widget.on_zoom_out),
('ZoomFit', gtk.STOCK_ZOOM_FIT, 'Fit Width', None, None, self.widget.on_zoom_fit),
('Zoom100', gtk.STOCK_ZOOM_100, None, None, None, self.widget.on_zoom_100),
))
# Add the actiongroup to the uimanager
uimanager.insert_action_group(actiongroup, 0)
# Add a UI description
uimanager.add_ui_from_string(self.ui)
# Scrolled window
scrolled = gtk.ScrolledWindow()
scrolled.add(self.widget)
# Create a Toolbar
toolbar = uimanager.get_widget('/ToolBar')
vbox.pack_start(toolbar, False)
vbox.pack_start(scrolled)
self.set_focus(self.widget)
self.show_all()
def show(res):
win = PyBootchartWindow(res)
win.connect('destroy', gtk.main_quit)
gtk.main()

View File

@@ -0,0 +1,71 @@
import sys
import os
import optparse
import parsing
import gui
import batch
def _mk_options_parser():
"""Make an options parser."""
usage = "%prog [options] PATH, ..., PATH"
version = "%prog v0.0.0"
parser = optparse.OptionParser(usage, version=version)
parser.add_option("-i", "--interactive", action="store_true", dest="interactive", default=False,
help="start in active mode")
parser.add_option("-f", "--format", dest="format", default = None,
help="image format (...); default format ...")
parser.add_option("-o", "--output", dest="output", metavar="PATH", default=None,
help="output path (file or directory) where charts are stored")
parser.add_option("-n", "--no-prune", action="store_false", dest="prune", default=True,
help="do not prune the process tree")
parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False,
help="suppress informational messages")
parser.add_option("--very-quiet", action="store_true", dest="veryquiet", default=False,
help="suppress all messages except errors")
parser.add_option("--verbose", action="store_true", dest="verbose", default=False,
help="print all messages")
return parser
def _get_filename(paths, options):
"""Construct a usable filename for outputs based on the paths and options given on the commandline."""
dir = ""
file = "bootchart"
if options.output != None and not(os.path.isdir(options.output)):
return options.output
if options.output != None:
dir = options.output
if len(paths) == 1:
if os.path.isdir(paths[0]):
file = os.path.split(paths[0])[-1]
elif os.path.splitext(paths[0])[1] in [".tar", ".tgz", ".tar.gz"]:
file = os.path.splitext(paths[0])[0]
return os.path.join(dir, file + "." + options.format)
def main(argv=None):
try:
if argv is None:
argv = sys.argv[1:]
parser = _mk_options_parser()
options, args = parser.parse_args(argv)
if len(args) == 0:
parser.error("insufficient arguments, expected at least one path.")
return 2
res = parsing.parse(args, options.prune)
if options.interactive or options.format == None:
gui.show(res)
else:
filename = _get_filename(args, options)
batch.render(res, options.format, filename)
print "bootchart written to", filename
return 0
except parsing.ParseError, ex:
print("Parse error: %s" % ex)
return 2
if __name__ == '__main__':
sys.exit(main())

View File

@@ -0,0 +1,223 @@
from __future__ import with_statement
import os
import string
import re
import tarfile
from collections import defaultdict
from samples import *
from process_tree import ProcessTree
class ParseError(Exception):
"""Represents errors during parse of the bootchart."""
def __init__(self, value):
self.value = value
def __str__(self):
return self.value
def _parse_headers(file):
"""Parses the headers of the bootchart."""
def parse((headers,last), line):
if '=' in line: last,value = map(string.strip, line.split('=', 1))
else: value = line.strip()
headers[last] += value
return headers,last
return reduce(parse, file.read().split('\n'), (defaultdict(str),''))[0]
def _parse_timed_blocks(file):
"""Parses (ie., splits) a file into so-called timed-blocks. A
timed-block consists of a timestamp on a line by itself followed
by zero or more lines of data for that point in time."""
def parse(block):
lines = block.split('\n')
if not lines:
raise ParseError('expected a timed-block consisting a timestamp followed by data lines')
try:
return (int(lines[0]), lines[1:])
except ValueError:
raise ParseError("expected a timed-block, but timestamp '%s' is not an integer" % lines[0])
blocks = file.read().split('\n\n')
return [parse(block) for block in blocks if block.strip()]
def _parse_proc_ps_log(file):
"""
* See proc(5) for details.
*
* {pid, comm, state, ppid, pgrp, session, tty_nr, tpgid, flags, minflt, cminflt, majflt, cmajflt, utime, stime,
* cutime, cstime, priority, nice, 0, itrealvalue, starttime, vsize, rss, rlim, startcode, endcode, startstack,
* kstkesp, kstkeip}
"""
processMap = {}
ltime = 0
timed_blocks = _parse_timed_blocks(file)
for time, lines in timed_blocks:
for line in lines:
tokens = line.split(' ')
offset = [index for index, token in enumerate(tokens[1:]) if token.endswith(')')][0]
pid, cmd, state, ppid = int(tokens[0]), ' '.join(tokens[1:2+offset]), tokens[2+offset], int(tokens[3+offset])
userCpu, sysCpu, stime= int(tokens[13+offset]), int(tokens[14+offset]), int(tokens[21+offset])
if processMap.has_key(pid):
process = processMap[pid]
process.cmd = cmd.replace('(', '').replace(')', '') # why rename after latest name??
else:
process = Process(pid, cmd, ppid, min(time, stime))
processMap[pid] = process
if process.last_user_cpu_time is not None and process.last_sys_cpu_time is not None and ltime is not None:
userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, time - ltime)
cpuSample = CPUSample('null', userCpuLoad, sysCpuLoad, 0.0)
process.samples.append(ProcessSample(time, state, cpuSample))
process.last_user_cpu_time = userCpu
process.last_sys_cpu_time = sysCpu
ltime = time
startTime = timed_blocks[0][0]
avgSampleLength = (ltime - startTime)/(len(timed_blocks)-1)
for process in processMap.values():
process.set_parent(processMap)
for process in processMap.values():
process.calc_stats(avgSampleLength)
return ProcessStats(processMap.values(), avgSampleLength, startTime, ltime)
def _parse_proc_stat_log(file):
samples = []
ltimes = None
for time, lines in _parse_timed_blocks(file):
# CPU times {user, nice, system, idle, io_wait, irq, softirq}
tokens = lines[0].split();
times = [ int(token) for token in tokens[1:] ]
if ltimes:
user = float((times[0] + times[1]) - (ltimes[0] + ltimes[1]))
system = float((times[2] + times[5] + times[6]) - (ltimes[2] + ltimes[5] + ltimes[6]))
idle = float(times[3] - ltimes[3])
iowait = float(times[4] - ltimes[4])
aSum = max(user + system + idle + iowait, 1)
samples.append( CPUSample(time, user/aSum, system/aSum, iowait/aSum) )
ltimes = times
# skip the rest of statistics lines
return samples
def _parse_proc_disk_stat_log(file, numCpu):
"""
Parse file for disk stats, but only look at the whole disks, eg. sda,
not sda1, sda2 etc. The format of relevant lines should be:
{major minor name rio rmerge rsect ruse wio wmerge wsect wuse running use aveq}
"""
DISK_REGEX = 'hd.$|sd.$'
def is_relevant_line(line):
return len(line.split()) == 14 and re.match(DISK_REGEX, line.split()[2])
disk_stat_samples = []
for time, lines in _parse_timed_blocks(file):
sample = DiskStatSample(time)
relevant_tokens = [line.split() for line in lines if is_relevant_line(line)]
for tokens in relevant_tokens:
disk, rsect, wsect, use = tokens[2], int(tokens[5]), int(tokens[9]), int(tokens[12])
sample.add_diskdata([rsect, wsect, use])
disk_stat_samples.append(sample)
disk_stats = []
for sample1, sample2 in zip(disk_stat_samples[:-1], disk_stat_samples[1:]):
interval = sample1.time - sample2.time
sums = [ a - b for a, b in zip(sample1.diskdata, sample2.diskdata) ]
readTput = sums[0] / 2.0 * 100.0 / interval
writeTput = sums[1] / 2.0 * 100.0 / interval
util = float( sums[2] ) / 10 / interval / numCpu
util = max(0.0, min(1.0, util))
disk_stats.append(DiskSample(sample2.time, readTput, writeTput, util))
return disk_stats
def get_num_cpus(headers):
"""Get the number of CPUs from the system.cpu header property. As the
CPU utilization graphs are relative, the number of CPUs currently makes
no difference."""
if headers is None:
return 1
cpu_model = headers.get("system.cpu")
if cpu_model is None:
return 1
mat = re.match(".*\\((\\d+)\\)", cpu_model)
if mat is None:
return 1
return int(mat.group(1))
class ParserState:
def __init__(self):
self.headers = None
self.disk_stats = None
self.ps_stats = None
self.cpu_stats = None
def valid(self):
return self.headers != None and self.disk_stats != None and self.ps_stats != None and self.cpu_stats != None
_relevant_files = set(["header", "proc_diskstats.log", "proc_ps.log", "proc_stat.log"])
def _do_parse(state, name, file):
if name == "header":
state.headers = _parse_headers(file)
elif name == "proc_diskstats.log":
state.disk_stats = _parse_proc_disk_stat_log(file, get_num_cpus(state.headers))
elif name == "proc_ps.log":
state.ps_stats = _parse_proc_ps_log(file)
elif name == "proc_stat.log":
state.cpu_stats = _parse_proc_stat_log(file)
return state
def parse_file(state, filename):
basename = os.path.basename(filename)
if not(basename in _relevant_files):
return state
with open(filename, "rb") as file:
return _do_parse(state, basename, file)
def parse_paths(state, paths):
for path in paths:
root,extension = os.path.splitext(path)
if not(os.path.exists(path)):
print "warning: path '%s' does not exist, ignoring." % path
continue
if os.path.isdir(path):
files = [ f for f in [os.path.join(path, f) for f in os.listdir(path)] if os.path.isfile(f) ]
files.sort()
state = parse_paths(state, files)
elif extension in [".tar", ".tgz", ".tar.gz"]:
tf = None
try:
tf = tarfile.open(path, 'r:*')
for name in tf.getnames():
state = _do_parse(state, name, tf.extractfile(name))
except tarfile.ReadError, error:
raise ParseError("error: could not read tarfile '%s': %s." % (path, error))
finally:
if tf != None:
tf.close()
else:
state = parse_file(state, path)
return state
def parse(paths, prune):
state = parse_paths(ParserState(), paths)
if not state.valid():
raise ParseError("empty state: '%s' does not contain a valid bootchart" % ", ".join(paths))
monitored_app = state.headers.get("profile.process")
proc_tree = ProcessTree(state.ps_stats, monitored_app, prune)
return (state.headers, state.cpu_stats, state.disk_stats, proc_tree)

View File

@@ -0,0 +1,270 @@
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 = 'bootchartd'
EXPLODER_PROCESSES = set(['hwup'])
def __init__(self, psstats, monitoredApp, prune, for_testing = False):
self.process_tree = []
self.psstats = psstats
self.process_list = sorted(psstats.process_list, key = lambda p: p.pid)
self.sample_period = psstats.sample_period
self.build()
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
if for_testing:
return
# print 'proc_tree before prune: num_proc=%i, duration=%i' % (self.num_nodes(self.process_list), self.duration)
removed = self.merge_logger(self.process_tree, self.LOGGER_PROC, monitoredApp, False)
print "Merged %i logger processes" % removed
if prune:
removed = self.prune(self.process_tree, None)
print "Pruned %i processes" % removed
removed = self.merge_exploders(self.process_tree, self.EXPLODER_PROCESSES)
print "Pruned %i exploders" % removed
removed = self.merge_siblings(self.process_tree)
print "Pruned %i threads" % removed
removed = self.merge_runs(self.process_tree)
print "Pruned %i runs" % removed
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 == 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 == 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)
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

View File

@@ -0,0 +1,93 @@
class DiskStatSample:
def __init__(self, time):
self.time = time
self.diskdata = [0, 0, 0]
def add_diskdata(self, new_diskdata):
self.diskdata = [ a + b for a, b in zip(self.diskdata, new_diskdata) ]
class CPUSample:
def __init__(self, time, user, sys, io):
self.time = time
self.user = user
self.sys = sys
self.io = io
def __str__(self):
return str(self.time) + "\t" + str(self.user) + "\t" + str(self.sys) + "\t" + str(self.io);
class ProcessSample:
def __init__(self, time, state, cpu_sample):
self.time = time
self.state = state
self.cpu_sample = cpu_sample
def __str__(self):
return str(self.time) + "\t" + str(self.state) + "\t" + str(self.cpu_sample);
class ProcessStats:
def __init__(self, process_list, sample_period, start_time, end_time):
self.process_list = process_list
self.sample_period = sample_period
self.start_time = start_time
self.end_time = end_time
class Process:
def __init__(self, pid, cmd, ppid, start_time):
self.pid = pid
self.cmd = cmd.strip('(').strip(')')
self.ppid = ppid
self.start_time = start_time
self.samples = []
self.parent = None
self.child_list = []
self.duration = 0
self.active = None
self.last_user_cpu_time = None
self.last_sys_cpu_time = None
def __str__(self):
return " ".join([str(self.pid), self.cmd, str(self.ppid), '[ ' + str(len(self.samples)) + ' samples ]' ])
def calc_stats(self, samplePeriod):
if self.samples:
firstSample = self.samples[0]
lastSample = self.samples[-1]
self.start_time = min(firstSample.time, self.start_time)
self.duration = lastSample.time - self.start_time + samplePeriod
activeCount = sum( [1 for sample in self.samples if sample.cpu_sample and sample.cpu_sample.sys + sample.cpu_sample.user + sample.cpu_sample.io > 0.0] )
activeCount = activeCount + sum( [1 for sample in self.samples if sample.state == 'D'] )
self.active = (activeCount>2)
def calc_load(self, userCpu, sysCpu, interval):
userCpuLoad = float(userCpu - self.last_user_cpu_time) / interval
sysCpuLoad = float(sysCpu - self.last_sys_cpu_time) / interval
cpuLoad = userCpuLoad + sysCpuLoad
# normalize
if cpuLoad > 1.0:
userCpuLoad = userCpuLoad / cpuLoad;
sysCpuLoad = sysCpuLoad / cpuLoad;
return (userCpuLoad, sysCpuLoad)
def set_parent(self, processMap):
if self.ppid != None:
self.parent = processMap.get(self.ppid)
if self.parent == None and self.pid > 1:
print "warning: no parent for pid '%i' with ppid '%i'" % (self.pid,self.ppid)
def get_end_time(self):
return self.start_time + self.duration
class DiskSample:
def __init__(self, time, read, write, util):
self.time = time
self.read = read
self.write = write
self.util = util
self.tput = read + write
def __str__(self):
return "\t".join([str(self.time), str(self.read), str(self.write), str(self.util)])

View File

@@ -0,0 +1,93 @@
import sys, os, re, struct, operator, math
from collections import defaultdict
import unittest
sys.path.insert(0, os.getcwd())
import parsing
debug = False
def floatEq(f1, f2):
return math.fabs(f1-f2) < 0.00001
class TestBCParser(unittest.TestCase):
def setUp(self):
self.name = "My first unittest"
self.rootdir = '../examples/1'
def mk_fname(self,f):
return os.path.join(self.rootdir, f)
def testParseHeader(self):
state = parsing.parse_file(parsing.ParserState(), self.mk_fname('header'))
self.assertEqual(6, len(state.headers))
self.assertEqual(2, parsing.get_num_cpus(state.headers))
def test_parseTimedBlocks(self):
state = parsing.parse_file(parsing.ParserState(), self.mk_fname('proc_diskstats.log'))
self.assertEqual(141, len(state.disk_stats))
def testParseProcPsLog(self):
state = parsing.parse_file(parsing.ParserState(), self.mk_fname('proc_ps.log'))
samples = state.ps_stats
processes = samples.process_list
sorted_processes = sorted(processes, key=lambda p: p.pid )
for index, line in enumerate(open(self.mk_fname('extract2.proc_ps.log'))):
tokens = line.split();
process = sorted_processes[index]
if debug:
print tokens[0:4]
print process.pid, process.cmd, process.ppid, len(process.samples)
print '-------------------'
self.assertEqual(tokens[0], str(process.pid))
self.assertEqual(tokens[1], str(process.cmd))
self.assertEqual(tokens[2], str(process.ppid))
self.assertEqual(tokens[3], str(len(process.samples)))
def testparseProcDiskStatLog(self):
state_with_headers = parsing.parse_file(parsing.ParserState(), self.mk_fname('header'))
state_with_headers.headers['system.cpu'] = 'xxx (2)'
samples = parsing.parse_file(state_with_headers, self.mk_fname('proc_diskstats.log')).disk_stats
self.assertEqual(141, len(samples))
for index, line in enumerate(open(self.mk_fname('extract.proc_diskstats.log'))):
tokens = line.split('\t')
sample = samples[index]
if debug:
print line.rstrip(),
print sample
print '-------------------'
self.assertEqual(tokens[0], str(sample.time))
self.assert_(floatEq(float(tokens[1]), sample.read))
self.assert_(floatEq(float(tokens[2]), sample.write))
self.assert_(floatEq(float(tokens[3]), sample.util))
def testparseProcStatLog(self):
samples = parsing.parse_file(parsing.ParserState(), self.mk_fname('proc_stat.log')).cpu_stats
self.assertEqual(141, len(samples))
for index, line in enumerate(open(self.mk_fname('extract.proc_stat.log'))):
tokens = line.split('\t')
sample = samples[index]
if debug:
print line.rstrip()
print sample
print '-------------------'
self.assert_(floatEq(float(tokens[0]), sample.time))
self.assert_(floatEq(float(tokens[1]), sample.user))
self.assert_(floatEq(float(tokens[2]), sample.sys))
self.assert_(floatEq(float(tokens[3]), sample.io))
def testParseLogDir(self):
res = parsing.parse([self.rootdir], False)
self.assertEqual(4, len(res))
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,78 @@
import sys
import os
import unittest
sys.path.insert(0, os.getcwd())
import parsing
import process_tree
class TestProcessTree(unittest.TestCase):
def setUp(self):
self.name = "Process tree unittest"
self.rootdir = '../examples/1'
self.ps_stats = parsing.parse_file(parsing.ParserState(), self.mk_fname('proc_ps.log')).ps_stats
self.processtree = process_tree.ProcessTree(self.ps_stats, None, False, for_testing = True)
def mk_fname(self,f):
return os.path.join(self.rootdir, f)
def flatten(self, process_tree):
flattened = []
for p in process_tree:
flattened.append(p)
flattened.extend(self.flatten(p.child_list))
return flattened
def checkAgainstJavaExtract(self, filename, process_tree):
for expected, actual in zip(open(filename), self.flatten(process_tree)):
tokens = expected.split('\t')
self.assertEqual(int(tokens[0]), actual.pid)
self.assertEqual(tokens[1], actual.cmd)
self.assertEqual(long(tokens[2]), 10 * actual.start_time)
self.assert_(long(tokens[3]) - 10 * actual.duration < 5, "duration")
self.assertEqual(int(tokens[4]), len(actual.child_list))
self.assertEqual(int(tokens[5]), len(actual.samples))
def testBuild(self):
process_tree = self.processtree.process_tree
self.checkAgainstJavaExtract(self.mk_fname('extract.processtree.1.log'), process_tree)
def testMergeLogger(self):
self.processtree.merge_logger(self.processtree.process_tree, 'bootchartd', None, False)
process_tree = self.processtree.process_tree
self.checkAgainstJavaExtract(self.mk_fname('extract.processtree.2.log'), process_tree)
def testPrune(self):
self.processtree.merge_logger(self.processtree.process_tree, 'bootchartd', None, False)
self.processtree.prune(self.processtree.process_tree, None)
process_tree = self.processtree.process_tree
self.checkAgainstJavaExtract(self.mk_fname('extract.processtree.3b.log'), process_tree)
def testMergeExploders(self):
self.processtree.merge_logger(self.processtree.process_tree, 'bootchartd', None, False)
self.processtree.prune(self.processtree.process_tree, None)
self.processtree.merge_exploders(self.processtree.process_tree, set(['hwup']))
process_tree = self.processtree.process_tree
self.checkAgainstJavaExtract(self.mk_fname('extract.processtree.3c.log'), process_tree)
def testMergeSiblings(self):
self.processtree.merge_logger(self.processtree.process_tree, 'bootchartd', None, False)
self.processtree.prune(self.processtree.process_tree, None)
self.processtree.merge_exploders(self.processtree.process_tree, set(['hwup']))
self.processtree.merge_siblings(self.processtree.process_tree)
process_tree = self.processtree.process_tree
self.checkAgainstJavaExtract(self.mk_fname('extract.processtree.3d.log'), process_tree)
def testMergeRuns(self):
self.processtree.merge_logger(self.processtree.process_tree, 'bootchartd', None, False)
self.processtree.prune(self.processtree.process_tree, None)
self.processtree.merge_exploders(self.processtree.process_tree, set(['hwup']))
self.processtree.merge_siblings(self.processtree.process_tree)
self.processtree.merge_runs(self.processtree.process_tree)
process_tree = self.processtree.process_tree
self.checkAgainstJavaExtract(self.mk_fname('extract.processtree.3e.log'), process_tree)
if __name__ == '__main__':
unittest.main()