summaryrefslogtreecommitdiffstats
path: root/scripts/pybootchartgui/pybootchartgui
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/pybootchartgui/pybootchartgui')
-rw-r--r--scripts/pybootchartgui/pybootchartgui/__init__.py0
-rw-r--r--scripts/pybootchartgui/pybootchartgui/batch.py46
-rw-r--r--scripts/pybootchartgui/pybootchartgui/draw.py894
-rw-r--r--scripts/pybootchartgui/pybootchartgui/gui.py350
l---------scripts/pybootchartgui/pybootchartgui/main.py1
-rw-r--r--scripts/pybootchartgui/pybootchartgui/main.py.in187
-rw-r--r--scripts/pybootchartgui/pybootchartgui/parsing.py740
-rw-r--r--scripts/pybootchartgui/pybootchartgui/process_tree.py292
-rw-r--r--scripts/pybootchartgui/pybootchartgui/samples.py151
-rw-r--r--scripts/pybootchartgui/pybootchartgui/tests/parser_test.py105
-rw-r--r--scripts/pybootchartgui/pybootchartgui/tests/process_tree_test.py92
11 files changed, 2858 insertions, 0 deletions
diff --git a/scripts/pybootchartgui/pybootchartgui/__init__.py b/scripts/pybootchartgui/pybootchartgui/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/__init__.py
diff --git a/scripts/pybootchartgui/pybootchartgui/batch.py b/scripts/pybootchartgui/pybootchartgui/batch.py
new file mode 100644
index 0000000000..05c714e95e
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/batch.py
@@ -0,0 +1,46 @@
1# This file is part of pybootchartgui.
2
3# pybootchartgui is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7
8# pybootchartgui is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12
13# You should have received a copy of the GNU General Public License
14# along with pybootchartgui. If not, see <http://www.gnu.org/licenses/>.
15
16import cairo
17from . import draw
18from .draw import RenderOptions
19
20def render(writer, trace, app_options, filename):
21 handlers = {
22 "png": (lambda w, h: cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h), \
23 lambda sfc: sfc.write_to_png(filename)),
24 "pdf": (lambda w, h: cairo.PDFSurface(filename, w, h), lambda sfc: 0),
25 "svg": (lambda w, h: cairo.SVGSurface(filename, w, h), lambda sfc: 0)
26 }
27
28 if app_options.format is None:
29 fmt = filename.rsplit('.', 1)[1]
30 else:
31 fmt = app_options.format
32
33 if not (fmt in handlers):
34 writer.error ("Unknown format '%s'." % fmt)
35 return 10
36
37 make_surface, write_surface = handlers[fmt]
38 options = RenderOptions (app_options)
39 (w, h) = draw.extents (options, 1.0, trace)
40 w = max (w, draw.MIN_IMG_W)
41 surface = make_surface (w, h)
42 ctx = cairo.Context (surface)
43 draw.render (ctx, options, 1.0, trace)
44 write_surface (surface)
45 writer.status ("bootchart written to '%s'" % filename)
46
diff --git a/scripts/pybootchartgui/pybootchartgui/draw.py b/scripts/pybootchartgui/pybootchartgui/draw.py
new file mode 100644
index 0000000000..8c574be50c
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/draw.py
@@ -0,0 +1,894 @@
1# This file is part of pybootchartgui.
2
3# pybootchartgui is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7
8# pybootchartgui is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12
13# You should have received a copy of the GNU General Public License
14# along with pybootchartgui. If not, see <http://www.gnu.org/licenses/>.
15
16
17import cairo
18import math
19import re
20import random
21import colorsys
22from operator import itemgetter
23
24class RenderOptions:
25
26 def __init__(self, app_options):
27 # should we render a cumulative CPU time chart
28 self.cumulative = True
29 self.charts = True
30 self.kernel_only = False
31 self.app_options = app_options
32
33 def proc_tree (self, trace):
34 if self.kernel_only:
35 return trace.kernel_tree
36 else:
37 return trace.proc_tree
38
39# Process tree background color.
40BACK_COLOR = (1.0, 1.0, 1.0, 1.0)
41
42WHITE = (1.0, 1.0, 1.0, 1.0)
43# Process tree border color.
44BORDER_COLOR = (0.63, 0.63, 0.63, 1.0)
45# Second tick line color.
46TICK_COLOR = (0.92, 0.92, 0.92, 1.0)
47# 5-second tick line color.
48TICK_COLOR_BOLD = (0.86, 0.86, 0.86, 1.0)
49# Annotation colour
50ANNOTATION_COLOR = (0.63, 0.0, 0.0, 0.5)
51# Text color.
52TEXT_COLOR = (0.0, 0.0, 0.0, 1.0)
53
54# Font family
55FONT_NAME = "Bitstream Vera Sans"
56# Title text font.
57TITLE_FONT_SIZE = 18
58# Default text font.
59TEXT_FONT_SIZE = 12
60# Axis label font.
61AXIS_FONT_SIZE = 11
62# Legend font.
63LEGEND_FONT_SIZE = 12
64
65# CPU load chart color.
66CPU_COLOR = (0.40, 0.55, 0.70, 1.0)
67# IO wait chart color.
68IO_COLOR = (0.76, 0.48, 0.48, 0.5)
69# Disk throughput color.
70DISK_TPUT_COLOR = (0.20, 0.71, 0.20, 1.0)
71# CPU load chart color.
72FILE_OPEN_COLOR = (0.20, 0.71, 0.71, 1.0)
73# Mem cached color
74MEM_CACHED_COLOR = CPU_COLOR
75# Mem used color
76MEM_USED_COLOR = IO_COLOR
77# Buffers color
78MEM_BUFFERS_COLOR = (0.4, 0.4, 0.4, 0.3)
79# Swap color
80MEM_SWAP_COLOR = DISK_TPUT_COLOR
81
82# Process border color.
83PROC_BORDER_COLOR = (0.71, 0.71, 0.71, 1.0)
84# Waiting process color.
85PROC_COLOR_D = (0.76, 0.48, 0.48, 0.5)
86# Running process color.
87PROC_COLOR_R = CPU_COLOR
88# Sleeping process color.
89PROC_COLOR_S = (0.94, 0.94, 0.94, 1.0)
90# Stopped process color.
91PROC_COLOR_T = (0.94, 0.50, 0.50, 1.0)
92# Zombie process color.
93PROC_COLOR_Z = (0.71, 0.71, 0.71, 1.0)
94# Dead process color.
95PROC_COLOR_X = (0.71, 0.71, 0.71, 0.125)
96# Paging process color.
97PROC_COLOR_W = (0.71, 0.71, 0.71, 0.125)
98
99# Process label color.
100PROC_TEXT_COLOR = (0.19, 0.19, 0.19, 1.0)
101# Process label font.
102PROC_TEXT_FONT_SIZE = 12
103
104# Signature color.
105SIG_COLOR = (0.0, 0.0, 0.0, 0.3125)
106# Signature font.
107SIG_FONT_SIZE = 14
108# Signature text.
109SIGNATURE = "http://github.com/mmeeks/bootchart"
110
111# Process dependency line color.
112DEP_COLOR = (0.75, 0.75, 0.75, 1.0)
113# Process dependency line stroke.
114DEP_STROKE = 1.0
115
116# Process description date format.
117DESC_TIME_FORMAT = "mm:ss.SSS"
118
119# Cumulative coloring bits
120HSV_MAX_MOD = 31
121HSV_STEP = 7
122
123# Configure task color
124TASK_COLOR_CONFIGURE = (1.0, 1.0, 0.00, 1.0)
125# Compile task color.
126TASK_COLOR_COMPILE = (0.0, 1.00, 0.00, 1.0)
127# Install task color
128TASK_COLOR_INSTALL = (1.0, 0.00, 1.00, 1.0)
129# Sysroot task color
130TASK_COLOR_SYSROOT = (0.0, 0.00, 1.00, 1.0)
131# Package task color
132TASK_COLOR_PACKAGE = (0.0, 1.00, 1.00, 1.0)
133# Package Write RPM/DEB/IPK task color
134TASK_COLOR_PACKAGE_WRITE = (0.0, 0.50, 0.50, 1.0)
135
136# Process states
137STATE_UNDEFINED = 0
138STATE_RUNNING = 1
139STATE_SLEEPING = 2
140STATE_WAITING = 3
141STATE_STOPPED = 4
142STATE_ZOMBIE = 5
143
144STATE_COLORS = [(0, 0, 0, 0), PROC_COLOR_R, PROC_COLOR_S, PROC_COLOR_D, \
145 PROC_COLOR_T, PROC_COLOR_Z, PROC_COLOR_X, PROC_COLOR_W]
146
147# CumulativeStats Types
148STAT_TYPE_CPU = 0
149STAT_TYPE_IO = 1
150
151# Convert ps process state to an int
152def get_proc_state(flag):
153 return "RSDTZXW".find(flag) + 1
154
155def draw_text(ctx, text, color, x, y):
156 ctx.set_source_rgba(*color)
157 ctx.move_to(x, y)
158 ctx.show_text(text)
159
160def draw_fill_rect(ctx, color, rect):
161 ctx.set_source_rgba(*color)
162 ctx.rectangle(*rect)
163 ctx.fill()
164
165def draw_rect(ctx, color, rect):
166 ctx.set_source_rgba(*color)
167 ctx.rectangle(*rect)
168 ctx.stroke()
169
170def draw_legend_box(ctx, label, fill_color, x, y, s):
171 draw_fill_rect(ctx, fill_color, (x, y - s, s, s))
172 draw_rect(ctx, PROC_BORDER_COLOR, (x, y - s, s, s))
173 draw_text(ctx, label, TEXT_COLOR, x + s + 5, y)
174
175def draw_legend_line(ctx, label, fill_color, x, y, s):
176 draw_fill_rect(ctx, fill_color, (x, y - s/2, s + 1, 3))
177 ctx.arc(x + (s + 1)/2.0, y - (s - 3)/2.0, 2.5, 0, 2.0 * math.pi)
178 ctx.fill()
179 draw_text(ctx, label, TEXT_COLOR, x + s + 5, y)
180
181def draw_label_in_box(ctx, color, label, x, y, w, maxx):
182 label_w = ctx.text_extents(label)[2]
183 label_x = x + w / 2 - label_w / 2
184 if label_w + 10 > w:
185 label_x = x + w + 5
186 if label_x + label_w > maxx:
187 label_x = x - label_w - 5
188 draw_text(ctx, label, color, label_x, y)
189
190def draw_sec_labels(ctx, options, rect, sec_w, nsecs):
191 ctx.set_font_size(AXIS_FONT_SIZE)
192 prev_x = 0
193 for i in range(0, rect[2] + 1, sec_w):
194 if ((i / sec_w) % nsecs == 0) :
195 if options.app_options.as_minutes :
196 label = "%.1f" % (i / sec_w / 60.0)
197 else :
198 label = "%d" % (i / sec_w)
199 label_w = ctx.text_extents(label)[2]
200 x = rect[0] + i - label_w/2
201 if x >= prev_x:
202 draw_text(ctx, label, TEXT_COLOR, x, rect[1] - 2)
203 prev_x = x + label_w
204
205def draw_box_ticks(ctx, rect, sec_w):
206 draw_rect(ctx, BORDER_COLOR, tuple(rect))
207
208 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
209
210 for i in range(sec_w, rect[2] + 1, sec_w):
211 if ((i / sec_w) % 10 == 0) :
212 ctx.set_line_width(1.5)
213 elif sec_w < 5 :
214 continue
215 else :
216 ctx.set_line_width(1.0)
217 if ((i / sec_w) % 30 == 0) :
218 ctx.set_source_rgba(*TICK_COLOR_BOLD)
219 else :
220 ctx.set_source_rgba(*TICK_COLOR)
221 ctx.move_to(rect[0] + i, rect[1] + 1)
222 ctx.line_to(rect[0] + i, rect[1] + rect[3] - 1)
223 ctx.stroke()
224 ctx.set_line_width(1.0)
225
226 ctx.set_line_cap(cairo.LINE_CAP_BUTT)
227
228def draw_annotations(ctx, proc_tree, times, rect):
229 ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
230 ctx.set_source_rgba(*ANNOTATION_COLOR)
231 ctx.set_dash([4, 4])
232
233 for time in times:
234 if time is not None:
235 x = ((time - proc_tree.start_time) * rect[2] / proc_tree.duration)
236
237 ctx.move_to(rect[0] + x, rect[1] + 1)
238 ctx.line_to(rect[0] + x, rect[1] + rect[3] - 1)
239 ctx.stroke()
240
241 ctx.set_line_cap(cairo.LINE_CAP_BUTT)
242 ctx.set_dash([])
243
244def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree, data_range):
245 ctx.set_line_width(0.5)
246 x_shift = proc_tree.start_time
247
248 def transform_point_coords(point, x_base, y_base, \
249 xscale, yscale, x_trans, y_trans):
250 x = (point[0] - x_base) * xscale + x_trans
251 y = (point[1] - y_base) * -yscale + y_trans + chart_bounds[3]
252 return x, y
253
254 max_x = max (x for (x, y) in data)
255 max_y = max (y for (x, y) in data)
256 # avoid divide by zero
257 if max_y == 0:
258 max_y = 1.0
259 xscale = float (chart_bounds[2]) / max_x
260 # If data_range is given, scale the chart so that the value range in
261 # data_range matches the chart bounds exactly.
262 # Otherwise, scale so that the actual data matches the chart bounds.
263 if data_range:
264 yscale = float(chart_bounds[3]) / (data_range[1] - data_range[0])
265 ybase = data_range[0]
266 else:
267 yscale = float(chart_bounds[3]) / max_y
268 ybase = 0
269
270 first = transform_point_coords (data[0], x_shift, ybase, xscale, yscale, \
271 chart_bounds[0], chart_bounds[1])
272 last = transform_point_coords (data[-1], x_shift, ybase, xscale, yscale, \
273 chart_bounds[0], chart_bounds[1])
274
275 ctx.set_source_rgba(*color)
276 ctx.move_to(*first)
277 for point in data:
278 x, y = transform_point_coords (point, x_shift, ybase, xscale, yscale, \
279 chart_bounds[0], chart_bounds[1])
280 ctx.line_to(x, y)
281 if fill:
282 ctx.stroke_preserve()
283 ctx.line_to(last[0], chart_bounds[1]+chart_bounds[3])
284 ctx.line_to(first[0], chart_bounds[1]+chart_bounds[3])
285 ctx.line_to(first[0], first[1])
286 ctx.fill()
287 else:
288 ctx.stroke()
289 ctx.set_line_width(1.0)
290
291bar_h = 55
292meminfo_bar_h = 2 * bar_h
293header_h = 60
294# offsets
295off_x, off_y = 220, 10
296sec_w_base = 1 # the width of a second
297proc_h = 16 # the height of a process
298leg_s = 10
299MIN_IMG_W = 800
300CUML_HEIGHT = 2000 # Increased value to accomodate CPU and I/O Graphs
301OPTIONS = None
302
303def extents(options, xscale, trace):
304 start = min(trace.start.keys())
305 end = start
306
307 processes = 0
308 for proc in trace.processes:
309 if not options.app_options.show_all and \
310 trace.processes[proc][1] - trace.processes[proc][0] < options.app_options.mintime:
311 continue
312
313 if trace.processes[proc][1] > end:
314 end = trace.processes[proc][1]
315 processes += 1
316
317 if trace.min is not None and trace.max is not None:
318 start = trace.min
319 end = trace.max
320
321 w = int ((end - start) * sec_w_base * xscale) + 2 * off_x
322 h = proc_h * processes + header_h + 2 * off_y
323
324 return (w, h)
325
326def clip_visible(clip, rect):
327 xmax = max (clip[0], rect[0])
328 ymax = max (clip[1], rect[1])
329 xmin = min (clip[0] + clip[2], rect[0] + rect[2])
330 ymin = min (clip[1] + clip[3], rect[1] + rect[3])
331 return (xmin > xmax and ymin > ymax)
332
333def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w):
334 proc_tree = options.proc_tree(trace)
335
336 # render bar legend
337 ctx.set_font_size(LEGEND_FONT_SIZE)
338
339 draw_legend_box(ctx, "CPU (user+sys)", CPU_COLOR, off_x, curr_y+20, leg_s)
340 draw_legend_box(ctx, "I/O (wait)", IO_COLOR, off_x + 120, curr_y+20, leg_s)
341
342 # render I/O wait
343 chart_rect = (off_x, curr_y+30, w, bar_h)
344 if clip_visible (clip, chart_rect):
345 draw_box_ticks (ctx, chart_rect, sec_w)
346 draw_annotations (ctx, proc_tree, trace.times, chart_rect)
347 draw_chart (ctx, IO_COLOR, True, chart_rect, \
348 [(sample.time, sample.user + sample.sys + sample.io) for sample in trace.cpu_stats], \
349 proc_tree, None)
350 # render CPU load
351 draw_chart (ctx, CPU_COLOR, True, chart_rect, \
352 [(sample.time, sample.user + sample.sys) for sample in trace.cpu_stats], \
353 proc_tree, None)
354
355 curr_y = curr_y + 30 + bar_h
356
357 # render second chart
358 draw_legend_line(ctx, "Disk throughput", DISK_TPUT_COLOR, off_x, curr_y+20, leg_s)
359 draw_legend_box(ctx, "Disk utilization", IO_COLOR, off_x + 120, curr_y+20, leg_s)
360
361 # render I/O utilization
362 chart_rect = (off_x, curr_y+30, w, bar_h)
363 if clip_visible (clip, chart_rect):
364 draw_box_ticks (ctx, chart_rect, sec_w)
365 draw_annotations (ctx, proc_tree, trace.times, chart_rect)
366 draw_chart (ctx, IO_COLOR, True, chart_rect, \
367 [(sample.time, sample.util) for sample in trace.disk_stats], \
368 proc_tree, None)
369
370 # render disk throughput
371 max_sample = max (trace.disk_stats, key = lambda s: s.tput)
372 if clip_visible (clip, chart_rect):
373 draw_chart (ctx, DISK_TPUT_COLOR, False, chart_rect, \
374 [(sample.time, sample.tput) for sample in trace.disk_stats], \
375 proc_tree, None)
376
377 pos_x = off_x + ((max_sample.time - proc_tree.start_time) * w / proc_tree.duration)
378
379 shift_x, shift_y = -20, 20
380 if (pos_x < off_x + 245):
381 shift_x, shift_y = 5, 40
382
383 label = "%dMB/s" % round ((max_sample.tput) / 1024.0)
384 draw_text (ctx, label, DISK_TPUT_COLOR, pos_x + shift_x, curr_y + shift_y)
385
386 curr_y = curr_y + 30 + bar_h
387
388 # render mem usage
389 chart_rect = (off_x, curr_y+30, w, meminfo_bar_h)
390 mem_stats = trace.mem_stats
391 if mem_stats and clip_visible (clip, chart_rect):
392 mem_scale = max(sample.records['MemTotal'] - sample.records['MemFree'] for sample in mem_stats)
393 draw_legend_box(ctx, "Mem cached (scale: %u MiB)" % (float(mem_scale) / 1024), MEM_CACHED_COLOR, off_x, curr_y+20, leg_s)
394 draw_legend_box(ctx, "Used", MEM_USED_COLOR, off_x + 240, curr_y+20, leg_s)
395 draw_legend_box(ctx, "Buffers", MEM_BUFFERS_COLOR, off_x + 360, curr_y+20, leg_s)
396 draw_legend_line(ctx, "Swap (scale: %u MiB)" % max([(sample.records['SwapTotal'] - sample.records['SwapFree'])/1024 for sample in mem_stats]), \
397 MEM_SWAP_COLOR, off_x + 480, curr_y+20, leg_s)
398 draw_box_ticks(ctx, chart_rect, sec_w)
399 draw_annotations(ctx, proc_tree, trace.times, chart_rect)
400 draw_chart(ctx, MEM_BUFFERS_COLOR, True, chart_rect, \
401 [(sample.time, sample.records['MemTotal'] - sample.records['MemFree']) for sample in trace.mem_stats], \
402 proc_tree, [0, mem_scale])
403 draw_chart(ctx, MEM_USED_COLOR, True, chart_rect, \
404 [(sample.time, sample.records['MemTotal'] - sample.records['MemFree'] - sample.records['Buffers']) for sample in mem_stats], \
405 proc_tree, [0, mem_scale])
406 draw_chart(ctx, MEM_CACHED_COLOR, True, chart_rect, \
407 [(sample.time, sample.records['Cached']) for sample in mem_stats], \
408 proc_tree, [0, mem_scale])
409 draw_chart(ctx, MEM_SWAP_COLOR, False, chart_rect, \
410 [(sample.time, float(sample.records['SwapTotal'] - sample.records['SwapFree'])) for sample in mem_stats], \
411 proc_tree, None)
412
413 curr_y = curr_y + meminfo_bar_h
414
415 return curr_y
416
417def render_processes_chart(ctx, options, trace, curr_y, w, h, sec_w):
418 chart_rect = [off_x, curr_y+header_h, w, h - 2 * off_y - (curr_y+header_h) + proc_h]
419
420 draw_legend_box (ctx, "Configure", \
421 TASK_COLOR_CONFIGURE, off_x , curr_y + 45, leg_s)
422 draw_legend_box (ctx, "Compile", \
423 TASK_COLOR_COMPILE, off_x+120, curr_y + 45, leg_s)
424 draw_legend_box (ctx, "Install", \
425 TASK_COLOR_INSTALL, off_x+240, curr_y + 45, leg_s)
426 draw_legend_box (ctx, "Populate Sysroot", \
427 TASK_COLOR_SYSROOT, off_x+360, curr_y + 45, leg_s)
428 draw_legend_box (ctx, "Package", \
429 TASK_COLOR_PACKAGE, off_x+480, curr_y + 45, leg_s)
430 draw_legend_box (ctx, "Package Write",
431 TASK_COLOR_PACKAGE_WRITE, off_x+600, curr_y + 45, leg_s)
432
433 ctx.set_font_size(PROC_TEXT_FONT_SIZE)
434
435 draw_box_ticks(ctx, chart_rect, sec_w)
436 draw_sec_labels(ctx, options, chart_rect, sec_w, 30)
437
438 y = curr_y+header_h
439
440 offset = trace.min or min(trace.start.keys())
441 for s in sorted(trace.start.keys()):
442 for val in sorted(trace.start[s]):
443 if not options.app_options.show_all and \
444 trace.processes[val][1] - s < options.app_options.mintime:
445 continue
446 task = val.split(":")[1]
447 #print val
448 #print trace.processes[val][1]
449 #print s
450 x = chart_rect[0] + (s - offset) * sec_w
451 w = ((trace.processes[val][1] - s) * sec_w)
452
453 #print "proc at %s %s %s %s" % (x, y, w, proc_h)
454 col = None
455 if task == "do_compile":
456 col = TASK_COLOR_COMPILE
457 elif task == "do_configure":
458 col = TASK_COLOR_CONFIGURE
459 elif task == "do_install":
460 col = TASK_COLOR_INSTALL
461 elif task == "do_populate_sysroot":
462 col = TASK_COLOR_SYSROOT
463 elif task == "do_package":
464 col = TASK_COLOR_PACKAGE
465 elif task == "do_package_write_rpm" or \
466 task == "do_package_write_deb" or \
467 task == "do_package_write_ipk":
468 col = TASK_COLOR_PACKAGE_WRITE
469 else:
470 col = WHITE
471
472 if col:
473 draw_fill_rect(ctx, col, (x, y, w, proc_h))
474 draw_rect(ctx, PROC_BORDER_COLOR, (x, y, w, proc_h))
475
476 draw_label_in_box(ctx, PROC_TEXT_COLOR, val, x, y + proc_h - 4, w, proc_h)
477 y = y + proc_h
478
479 return curr_y
480
481#
482# Render the chart.
483#
484def render(ctx, options, xscale, trace):
485 (w, h) = extents (options, xscale, trace)
486 global OPTIONS
487 OPTIONS = options.app_options
488
489 # x, y, w, h
490 clip = ctx.clip_extents()
491
492 sec_w = int (xscale * sec_w_base)
493 ctx.set_line_width(1.0)
494 ctx.select_font_face(FONT_NAME)
495 draw_fill_rect(ctx, WHITE, (0, 0, max(w, MIN_IMG_W), h))
496 w -= 2*off_x
497 curr_y = off_y;
498
499 curr_y = render_processes_chart (ctx, options, trace, curr_y, w, h, sec_w)
500
501 return
502
503 proc_tree = options.proc_tree (trace)
504
505 # draw the title and headers
506 if proc_tree.idle:
507 duration = proc_tree.idle
508 else:
509 duration = proc_tree.duration
510
511 if not options.kernel_only:
512 curr_y = draw_header (ctx, trace.headers, duration)
513 else:
514 curr_y = off_y;
515
516 if options.charts:
517 curr_y = render_charts (ctx, options, clip, trace, curr_y, w, h, sec_w)
518
519 # draw process boxes
520 proc_height = h
521 if proc_tree.taskstats and options.cumulative:
522 proc_height -= CUML_HEIGHT
523
524 draw_process_bar_chart(ctx, clip, options, proc_tree, trace.times,
525 curr_y, w, proc_height, sec_w)
526
527 curr_y = proc_height
528 ctx.set_font_size(SIG_FONT_SIZE)
529 draw_text(ctx, SIGNATURE, SIG_COLOR, off_x + 5, proc_height - 8)
530
531 # draw a cumulative CPU-time-per-process graph
532 if proc_tree.taskstats and options.cumulative:
533 cuml_rect = (off_x, curr_y + off_y, w, CUML_HEIGHT/2 - off_y * 2)
534 if clip_visible (clip, cuml_rect):
535 draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, sec_w, STAT_TYPE_CPU)
536
537 # draw a cumulative I/O-time-per-process graph
538 if proc_tree.taskstats and options.cumulative:
539 cuml_rect = (off_x, curr_y + off_y * 100, w, CUML_HEIGHT/2 - off_y * 2)
540 if clip_visible (clip, cuml_rect):
541 draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, sec_w, STAT_TYPE_IO)
542
543def draw_process_bar_chart(ctx, clip, options, proc_tree, times, curr_y, w, h, sec_w):
544 header_size = 0
545 if not options.kernel_only:
546 draw_legend_box (ctx, "Running (%cpu)",
547 PROC_COLOR_R, off_x , curr_y + 45, leg_s)
548 draw_legend_box (ctx, "Unint.sleep (I/O)",
549 PROC_COLOR_D, off_x+120, curr_y + 45, leg_s)
550 draw_legend_box (ctx, "Sleeping",
551 PROC_COLOR_S, off_x+240, curr_y + 45, leg_s)
552 draw_legend_box (ctx, "Zombie",
553 PROC_COLOR_Z, off_x+360, curr_y + 45, leg_s)
554 header_size = 45
555
556 chart_rect = [off_x, curr_y + header_size + 15,
557 w, h - 2 * off_y - (curr_y + header_size + 15) + proc_h]
558 ctx.set_font_size (PROC_TEXT_FONT_SIZE)
559
560 draw_box_ticks (ctx, chart_rect, sec_w)
561 if sec_w > 100:
562 nsec = 1
563 else:
564 nsec = 5
565 draw_sec_labels (ctx, options, chart_rect, sec_w, nsec)
566 draw_annotations (ctx, proc_tree, times, chart_rect)
567
568 y = curr_y + 60
569 for root in proc_tree.process_tree:
570 draw_processes_recursively(ctx, root, proc_tree, y, proc_h, chart_rect, clip)
571 y = y + proc_h * proc_tree.num_nodes([root])
572
573
574def draw_header (ctx, headers, duration):
575 toshow = [
576 ('system.uname', 'uname', lambda s: s),
577 ('system.release', 'release', lambda s: s),
578 ('system.cpu', 'CPU', lambda s: re.sub('model name\s*:\s*', '', s, 1)),
579 ('system.kernel.options', 'kernel options', lambda s: s),
580 ]
581
582 header_y = ctx.font_extents()[2] + 10
583 ctx.set_font_size(TITLE_FONT_SIZE)
584 draw_text(ctx, headers['title'], TEXT_COLOR, off_x, header_y)
585 ctx.set_font_size(TEXT_FONT_SIZE)
586
587 for (headerkey, headertitle, mangle) in toshow:
588 header_y += ctx.font_extents()[2]
589 if headerkey in headers:
590 value = headers.get(headerkey)
591 else:
592 value = ""
593 txt = headertitle + ': ' + mangle(value)
594 draw_text(ctx, txt, TEXT_COLOR, off_x, header_y)
595
596 dur = duration / 100.0
597 txt = 'time : %02d:%05.2f' % (math.floor(dur/60), dur - 60 * math.floor(dur/60))
598 if headers.get('system.maxpid') is not None:
599 txt = txt + ' max pid: %s' % (headers.get('system.maxpid'))
600
601 header_y += ctx.font_extents()[2]
602 draw_text (ctx, txt, TEXT_COLOR, off_x, header_y)
603
604 return header_y
605
606def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) :
607 x = rect[0] + ((proc.start_time - proc_tree.start_time) * rect[2] / proc_tree.duration)
608 w = ((proc.duration) * rect[2] / proc_tree.duration)
609
610 draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip)
611 draw_rect(ctx, PROC_BORDER_COLOR, (x, y, w, proc_h))
612 ipid = int(proc.pid)
613 if not OPTIONS.show_all:
614 cmdString = proc.cmd
615 else:
616 cmdString = ''
617 if (OPTIONS.show_pid or OPTIONS.show_all) and ipid is not 0:
618 cmdString = cmdString + " [" + str(ipid // 1000) + "]"
619 if OPTIONS.show_all:
620 if proc.args:
621 cmdString = cmdString + " '" + "' '".join(proc.args) + "'"
622 else:
623 cmdString = cmdString + " " + proc.exe
624
625 draw_label_in_box(ctx, PROC_TEXT_COLOR, cmdString, x, y + proc_h - 4, w, rect[0] + rect[2])
626
627 next_y = y + proc_h
628 for child in proc.child_list:
629 if next_y > clip[1] + clip[3]:
630 break
631 child_x, child_y = draw_processes_recursively(ctx, child, proc_tree, next_y, proc_h, rect, clip)
632 draw_process_connecting_lines(ctx, x, y, child_x, child_y, proc_h)
633 next_y = next_y + proc_h * proc_tree.num_nodes([child])
634
635 return x, y
636
637
638def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip):
639
640 if y > clip[1] + clip[3] or y + proc_h + 2 < clip[1]:
641 return
642
643 draw_fill_rect(ctx, PROC_COLOR_S, (x, y, w, proc_h))
644
645 last_tx = -1
646 for sample in proc.samples :
647 tx = rect[0] + round(((sample.time - proc_tree.start_time) * rect[2] / proc_tree.duration))
648
649 # samples are sorted chronologically
650 if tx < clip[0]:
651 continue
652 if tx > clip[0] + clip[2]:
653 break
654
655 tw = round(proc_tree.sample_period * rect[2] / float(proc_tree.duration))
656 if last_tx != -1 and abs(last_tx - tx) <= tw:
657 tw -= last_tx - tx
658 tx = last_tx
659 tw = max (tw, 1) # nice to see at least something
660
661 last_tx = tx + tw
662 state = get_proc_state( sample.state )
663
664 color = STATE_COLORS[state]
665 if state == STATE_RUNNING:
666 alpha = min (sample.cpu_sample.user + sample.cpu_sample.sys, 1.0)
667 color = tuple(list(PROC_COLOR_R[0:3]) + [alpha])
668# print "render time %d [ tx %d tw %d ], sample state %s color %s alpha %g" % (sample.time, tx, tw, state, color, alpha)
669 elif state == STATE_SLEEPING:
670 continue
671
672 draw_fill_rect(ctx, color, (tx, y, tw, proc_h))
673
674def draw_process_connecting_lines(ctx, px, py, x, y, proc_h):
675 ctx.set_source_rgba(*DEP_COLOR)
676 ctx.set_dash([2, 2])
677 if abs(px - x) < 3:
678 dep_off_x = 3
679 dep_off_y = proc_h / 4
680 ctx.move_to(x, y + proc_h / 2)
681 ctx.line_to(px - dep_off_x, y + proc_h / 2)
682 ctx.line_to(px - dep_off_x, py - dep_off_y)
683 ctx.line_to(px, py - dep_off_y)
684 else:
685 ctx.move_to(x, y + proc_h / 2)
686 ctx.line_to(px, y + proc_h / 2)
687 ctx.line_to(px, py)
688 ctx.stroke()
689 ctx.set_dash([])
690
691# elide the bootchart collector - it is quite distorting
692def elide_bootchart(proc):
693 return proc.cmd == 'bootchartd' or proc.cmd == 'bootchart-colle'
694
695class CumlSample:
696 def __init__(self, proc):
697 self.cmd = proc.cmd
698 self.samples = []
699 self.merge_samples (proc)
700 self.color = None
701
702 def merge_samples(self, proc):
703 self.samples.extend (proc.samples)
704 self.samples.sort (key = lambda p: p.time)
705
706 def next(self):
707 global palette_idx
708 palette_idx += HSV_STEP
709 return palette_idx
710
711 def get_color(self):
712 if self.color is None:
713 i = self.next() % HSV_MAX_MOD
714 h = 0.0
715 if i is not 0:
716 h = (1.0 * i) / HSV_MAX_MOD
717 s = 0.5
718 v = 1.0
719 c = colorsys.hsv_to_rgb (h, s, v)
720 self.color = (c[0], c[1], c[2], 1.0)
721 return self.color
722
723
724def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, sec_w, stat_type):
725 global palette_idx
726 palette_idx = 0
727
728 time_hash = {}
729 total_time = 0.0
730 m_proc_list = {}
731
732 if stat_type is STAT_TYPE_CPU:
733 sample_value = 'cpu'
734 else:
735 sample_value = 'io'
736 for proc in proc_tree.process_list:
737 if elide_bootchart(proc):
738 continue
739
740 for sample in proc.samples:
741 total_time += getattr(sample.cpu_sample, sample_value)
742 if not sample.time in time_hash:
743 time_hash[sample.time] = 1
744
745 # merge pids with the same cmd
746 if not proc.cmd in m_proc_list:
747 m_proc_list[proc.cmd] = CumlSample (proc)
748 continue
749 s = m_proc_list[proc.cmd]
750 s.merge_samples (proc)
751
752 # all the sample times
753 times = sorted(time_hash)
754 if len (times) < 2:
755 print("degenerate boot chart")
756 return
757
758 pix_per_ns = chart_bounds[3] / total_time
759# print "total time: %g pix-per-ns %g" % (total_time, pix_per_ns)
760
761 # FIXME: we have duplicates in the process list too [!] - why !?
762
763 # Render bottom up, left to right
764 below = {}
765 for time in times:
766 below[time] = chart_bounds[1] + chart_bounds[3]
767
768 # same colors each time we render
769 random.seed (0)
770
771 ctx.set_line_width(1)
772
773 legends = []
774 labels = []
775
776 # render each pid in order
777 for cs in m_proc_list.values():
778 row = {}
779 cuml = 0.0
780
781 # print "pid : %s -> %g samples %d" % (proc.cmd, cuml, len (cs.samples))
782 for sample in cs.samples:
783 cuml += getattr(sample.cpu_sample, sample_value)
784 row[sample.time] = cuml
785
786 process_total_time = cuml
787
788 # hide really tiny processes
789 if cuml * pix_per_ns <= 2:
790 continue
791
792 last_time = times[0]
793 y = last_below = below[last_time]
794 last_cuml = cuml = 0.0
795
796 ctx.set_source_rgba(*cs.get_color())
797 for time in times:
798 render_seg = False
799
800 # did the underlying trend increase ?
801 if below[time] != last_below:
802 last_below = below[last_time]
803 last_cuml = cuml
804 render_seg = True
805
806 # did we move up a pixel increase ?
807 if time in row:
808 nc = round (row[time] * pix_per_ns)
809 if nc != cuml:
810 last_cuml = cuml
811 cuml = nc
812 render_seg = True
813
814# if last_cuml > cuml:
815# assert fail ... - un-sorted process samples
816
817 # draw the trailing rectangle from the last time to
818 # before now, at the height of the last segment.
819 if render_seg:
820 w = math.ceil ((time - last_time) * chart_bounds[2] / proc_tree.duration) + 1
821 x = chart_bounds[0] + round((last_time - proc_tree.start_time) * chart_bounds[2] / proc_tree.duration)
822 ctx.rectangle (x, below[last_time] - last_cuml, w, last_cuml)
823 ctx.fill()
824# ctx.stroke()
825 last_time = time
826 y = below [time] - cuml
827
828 row[time] = y
829
830 # render the last segment
831 x = chart_bounds[0] + round((last_time - proc_tree.start_time) * chart_bounds[2] / proc_tree.duration)
832 y = below[last_time] - cuml
833 ctx.rectangle (x, y, chart_bounds[2] - x, cuml)
834 ctx.fill()
835# ctx.stroke()
836
837 # render legend if it will fit
838 if cuml > 8:
839 label = cs.cmd
840 extnts = ctx.text_extents(label)
841 label_w = extnts[2]
842 label_h = extnts[3]
843# print "Text extents %g by %g" % (label_w, label_h)
844 labels.append((label,
845 chart_bounds[0] + chart_bounds[2] - label_w - off_x * 2,
846 y + (cuml + label_h) / 2))
847 if cs in legends:
848 print("ARGH - duplicate process in list !")
849
850 legends.append ((cs, process_total_time))
851
852 below = row
853
854 # render grid-lines over the top
855 draw_box_ticks(ctx, chart_bounds, sec_w)
856
857 # render labels
858 for l in labels:
859 draw_text(ctx, l[0], TEXT_COLOR, l[1], l[2])
860
861 # Render legends
862 font_height = 20
863 label_width = 300
864 LEGENDS_PER_COL = 15
865 LEGENDS_TOTAL = 45
866 ctx.set_font_size (TITLE_FONT_SIZE)
867 dur_secs = duration / 100
868 cpu_secs = total_time / 1000000000
869
870 # misleading - with multiple CPUs ...
871# idle = ((dur_secs - cpu_secs) / dur_secs) * 100.0
872 if stat_type is STAT_TYPE_CPU:
873 label = "Cumulative CPU usage, by process; total CPU: " \
874 " %.5g(s) time: %.3g(s)" % (cpu_secs, dur_secs)
875 else:
876 label = "Cumulative I/O usage, by process; total I/O: " \
877 " %.5g(s) time: %.3g(s)" % (cpu_secs, dur_secs)
878
879 draw_text(ctx, label, TEXT_COLOR, chart_bounds[0] + off_x,
880 chart_bounds[1] + font_height)
881
882 i = 0
883 legends = sorted(legends, key=itemgetter(1), reverse=True)
884 ctx.set_font_size(TEXT_FONT_SIZE)
885 for t in legends:
886 cs = t[0]
887 time = t[1]
888 x = chart_bounds[0] + off_x + int (i/LEGENDS_PER_COL) * label_width
889 y = chart_bounds[1] + font_height * ((i % LEGENDS_PER_COL) + 2)
890 str = "%s - %.0f(ms) (%2.2f%%)" % (cs.cmd, time/1000000, (time/total_time) * 100.0)
891 draw_legend_box(ctx, str, cs.color, x, y, leg_s)
892 i = i + 1
893 if i >= LEGENDS_TOTAL:
894 break
diff --git a/scripts/pybootchartgui/pybootchartgui/gui.py b/scripts/pybootchartgui/pybootchartgui/gui.py
new file mode 100644
index 0000000000..7fedd232df
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/gui.py
@@ -0,0 +1,350 @@
1# This file is part of pybootchartgui.
2
3# pybootchartgui is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7
8# pybootchartgui is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12
13# You should have received a copy of the GNU General Public License
14# along with pybootchartgui. If not, see <http://www.gnu.org/licenses/>.
15
16import gobject
17import gtk
18import gtk.gdk
19import gtk.keysyms
20from . import draw
21from .draw import RenderOptions
22
23class PyBootchartWidget(gtk.DrawingArea):
24 __gsignals__ = {
25 'expose-event': 'override',
26 'clicked' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gtk.gdk.Event)),
27 'position-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT, gobject.TYPE_INT)),
28 'set-scroll-adjustments' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gtk.Adjustment, gtk.Adjustment))
29 }
30
31 def __init__(self, trace, options, xscale):
32 gtk.DrawingArea.__init__(self)
33
34 self.trace = trace
35 self.options = options
36
37 self.set_flags(gtk.CAN_FOCUS)
38
39 self.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
40 self.connect("button-press-event", self.on_area_button_press)
41 self.connect("button-release-event", self.on_area_button_release)
42 self.add_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
43 self.connect("motion-notify-event", self.on_area_motion_notify)
44 self.connect("scroll-event", self.on_area_scroll_event)
45 self.connect('key-press-event', self.on_key_press_event)
46
47 self.connect('set-scroll-adjustments', self.on_set_scroll_adjustments)
48 self.connect("size-allocate", self.on_allocation_size_changed)
49 self.connect("position-changed", self.on_position_changed)
50
51 self.zoom_ratio = 1.0
52 self.xscale = xscale
53 self.x, self.y = 0.0, 0.0
54
55 self.chart_width, self.chart_height = draw.extents(self.options, self.xscale, self.trace)
56 self.hadj = None
57 self.vadj = None
58 self.hadj_changed_signal_id = None
59 self.vadj_changed_signal_id = None
60
61 def do_expose_event(self, event):
62 cr = self.window.cairo_create()
63
64 # set a clip region for the expose event
65 cr.rectangle(
66 event.area.x, event.area.y,
67 event.area.width, event.area.height
68 )
69 cr.clip()
70 self.draw(cr, self.get_allocation())
71 return False
72
73 def draw(self, cr, rect):
74 cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
75 cr.paint()
76 cr.scale(self.zoom_ratio, self.zoom_ratio)
77 cr.translate(-self.x, -self.y)
78 draw.render(cr, self.options, self.xscale, self.trace)
79
80 def position_changed(self):
81 self.emit("position-changed", self.x, self.y)
82
83 ZOOM_INCREMENT = 1.25
84
85 def zoom_image (self, zoom_ratio):
86 self.zoom_ratio = zoom_ratio
87 self._set_scroll_adjustments (self.hadj, self.vadj)
88 self.queue_draw()
89
90 def zoom_to_rect (self, rect):
91 zoom_ratio = float(rect.width)/float(self.chart_width)
92 self.zoom_image(zoom_ratio)
93 self.x = 0
94 self.position_changed()
95
96 def set_xscale(self, xscale):
97 old_mid_x = self.x + self.hadj.page_size / 2
98 self.xscale = xscale
99 self.chart_width, self.chart_height = draw.extents(self.options, self.xscale, self.trace)
100 new_x = old_mid_x
101 self.zoom_image (self.zoom_ratio)
102
103 def on_expand(self, action):
104 self.set_xscale (int(self.xscale * 1.5 + 0.5))
105
106 def on_contract(self, action):
107 self.set_xscale (max(int(self.xscale / 1.5), 1))
108
109 def on_zoom_in(self, action):
110 self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
111
112 def on_zoom_out(self, action):
113 self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
114
115 def on_zoom_fit(self, action):
116 self.zoom_to_rect(self.get_allocation())
117
118 def on_zoom_100(self, action):
119 self.zoom_image(1.0)
120 self.set_xscale(1.0)
121
122 def show_toggled(self, button):
123 self.options.app_options.show_all = button.get_property ('active')
124 self.chart_width, self.chart_height = draw.extents(self.options, self.xscale, self.trace)
125 self._set_scroll_adjustments(self.hadj, self.vadj)
126 self.queue_draw()
127
128 POS_INCREMENT = 100
129
130 def on_key_press_event(self, widget, event):
131 if event.keyval == gtk.keysyms.Left:
132 self.x -= self.POS_INCREMENT/self.zoom_ratio
133 elif event.keyval == gtk.keysyms.Right:
134 self.x += self.POS_INCREMENT/self.zoom_ratio
135 elif event.keyval == gtk.keysyms.Up:
136 self.y -= self.POS_INCREMENT/self.zoom_ratio
137 elif event.keyval == gtk.keysyms.Down:
138 self.y += self.POS_INCREMENT/self.zoom_ratio
139 else:
140 return False
141 self.queue_draw()
142 self.position_changed()
143 return True
144
145 def on_area_button_press(self, area, event):
146 if event.button == 2 or event.button == 1:
147 area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
148 self.prevmousex = event.x
149 self.prevmousey = event.y
150 if event.type not in (gtk.gdk.BUTTON_PRESS, gtk.gdk.BUTTON_RELEASE):
151 return False
152 return False
153
154 def on_area_button_release(self, area, event):
155 if event.button == 2 or event.button == 1:
156 area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
157 self.prevmousex = None
158 self.prevmousey = None
159 return True
160 return False
161
162 def on_area_scroll_event(self, area, event):
163 if event.state & gtk.gdk.CONTROL_MASK:
164 if event.direction == gtk.gdk.SCROLL_UP:
165 self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
166 return True
167 if event.direction == gtk.gdk.SCROLL_DOWN:
168 self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
169 return True
170 return False
171
172 def on_area_motion_notify(self, area, event):
173 state = event.state
174 if state & gtk.gdk.BUTTON2_MASK or state & gtk.gdk.BUTTON1_MASK:
175 x, y = int(event.x), int(event.y)
176 # pan the image
177 self.x += (self.prevmousex - x)/self.zoom_ratio
178 self.y += (self.prevmousey - y)/self.zoom_ratio
179 self.queue_draw()
180 self.prevmousex = x
181 self.prevmousey = y
182 self.position_changed()
183 return True
184
185 def on_set_scroll_adjustments(self, area, hadj, vadj):
186 self._set_scroll_adjustments (hadj, vadj)
187
188 def on_allocation_size_changed(self, widget, allocation):
189 self.hadj.page_size = allocation.width
190 self.hadj.page_increment = allocation.width * 0.9
191 self.vadj.page_size = allocation.height
192 self.vadj.page_increment = allocation.height * 0.9
193
194 def _set_adj_upper(self, adj, upper):
195 changed = False
196 value_changed = False
197
198 if adj.upper != upper:
199 adj.upper = upper
200 changed = True
201
202 max_value = max(0.0, upper - adj.page_size)
203 if adj.value > max_value:
204 adj.value = max_value
205 value_changed = True
206
207 if changed:
208 adj.changed()
209 if value_changed:
210 adj.value_changed()
211
212 def _set_scroll_adjustments(self, hadj, vadj):
213 if hadj == None:
214 hadj = gtk.Adjustment(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
215 if vadj == None:
216 vadj = gtk.Adjustment(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
217
218 if self.hadj_changed_signal_id != None and \
219 self.hadj != None and hadj != self.hadj:
220 self.hadj.disconnect (self.hadj_changed_signal_id)
221 if self.vadj_changed_signal_id != None and \
222 self.vadj != None and vadj != self.vadj:
223 self.vadj.disconnect (self.vadj_changed_signal_id)
224
225 if hadj != None:
226 self.hadj = hadj
227 self._set_adj_upper (self.hadj, self.zoom_ratio * self.chart_width)
228 self.hadj_changed_signal_id = self.hadj.connect('value-changed', self.on_adjustments_changed)
229
230 if vadj != None:
231 self.vadj = vadj
232 self._set_adj_upper (self.vadj, self.zoom_ratio * self.chart_height)
233 self.vadj_changed_signal_id = self.vadj.connect('value-changed', self.on_adjustments_changed)
234
235 def on_adjustments_changed(self, adj):
236 self.x = self.hadj.value / self.zoom_ratio
237 self.y = self.vadj.value / self.zoom_ratio
238 self.queue_draw()
239
240 def on_position_changed(self, widget, x, y):
241 self.hadj.value = x * self.zoom_ratio
242 self.vadj.value = y * self.zoom_ratio
243
244PyBootchartWidget.set_set_scroll_adjustments_signal('set-scroll-adjustments')
245
246class PyBootchartShell(gtk.VBox):
247 ui = '''
248 <ui>
249 <toolbar name="ToolBar">
250 <toolitem action="Expand"/>
251 <toolitem action="Contract"/>
252 <separator/>
253 <toolitem action="ZoomIn"/>
254 <toolitem action="ZoomOut"/>
255 <toolitem action="ZoomFit"/>
256 <toolitem action="Zoom100"/>
257 </toolbar>
258 </ui>
259 '''
260 def __init__(self, window, trace, options, xscale):
261 gtk.VBox.__init__(self)
262
263 self.widget = PyBootchartWidget(trace, options, xscale)
264
265 # Create a UIManager instance
266 uimanager = self.uimanager = gtk.UIManager()
267
268 # Add the accelerator group to the toplevel window
269 accelgroup = uimanager.get_accel_group()
270 window.add_accel_group(accelgroup)
271
272 # Create an ActionGroup
273 actiongroup = gtk.ActionGroup('Actions')
274 self.actiongroup = actiongroup
275
276 # Create actions
277 actiongroup.add_actions((
278 ('Expand', gtk.STOCK_ADD, None, None, None, self.widget.on_expand),
279 ('Contract', gtk.STOCK_REMOVE, None, None, None, self.widget.on_contract),
280 ('ZoomIn', gtk.STOCK_ZOOM_IN, None, None, None, self.widget.on_zoom_in),
281 ('ZoomOut', gtk.STOCK_ZOOM_OUT, None, None, None, self.widget.on_zoom_out),
282 ('ZoomFit', gtk.STOCK_ZOOM_FIT, 'Fit Width', None, None, self.widget.on_zoom_fit),
283 ('Zoom100', gtk.STOCK_ZOOM_100, None, None, None, self.widget.on_zoom_100),
284 ))
285
286 # Add the actiongroup to the uimanager
287 uimanager.insert_action_group(actiongroup, 0)
288
289 # Add a UI description
290 uimanager.add_ui_from_string(self.ui)
291
292 # Scrolled window
293 scrolled = gtk.ScrolledWindow()
294 scrolled.add(self.widget)
295
296 # toolbar / h-box
297 hbox = gtk.HBox(False, 8)
298
299 # Create a Toolbar
300 toolbar = uimanager.get_widget('/ToolBar')
301 hbox.pack_start(toolbar, True, True)
302
303 if not options.kernel_only:
304 # Misc. options
305 button = gtk.CheckButton("Show more")
306 button.connect ('toggled', self.widget.show_toggled)
307 button.set_active(options.app_options.show_all)
308 hbox.pack_start (button, False, True)
309
310 self.pack_start(hbox, False)
311 self.pack_start(scrolled)
312 self.show_all()
313
314 def grab_focus(self, window):
315 window.set_focus(self.widget)
316
317
318class PyBootchartWindow(gtk.Window):
319
320 def __init__(self, trace, app_options):
321 gtk.Window.__init__(self)
322
323 window = self
324 window.set_title("Bootchart %s" % trace.filename)
325 window.set_default_size(750, 550)
326
327 tab_page = gtk.Notebook()
328 tab_page.show()
329 window.add(tab_page)
330
331 full_opts = RenderOptions(app_options)
332 full_tree = PyBootchartShell(window, trace, full_opts, 1.0)
333 tab_page.append_page (full_tree, gtk.Label("Full tree"))
334
335 if trace.kernel is not None and len (trace.kernel) > 2:
336 kernel_opts = RenderOptions(app_options)
337 kernel_opts.cumulative = False
338 kernel_opts.charts = False
339 kernel_opts.kernel_only = True
340 kernel_tree = PyBootchartShell(window, trace, kernel_opts, 5.0)
341 tab_page.append_page (kernel_tree, gtk.Label("Kernel boot"))
342
343 full_tree.grab_focus(self)
344 self.show()
345
346
347def show(trace, options):
348 win = PyBootchartWindow(trace, options)
349 win.connect('destroy', gtk.main_quit)
350 gtk.main()
diff --git a/scripts/pybootchartgui/pybootchartgui/main.py b/scripts/pybootchartgui/pybootchartgui/main.py
new file mode 120000
index 0000000000..b45ae0a3d2
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/main.py
@@ -0,0 +1 @@
main.py.in \ No newline at end of file
diff --git a/scripts/pybootchartgui/pybootchartgui/main.py.in b/scripts/pybootchartgui/pybootchartgui/main.py.in
new file mode 100644
index 0000000000..21bb0be3a7
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/main.py.in
@@ -0,0 +1,187 @@
1#
2# ***********************************************************************
3# Warning: This file is auto-generated from main.py.in - edit it there.
4# ***********************************************************************
5#
6# pybootchartgui is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10
11# pybootchartgui is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15
16# You should have received a copy of the GNU General Public License
17# along with pybootchartgui. If not, see <http://www.gnu.org/licenses/>.
18
19from __future__ import print_function
20
21import sys
22import os
23import optparse
24
25from . import parsing
26from . import batch
27
28def _mk_options_parser():
29 """Make an options parser."""
30 usage = "%prog [options] /path/to/tmp/buildstats/<recipe-machine>/<BUILDNAME>/"
31 version = "%prog v1.0.0"
32 parser = optparse.OptionParser(usage, version=version)
33 parser.add_option("-i", "--interactive", action="store_true", dest="interactive", default=False,
34 help="start in active mode")
35 parser.add_option("-f", "--format", dest="format", default="png", choices=["png", "svg", "pdf"],
36 help="image format (png, svg, pdf); default format png")
37 parser.add_option("-o", "--output", dest="output", metavar="PATH", default=None,
38 help="output path (file or directory) where charts are stored")
39 parser.add_option("-s", "--split", dest="num", type=int, default=1,
40 help="split the output chart into <NUM> charts, only works with \"-o PATH\"")
41 parser.add_option("-m", "--mintime", dest="mintime", type=int, default=8,
42 help="only tasks longer than this time will be displayed")
43 parser.add_option("-M", "--minutes", action="store_true", dest="as_minutes", default=False,
44 help="display time in minutes instead of seconds")
45# parser.add_option("-n", "--no-prune", action="store_false", dest="prune", default=True,
46# help="do not prune the process tree")
47 parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False,
48 help="suppress informational messages")
49# parser.add_option("-t", "--boot-time", action="store_true", dest="boottime", default=False,
50# help="only display the boot time of the boot in text format (stdout)")
51 parser.add_option("--very-quiet", action="store_true", dest="veryquiet", default=False,
52 help="suppress all messages except errors")
53 parser.add_option("--verbose", action="store_true", dest="verbose", default=False,
54 help="print all messages")
55# parser.add_option("--profile", action="store_true", dest="profile", default=False,
56# help="profile rendering of chart (only useful when in batch mode indicated by -f)")
57# parser.add_option("--show-pid", action="store_true", dest="show_pid", default=False,
58# help="show process ids in the bootchart as 'processname [pid]'")
59 parser.add_option("--show-all", action="store_true", dest="show_all", default=False,
60 help="show all processes in the chart")
61# parser.add_option("--crop-after", dest="crop_after", metavar="PROCESS", default=None,
62# help="crop chart when idle after PROCESS is started")
63# parser.add_option("--annotate", action="append", dest="annotate", metavar="PROCESS", default=None,
64# help="annotate position where PROCESS is started; can be specified multiple times. " +
65# "To create a single annotation when any one of a set of processes is started, use commas to separate the names")
66# parser.add_option("--annotate-file", dest="annotate_file", metavar="FILENAME", default=None,
67# help="filename to write annotation points to")
68 parser.add_option("-T", "--full-time", action="store_true", dest="full_time", default=False,
69 help="display the full time regardless of which processes are currently shown")
70 return parser
71
72class Writer:
73 def __init__(self, write, options):
74 self.write = write
75 self.options = options
76
77 def error(self, msg):
78 self.write(msg)
79
80 def warn(self, msg):
81 if not self.options.quiet:
82 self.write(msg)
83
84 def info(self, msg):
85 if self.options.verbose:
86 self.write(msg)
87
88 def status(self, msg):
89 if not self.options.quiet:
90 self.write(msg)
91
92def _mk_writer(options):
93 def write(s):
94 print(s)
95 return Writer(write, options)
96
97def _get_filename(path):
98 """Construct a usable filename for outputs"""
99 dname = "."
100 fname = "bootchart"
101 if path != None:
102 if os.path.isdir(path):
103 dname = path
104 else:
105 fname = path
106 return os.path.join(dname, fname)
107
108def main(argv=None):
109 try:
110 if argv is None:
111 argv = sys.argv[1:]
112
113 parser = _mk_options_parser()
114 options, args = parser.parse_args(argv)
115
116 # Default values for disabled options
117 options.prune = True
118 options.boottime = False
119 options.profile = False
120 options.show_pid = False
121 options.crop_after = None
122 options.annotate = None
123 options.annotate_file = None
124
125 writer = _mk_writer(options)
126
127 if len(args) == 0:
128 print("No path given, trying /var/log/bootchart.tgz")
129 args = [ "/var/log/bootchart.tgz" ]
130
131 res = parsing.Trace(writer, args, options)
132
133 if options.interactive or options.output == None:
134 from . import gui
135 gui.show(res, options)
136 elif options.boottime:
137 import math
138 proc_tree = res.proc_tree
139 if proc_tree.idle:
140 duration = proc_tree.idle
141 else:
142 duration = proc_tree.duration
143 dur = duration / 100.0
144 print('%02d:%05.2f' % (math.floor(dur/60), dur - 60 * math.floor(dur/60)))
145 else:
146 if options.annotate_file:
147 f = open (options.annotate_file, "w")
148 try:
149 for time in res[4]:
150 if time is not None:
151 # output as ms
152 print(time * 10, file=f)
153 else:
154 print(file=f)
155 finally:
156 f.close()
157 filename = _get_filename(options.output)
158 res_list = parsing.split_res(res, options)
159 n = 1
160 width = len(str(len(res_list)))
161 s = "_%%0%dd." % width
162 for r in res_list:
163 if len(res_list) == 1:
164 f = filename + "." + options.format
165 else:
166 f = filename + s % n + options.format
167 n = n + 1
168 def render():
169 batch.render(writer, r, options, f)
170 if options.profile:
171 import cProfile
172 import pstats
173 profile = '%s.prof' % os.path.splitext(filename)[0]
174 cProfile.runctx('render()', globals(), locals(), profile)
175 p = pstats.Stats(profile)
176 p.strip_dirs().sort_stats('time').print_stats(20)
177 else:
178 render()
179
180 return 0
181 except parsing.ParseError as ex:
182 print(("Parse error: %s" % ex))
183 return 2
184
185
186if __name__ == '__main__':
187 sys.exit(main())
diff --git a/scripts/pybootchartgui/pybootchartgui/parsing.py b/scripts/pybootchartgui/pybootchartgui/parsing.py
new file mode 100644
index 0000000000..d423b9f77c
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/parsing.py
@@ -0,0 +1,740 @@
1# This file is part of pybootchartgui.
2
3# pybootchartgui is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7
8# pybootchartgui is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12
13# You should have received a copy of the GNU General Public License
14# along with pybootchartgui. If not, see <http://www.gnu.org/licenses/>.
15
16
17from __future__ import with_statement
18
19import os
20import string
21import re
22import sys
23import tarfile
24from time import clock
25from collections import defaultdict
26from functools import reduce
27
28from .samples import *
29from .process_tree import ProcessTree
30
31if sys.version_info >= (3, 0):
32 long = int
33
34# Parsing produces as its end result a 'Trace'
35
36class Trace:
37 def __init__(self, writer, paths, options):
38 self.processes = {}
39 self.start = {}
40 self.end = {}
41 self.min = None
42 self.max = None
43 self.headers = None
44 self.disk_stats = None
45 self.ps_stats = None
46 self.taskstats = None
47 self.cpu_stats = None
48 self.cmdline = None
49 self.kernel = None
50 self.kernel_tree = None
51 self.filename = None
52 self.parent_map = None
53 self.mem_stats = None
54
55 if len(paths):
56 parse_paths (writer, self, paths)
57 if not self.valid():
58 raise ParseError("empty state: '%s' does not contain a valid bootchart" % ", ".join(paths))
59
60 if options.full_time:
61 self.min = min(self.start.keys())
62 self.max = max(self.end.keys())
63
64 return
65
66 # Turn that parsed information into something more useful
67 # link processes into a tree of pointers, calculate statistics
68 self.compile(writer)
69
70 # Crop the chart to the end of the first idle period after the given
71 # process
72 if options.crop_after:
73 idle = self.crop (writer, options.crop_after)
74 else:
75 idle = None
76
77 # Annotate other times as the first start point of given process lists
78 self.times = [ idle ]
79 if options.annotate:
80 for procnames in options.annotate:
81 names = [x[:15] for x in procnames.split(",")]
82 for proc in self.ps_stats.process_map.values():
83 if proc.cmd in names:
84 self.times.append(proc.start_time)
85 break
86 else:
87 self.times.append(None)
88
89 self.proc_tree = ProcessTree(writer, self.kernel, self.ps_stats,
90 self.ps_stats.sample_period,
91 self.headers.get("profile.process"),
92 options.prune, idle, self.taskstats,
93 self.parent_map is not None)
94
95 if self.kernel is not None:
96 self.kernel_tree = ProcessTree(writer, self.kernel, None, 0,
97 self.headers.get("profile.process"),
98 False, None, None, True)
99
100 def valid(self):
101 return len(self.processes) != 0
102 return self.headers != None and self.disk_stats != None and \
103 self.ps_stats != None and self.cpu_stats != None
104
105 def add_process(self, process, start, end):
106 self.processes[process] = [start, end]
107 if start not in self.start:
108 self.start[start] = []
109 if process not in self.start[start]:
110 self.start[start].append(process)
111 if end not in self.end:
112 self.end[end] = []
113 if process not in self.end[end]:
114 self.end[end].append(process)
115
116 def compile(self, writer):
117
118 def find_parent_id_for(pid):
119 if pid is 0:
120 return 0
121 ppid = self.parent_map.get(pid)
122 if ppid:
123 # many of these double forks are so short lived
124 # that we have no samples, or process info for them
125 # so climb the parent hierarcy to find one
126 if int (ppid * 1000) not in self.ps_stats.process_map:
127# print "Pid '%d' short lived with no process" % ppid
128 ppid = find_parent_id_for (ppid)
129# else:
130# print "Pid '%d' has an entry" % ppid
131 else:
132# print "Pid '%d' missing from pid map" % pid
133 return 0
134 return ppid
135
136 # merge in the cmdline data
137 if self.cmdline is not None:
138 for proc in self.ps_stats.process_map.values():
139 rpid = int (proc.pid // 1000)
140 if rpid in self.cmdline:
141 cmd = self.cmdline[rpid]
142 proc.exe = cmd['exe']
143 proc.args = cmd['args']
144# else:
145# print "proc %d '%s' not in cmdline" % (rpid, proc.exe)
146
147 # re-parent any stray orphans if we can
148 if self.parent_map is not None:
149 for process in self.ps_stats.process_map.values():
150 ppid = find_parent_id_for (int(process.pid // 1000))
151 if ppid:
152 process.ppid = ppid * 1000
153
154 # stitch the tree together with pointers
155 for process in self.ps_stats.process_map.values():
156 process.set_parent (self.ps_stats.process_map)
157
158 # count on fingers variously
159 for process in self.ps_stats.process_map.values():
160 process.calc_stats (self.ps_stats.sample_period)
161
162 def crop(self, writer, crop_after):
163
164 def is_idle_at(util, start, j):
165 k = j + 1
166 while k < len(util) and util[k][0] < start + 300:
167 k += 1
168 k = min(k, len(util)-1)
169
170 if util[j][1] >= 0.25:
171 return False
172
173 avgload = sum(u[1] for u in util[j:k+1]) / (k-j+1)
174 if avgload < 0.25:
175 return True
176 else:
177 return False
178 def is_idle(util, start):
179 for j in range(0, len(util)):
180 if util[j][0] < start:
181 continue
182 return is_idle_at(util, start, j)
183 else:
184 return False
185
186 names = [x[:15] for x in crop_after.split(",")]
187 for proc in self.ps_stats.process_map.values():
188 if proc.cmd in names or proc.exe in names:
189 writer.info("selected proc '%s' from list (start %d)"
190 % (proc.cmd, proc.start_time))
191 break
192 if proc is None:
193 writer.warn("no selected crop proc '%s' in list" % crop_after)
194
195
196 cpu_util = [(sample.time, sample.user + sample.sys + sample.io) for sample in self.cpu_stats]
197 disk_util = [(sample.time, sample.util) for sample in self.disk_stats]
198
199 idle = None
200 for i in range(0, len(cpu_util)):
201 if cpu_util[i][0] < proc.start_time:
202 continue
203 if is_idle_at(cpu_util, cpu_util[i][0], i) \
204 and is_idle(disk_util, cpu_util[i][0]):
205 idle = cpu_util[i][0]
206 break
207
208 if idle is None:
209 writer.warn ("not idle after proc '%s'" % crop_after)
210 return None
211
212 crop_at = idle + 300
213 writer.info ("cropping at time %d" % crop_at)
214 while len (self.cpu_stats) \
215 and self.cpu_stats[-1].time > crop_at:
216 self.cpu_stats.pop()
217 while len (self.disk_stats) \
218 and self.disk_stats[-1].time > crop_at:
219 self.disk_stats.pop()
220
221 self.ps_stats.end_time = crop_at
222
223 cropped_map = {}
224 for key, value in self.ps_stats.process_map.items():
225 if (value.start_time <= crop_at):
226 cropped_map[key] = value
227
228 for proc in cropped_map.values():
229 proc.duration = min (proc.duration, crop_at - proc.start_time)
230 while len (proc.samples) \
231 and proc.samples[-1].time > crop_at:
232 proc.samples.pop()
233
234 self.ps_stats.process_map = cropped_map
235
236 return idle
237
238
239
240class ParseError(Exception):
241 """Represents errors during parse of the bootchart."""
242 def __init__(self, value):
243 self.value = value
244
245 def __str__(self):
246 return self.value
247
248def _parse_headers(file):
249 """Parses the headers of the bootchart."""
250 def parse(acc, line):
251 (headers, last) = acc
252 if '=' in line:
253 last, value = map (lambda x: x.strip(), line.split('=', 1))
254 else:
255 value = line.strip()
256 headers[last] += value
257 return headers, last
258 return reduce(parse, file.read().decode('utf-8').split('\n'), (defaultdict(str),''))[0]
259
260def _parse_timed_blocks(file):
261 """Parses (ie., splits) a file into so-called timed-blocks. A
262 timed-block consists of a timestamp on a line by itself followed
263 by zero or more lines of data for that point in time."""
264 def parse(block):
265 lines = block.split('\n')
266 if not lines:
267 raise ParseError('expected a timed-block consisting a timestamp followed by data lines')
268 try:
269 return (int(lines[0]), lines[1:])
270 except ValueError:
271 raise ParseError("expected a timed-block, but timestamp '%s' is not an integer" % lines[0])
272 blocks = file.read().decode('utf-8').split('\n\n')
273 return [parse(block) for block in blocks if block.strip() and not block.endswith(' not running\n')]
274
275def _parse_proc_ps_log(writer, file):
276 """
277 * See proc(5) for details.
278 *
279 * {pid, comm, state, ppid, pgrp, session, tty_nr, tpgid, flags, minflt, cminflt, majflt, cmajflt, utime, stime,
280 * cutime, cstime, priority, nice, 0, itrealvalue, starttime, vsize, rss, rlim, startcode, endcode, startstack,
281 * kstkesp, kstkeip}
282 """
283 processMap = {}
284 ltime = 0
285 timed_blocks = _parse_timed_blocks(file)
286 for time, lines in timed_blocks:
287 for line in lines:
288 if not line: continue
289 tokens = line.split(' ')
290 if len(tokens) < 21:
291 continue
292
293 offset = [index for index, token in enumerate(tokens[1:]) if token[-1] == ')'][0]
294 pid, cmd, state, ppid = int(tokens[0]), ' '.join(tokens[1:2+offset]), tokens[2+offset], int(tokens[3+offset])
295 userCpu, sysCpu, stime = int(tokens[13+offset]), int(tokens[14+offset]), int(tokens[21+offset])
296
297 # magic fixed point-ness ...
298 pid *= 1000
299 ppid *= 1000
300 if pid in processMap:
301 process = processMap[pid]
302 process.cmd = cmd.strip('()') # why rename after latest name??
303 else:
304 process = Process(writer, pid, cmd.strip('()'), ppid, min(time, stime))
305 processMap[pid] = process
306
307 if process.last_user_cpu_time is not None and process.last_sys_cpu_time is not None and ltime is not None:
308 userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime))
309 cpuSample = CPUSample('null', userCpuLoad, sysCpuLoad, 0.0)
310 process.samples.append(ProcessSample(time, state, cpuSample))
311
312 process.last_user_cpu_time = userCpu
313 process.last_sys_cpu_time = sysCpu
314 ltime = time
315
316 if len (timed_blocks) < 2:
317 return None
318
319 startTime = timed_blocks[0][0]
320 avgSampleLength = (ltime - startTime)/(len (timed_blocks) - 1)
321
322 return ProcessStats (writer, processMap, len (timed_blocks), avgSampleLength, startTime, ltime)
323
324def _parse_taskstats_log(writer, file):
325 """
326 * See bootchart-collector.c for details.
327 *
328 * { pid, ppid, comm, cpu_run_real_total, blkio_delay_total, swapin_delay_total }
329 *
330 """
331 processMap = {}
332 pidRewrites = {}
333 ltime = None
334 timed_blocks = _parse_timed_blocks(file)
335 for time, lines in timed_blocks:
336 # we have no 'stime' from taskstats, so prep 'init'
337 if ltime is None:
338 process = Process(writer, 1, '[init]', 0, 0)
339 processMap[1000] = process
340 ltime = time
341# continue
342 for line in lines:
343 if not line: continue
344 tokens = line.split(' ')
345 if len(tokens) != 6:
346 continue
347
348 opid, ppid, cmd = int(tokens[0]), int(tokens[1]), tokens[2]
349 cpu_ns, blkio_delay_ns, swapin_delay_ns = long(tokens[-3]), long(tokens[-2]), long(tokens[-1]),
350
351 # make space for trees of pids
352 opid *= 1000
353 ppid *= 1000
354
355 # when the process name changes, we re-write the pid.
356 if opid in pidRewrites:
357 pid = pidRewrites[opid]
358 else:
359 pid = opid
360
361 cmd = cmd.strip('(').strip(')')
362 if pid in processMap:
363 process = processMap[pid]
364 if process.cmd != cmd:
365 pid += 1
366 pidRewrites[opid] = pid
367# print "process mutation ! '%s' vs '%s' pid %s -> pid %s\n" % (process.cmd, cmd, opid, pid)
368 process = process.split (writer, pid, cmd, ppid, time)
369 processMap[pid] = process
370 else:
371 process.cmd = cmd;
372 else:
373 process = Process(writer, pid, cmd, ppid, time)
374 processMap[pid] = process
375
376 delta_cpu_ns = (float) (cpu_ns - process.last_cpu_ns)
377 delta_blkio_delay_ns = (float) (blkio_delay_ns - process.last_blkio_delay_ns)
378 delta_swapin_delay_ns = (float) (swapin_delay_ns - process.last_swapin_delay_ns)
379
380 # make up some state data ...
381 if delta_cpu_ns > 0:
382 state = "R"
383 elif delta_blkio_delay_ns + delta_swapin_delay_ns > 0:
384 state = "D"
385 else:
386 state = "S"
387
388 # retain the ns timing information into a CPUSample - that tries
389 # with the old-style to be a %age of CPU used in this time-slice.
390 if delta_cpu_ns + delta_blkio_delay_ns + delta_swapin_delay_ns > 0:
391# print "proc %s cpu_ns %g delta_cpu %g" % (cmd, cpu_ns, delta_cpu_ns)
392 cpuSample = CPUSample('null', delta_cpu_ns, 0.0,
393 delta_blkio_delay_ns,
394 delta_swapin_delay_ns)
395 process.samples.append(ProcessSample(time, state, cpuSample))
396
397 process.last_cpu_ns = cpu_ns
398 process.last_blkio_delay_ns = blkio_delay_ns
399 process.last_swapin_delay_ns = swapin_delay_ns
400 ltime = time
401
402 if len (timed_blocks) < 2:
403 return None
404
405 startTime = timed_blocks[0][0]
406 avgSampleLength = (ltime - startTime)/(len(timed_blocks)-1)
407
408 return ProcessStats (writer, processMap, len (timed_blocks), avgSampleLength, startTime, ltime)
409
410def _parse_proc_stat_log(file):
411 samples = []
412 ltimes = None
413 for time, lines in _parse_timed_blocks(file):
414 # skip emtpy lines
415 if not lines:
416 continue
417 # CPU times {user, nice, system, idle, io_wait, irq, softirq}
418 tokens = lines[0].split()
419 times = [ int(token) for token in tokens[1:] ]
420 if ltimes:
421 user = float((times[0] + times[1]) - (ltimes[0] + ltimes[1]))
422 system = float((times[2] + times[5] + times[6]) - (ltimes[2] + ltimes[5] + ltimes[6]))
423 idle = float(times[3] - ltimes[3])
424 iowait = float(times[4] - ltimes[4])
425
426 aSum = max(user + system + idle + iowait, 1)
427 samples.append( CPUSample(time, user/aSum, system/aSum, iowait/aSum) )
428
429 ltimes = times
430 # skip the rest of statistics lines
431 return samples
432
433def _parse_proc_disk_stat_log(file, numCpu):
434 """
435 Parse file for disk stats, but only look at the whole device, eg. sda,
436 not sda1, sda2 etc. The format of relevant lines should be:
437 {major minor name rio rmerge rsect ruse wio wmerge wsect wuse running use aveq}
438 """
439 disk_regex_re = re.compile ('^([hsv]d.|mtdblock\d|mmcblk\d|cciss/c\d+d\d+.*)$')
440
441 # this gets called an awful lot.
442 def is_relevant_line(linetokens):
443 if len(linetokens) != 14:
444 return False
445 disk = linetokens[2]
446 return disk_regex_re.match(disk)
447
448 disk_stat_samples = []
449
450 for time, lines in _parse_timed_blocks(file):
451 sample = DiskStatSample(time)
452 relevant_tokens = [linetokens for linetokens in map (lambda x: x.split(),lines) if is_relevant_line(linetokens)]
453
454 for tokens in relevant_tokens:
455 disk, rsect, wsect, use = tokens[2], int(tokens[5]), int(tokens[9]), int(tokens[12])
456 sample.add_diskdata([rsect, wsect, use])
457
458 disk_stat_samples.append(sample)
459
460 disk_stats = []
461 for sample1, sample2 in zip(disk_stat_samples[:-1], disk_stat_samples[1:]):
462 interval = sample1.time - sample2.time
463 if interval == 0:
464 interval = 1
465 sums = [ a - b for a, b in zip(sample1.diskdata, sample2.diskdata) ]
466 readTput = sums[0] / 2.0 * 100.0 / interval
467 writeTput = sums[1] / 2.0 * 100.0 / interval
468 util = float( sums[2] ) / 10 / interval / numCpu
469 util = max(0.0, min(1.0, util))
470 disk_stats.append(DiskSample(sample2.time, readTput, writeTput, util))
471
472 return disk_stats
473
474def _parse_proc_meminfo_log(file):
475 """
476 Parse file for global memory statistics.
477 The format of relevant lines should be: ^key: value( unit)?
478 """
479 used_values = ('MemTotal', 'MemFree', 'Buffers', 'Cached', 'SwapTotal', 'SwapFree',)
480
481 mem_stats = []
482 meminfo_re = re.compile(r'([^ \t:]+):\s*(\d+).*')
483
484 for time, lines in _parse_timed_blocks(file):
485 sample = MemSample(time)
486
487 for line in lines:
488 match = meminfo_re.match(line)
489 if not match:
490 raise ParseError("Invalid meminfo line \"%s\"" % match.groups(0))
491 sample.add_value(match.group(1), int(match.group(2)))
492
493 if sample.valid():
494 mem_stats.append(sample)
495
496 return mem_stats
497
498# if we boot the kernel with: initcall_debug printk.time=1 we can
499# get all manner of interesting data from the dmesg output
500# We turn this into a pseudo-process tree: each event is
501# characterised by a
502# we don't try to detect a "kernel finished" state - since the kernel
503# continues to do interesting things after init is called.
504#
505# sample input:
506# [ 0.000000] ACPI: FACP 3f4fc000 000F4 (v04 INTEL Napa 00000001 MSFT 01000013)
507# ...
508# [ 0.039993] calling migration_init+0x0/0x6b @ 1
509# [ 0.039993] initcall migration_init+0x0/0x6b returned 1 after 0 usecs
510def _parse_dmesg(writer, file):
511 timestamp_re = re.compile ("^\[\s*(\d+\.\d+)\s*]\s+(.*)$")
512 split_re = re.compile ("^(\S+)\s+([\S\+_-]+) (.*)$")
513 processMap = {}
514 idx = 0
515 inc = 1.0 / 1000000
516 kernel = Process(writer, idx, "k-boot", 0, 0.1)
517 processMap['k-boot'] = kernel
518 base_ts = False
519 max_ts = 0
520 for line in file.read().decode('utf-8').split('\n'):
521 t = timestamp_re.match (line)
522 if t is None:
523# print "duff timestamp " + line
524 continue
525
526 time_ms = float (t.group(1)) * 1000
527 # looks like we may have a huge diff after the clock
528 # has been set up. This could lead to huge graph:
529 # so huge we will be killed by the OOM.
530 # So instead of using the plain timestamp we will
531 # use a delta to first one and skip the first one
532 # for convenience
533 if max_ts == 0 and not base_ts and time_ms > 1000:
534 base_ts = time_ms
535 continue
536 max_ts = max(time_ms, max_ts)
537 if base_ts:
538# print "fscked clock: used %f instead of %f" % (time_ms - base_ts, time_ms)
539 time_ms -= base_ts
540 m = split_re.match (t.group(2))
541
542 if m is None:
543 continue
544# print "match: '%s'" % (m.group(1))
545 type = m.group(1)
546 func = m.group(2)
547 rest = m.group(3)
548
549 if t.group(2).startswith ('Write protecting the') or \
550 t.group(2).startswith ('Freeing unused kernel memory'):
551 kernel.duration = time_ms / 10
552 continue
553
554# print "foo: '%s' '%s' '%s'" % (type, func, rest)
555 if type == "calling":
556 ppid = kernel.pid
557 p = re.match ("\@ (\d+)", rest)
558 if p is not None:
559 ppid = float (p.group(1)) // 1000
560# print "match: '%s' ('%g') at '%s'" % (func, ppid, time_ms)
561 name = func.split ('+', 1) [0]
562 idx += inc
563 processMap[func] = Process(writer, ppid + idx, name, ppid, time_ms / 10)
564 elif type == "initcall":
565# print "finished: '%s' at '%s'" % (func, time_ms)
566 if func in processMap:
567 process = processMap[func]
568 process.duration = (time_ms / 10) - process.start_time
569 else:
570 print("corrupted init call for %s" % (func))
571
572 elif type == "async_waiting" or type == "async_continuing":
573 continue # ignore
574
575 return processMap.values()
576
577#
578# Parse binary pacct accounting file output if we have one
579# cf. /usr/include/linux/acct.h
580#
581def _parse_pacct(writer, file):
582 # read LE int32
583 def _read_le_int32(file):
584 byts = file.read(4)
585 return (ord(byts[0])) | (ord(byts[1]) << 8) | \
586 (ord(byts[2]) << 16) | (ord(byts[3]) << 24)
587
588 parent_map = {}
589 parent_map[0] = 0
590 while file.read(1) != "": # ignore flags
591 ver = file.read(1)
592 if ord(ver) < 3:
593 print("Invalid version 0x%x" % (ord(ver)))
594 return None
595
596 file.seek (14, 1) # user, group etc.
597 pid = _read_le_int32 (file)
598 ppid = _read_le_int32 (file)
599# print "Parent of %d is %d" % (pid, ppid)
600 parent_map[pid] = ppid
601 file.seek (4 + 4 + 16, 1) # timings
602 file.seek (16, 1) # acct_comm
603 return parent_map
604
605def _parse_paternity_log(writer, file):
606 parent_map = {}
607 parent_map[0] = 0
608 for line in file.read().decode('utf-8').split('\n'):
609 if not line:
610 continue
611 elems = line.split(' ') # <Child> <Parent>
612 if len (elems) >= 2:
613# print "paternity of %d is %d" % (int(elems[0]), int(elems[1]))
614 parent_map[int(elems[0])] = int(elems[1])
615 else:
616 print("Odd paternity line '%s'" % (line))
617 return parent_map
618
619def _parse_cmdline_log(writer, file):
620 cmdLines = {}
621 for block in file.read().decode('utf-8').split('\n\n'):
622 lines = block.split('\n')
623 if len (lines) >= 3:
624# print "Lines '%s'" % (lines[0])
625 pid = int (lines[0])
626 values = {}
627 values['exe'] = lines[1].lstrip(':')
628 args = lines[2].lstrip(':').split('\0')
629 args.pop()
630 values['args'] = args
631 cmdLines[pid] = values
632 return cmdLines
633
634def get_num_cpus(headers):
635 """Get the number of CPUs from the system.cpu header property. As the
636 CPU utilization graphs are relative, the number of CPUs currently makes
637 no difference."""
638 if headers is None:
639 return 1
640 if headers.get("system.cpu.num"):
641 return max (int (headers.get("system.cpu.num")), 1)
642 cpu_model = headers.get("system.cpu")
643 if cpu_model is None:
644 return 1
645 mat = re.match(".*\\((\\d+)\\)", cpu_model)
646 if mat is None:
647 return 1
648 return max (int(mat.group(1)), 1)
649
650def _do_parse(writer, state, filename, file):
651 writer.info("parsing '%s'" % filename)
652 t1 = clock()
653 paths = filename.split("/")
654 task = paths[-1]
655 pn = paths[-2]
656 start = None
657 end = None
658 for line in file:
659 if line.startswith("Started:"):
660 start = int(float(line.split()[-1]))
661 elif line.startswith("Ended:"):
662 end = int(float(line.split()[-1]))
663 if start and end:
664 state.add_process(pn + ":" + task, start, end)
665 t2 = clock()
666 writer.info(" %s seconds" % str(t2-t1))
667 return state
668
669def parse_file(writer, state, filename):
670 if state.filename is None:
671 state.filename = filename
672 basename = os.path.basename(filename)
673 with open(filename, "rb") as file:
674 return _do_parse(writer, state, filename, file)
675
676def parse_paths(writer, state, paths):
677 for path in paths:
678 if state.filename is None:
679 state.filename = path
680 root, extension = os.path.splitext(path)
681 if not(os.path.exists(path)):
682 writer.warn("warning: path '%s' does not exist, ignoring." % path)
683 continue
684 #state.filename = path
685 if os.path.isdir(path):
686 files = sorted([os.path.join(path, f) for f in os.listdir(path)])
687 state = parse_paths(writer, state, files)
688 elif extension in [".tar", ".tgz", ".gz"]:
689 if extension == ".gz":
690 root, extension = os.path.splitext(root)
691 if extension != ".tar":
692 writer.warn("warning: can only handle zipped tar files, not zipped '%s'-files; ignoring" % extension)
693 continue
694 tf = None
695 try:
696 writer.status("parsing '%s'" % path)
697 tf = tarfile.open(path, 'r:*')
698 for name in tf.getnames():
699 state = _do_parse(writer, state, name, tf.extractfile(name))
700 except tarfile.ReadError as error:
701 raise ParseError("error: could not read tarfile '%s': %s." % (path, error))
702 finally:
703 if tf != None:
704 tf.close()
705 else:
706 state = parse_file(writer, state, path)
707 return state
708
709def split_res(res, options):
710 """ Split the res into n pieces """
711 res_list = []
712 if options.num > 1:
713 s_list = sorted(res.start.keys())
714 frag_size = len(s_list) / float(options.num)
715 # Need the top value
716 if frag_size > int(frag_size):
717 frag_size = int(frag_size + 1)
718 else:
719 frag_size = int(frag_size)
720
721 start = 0
722 end = frag_size
723 while start < end:
724 state = Trace(None, [], None)
725 if options.full_time:
726 state.min = min(res.start.keys())
727 state.max = max(res.end.keys())
728 for i in range(start, end):
729 # Add this line for reference
730 #state.add_process(pn + ":" + task, start, end)
731 for p in res.start[s_list[i]]:
732 state.add_process(p, s_list[i], res.processes[p][1])
733 start = end
734 end = end + frag_size
735 if end > len(s_list):
736 end = len(s_list)
737 res_list.append(state)
738 else:
739 res_list.append(res)
740 return res_list
diff --git a/scripts/pybootchartgui/pybootchartgui/process_tree.py b/scripts/pybootchartgui/pybootchartgui/process_tree.py
new file mode 100644
index 0000000000..cf88110b1c
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/process_tree.py
@@ -0,0 +1,292 @@
1# This file is part of pybootchartgui.
2
3# pybootchartgui is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7
8# pybootchartgui is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12
13# You should have received a copy of the GNU General Public License
14# along with pybootchartgui. If not, see <http://www.gnu.org/licenses/>.
15
16class ProcessTree:
17 """ProcessTree encapsulates a process tree. The tree is built from log files
18 retrieved during the boot process. When building the process tree, it is
19 pruned and merged in order to be able to visualize it in a comprehensible
20 manner.
21
22 The following pruning techniques are used:
23
24 * idle processes that keep running during the last process sample
25 (which is a heuristic for a background processes) are removed,
26 * short-lived processes (i.e. processes that only live for the
27 duration of two samples or less) are removed,
28 * the processes used by the boot logger are removed,
29 * exploders (i.e. processes that are known to spawn huge meaningless
30 process subtrees) have their subtrees merged together,
31 * siblings (i.e. processes with the same command line living
32 concurrently -- thread heuristic) are merged together,
33 * process runs (unary trees with processes sharing the command line)
34 are merged together.
35
36 """
37 LOGGER_PROC = 'bootchart-colle'
38 EXPLODER_PROCESSES = set(['hwup'])
39
40 def __init__(self, writer, kernel, psstats, sample_period,
41 monitoredApp, prune, idle, taskstats,
42 accurate_parentage, for_testing = False):
43 self.writer = writer
44 self.process_tree = []
45 self.taskstats = taskstats
46 if psstats is None:
47 process_list = kernel
48 elif kernel is None:
49 process_list = psstats.process_map.values()
50 else:
51 process_list = list(kernel) + list(psstats.process_map.values())
52 self.process_list = sorted(process_list, key = lambda p: p.pid)
53 self.sample_period = sample_period
54
55 self.build()
56 if not accurate_parentage:
57 self.update_ppids_for_daemons(self.process_list)
58
59 self.start_time = self.get_start_time(self.process_tree)
60 self.end_time = self.get_end_time(self.process_tree)
61 self.duration = self.end_time - self.start_time
62 self.idle = idle
63
64 if for_testing:
65 return
66
67 removed = self.merge_logger(self.process_tree, self.LOGGER_PROC, monitoredApp, False)
68 writer.status("merged %i logger processes" % removed)
69
70 if prune:
71 p_processes = self.prune(self.process_tree, None)
72 p_exploders = self.merge_exploders(self.process_tree, self.EXPLODER_PROCESSES)
73 p_threads = self.merge_siblings(self.process_tree)
74 p_runs = self.merge_runs(self.process_tree)
75 writer.status("pruned %i process, %i exploders, %i threads, and %i runs" % (p_processes, p_exploders, p_threads, p_runs))
76
77 self.sort(self.process_tree)
78
79 self.start_time = self.get_start_time(self.process_tree)
80 self.end_time = self.get_end_time(self.process_tree)
81 self.duration = self.end_time - self.start_time
82
83 self.num_proc = self.num_nodes(self.process_tree)
84
85 def build(self):
86 """Build the process tree from the list of top samples."""
87 self.process_tree = []
88 for proc in self.process_list:
89 if not proc.parent:
90 self.process_tree.append(proc)
91 else:
92 proc.parent.child_list.append(proc)
93
94 def sort(self, process_subtree):
95 """Sort process tree."""
96 for p in process_subtree:
97 p.child_list.sort(key = lambda p: p.pid)
98 self.sort(p.child_list)
99
100 def num_nodes(self, process_list):
101 "Counts the number of nodes in the specified process tree."""
102 nodes = 0
103 for proc in process_list:
104 nodes = nodes + self.num_nodes(proc.child_list)
105 return nodes + len(process_list)
106
107 def get_start_time(self, process_subtree):
108 """Returns the start time of the process subtree. This is the start
109 time of the earliest process.
110
111 """
112 if not process_subtree:
113 return 100000000
114 return min( [min(proc.start_time, self.get_start_time(proc.child_list)) for proc in process_subtree] )
115
116 def get_end_time(self, process_subtree):
117 """Returns the end time of the process subtree. This is the end time
118 of the last collected sample.
119
120 """
121 if not process_subtree:
122 return -100000000
123 return max( [max(proc.start_time + proc.duration, self.get_end_time(proc.child_list)) for proc in process_subtree] )
124
125 def get_max_pid(self, process_subtree):
126 """Returns the max PID found in the process tree."""
127 if not process_subtree:
128 return -100000000
129 return max( [max(proc.pid, self.get_max_pid(proc.child_list)) for proc in process_subtree] )
130
131 def update_ppids_for_daemons(self, process_list):
132 """Fedora hack: when loading the system services from rc, runuser(1)
133 is used. This sets the PPID of all daemons to 1, skewing
134 the process tree. Try to detect this and set the PPID of
135 these processes the PID of rc.
136
137 """
138 rcstartpid = -1
139 rcendpid = -1
140 rcproc = None
141 for p in process_list:
142 if p.cmd == "rc" and p.ppid // 1000 == 1:
143 rcproc = p
144 rcstartpid = p.pid
145 rcendpid = self.get_max_pid(p.child_list)
146 if rcstartpid != -1 and rcendpid != -1:
147 for p in process_list:
148 if p.pid > rcstartpid and p.pid < rcendpid and p.ppid // 1000 == 1:
149 p.ppid = rcstartpid
150 p.parent = rcproc
151 for p in process_list:
152 p.child_list = []
153 self.build()
154
155 def prune(self, process_subtree, parent):
156 """Prunes the process tree by removing idle processes and processes
157 that only live for the duration of a single top sample. Sibling
158 processes with the same command line (i.e. threads) are merged
159 together. This filters out sleepy background processes, short-lived
160 processes and bootcharts' analysis tools.
161 """
162 def is_idle_background_process_without_children(p):
163 process_end = p.start_time + p.duration
164 return not p.active and \
165 process_end >= self.start_time + self.duration and \
166 p.start_time > self.start_time and \
167 p.duration > 0.9 * self.duration and \
168 self.num_nodes(p.child_list) == 0
169
170 num_removed = 0
171 idx = 0
172 while idx < len(process_subtree):
173 p = process_subtree[idx]
174 if parent != None or len(p.child_list) == 0:
175
176 prune = False
177 if is_idle_background_process_without_children(p):
178 prune = True
179 elif p.duration <= 2 * self.sample_period:
180 # short-lived process
181 prune = True
182
183 if prune:
184 process_subtree.pop(idx)
185 for c in p.child_list:
186 process_subtree.insert(idx, c)
187 num_removed += 1
188 continue
189 else:
190 num_removed += self.prune(p.child_list, p)
191 else:
192 num_removed += self.prune(p.child_list, p)
193 idx += 1
194
195 return num_removed
196
197 def merge_logger(self, process_subtree, logger_proc, monitored_app, app_tree):
198 """Merges the logger's process subtree. The logger will typically
199 spawn lots of sleep and cat processes, thus polluting the
200 process tree.
201
202 """
203 num_removed = 0
204 for p in process_subtree:
205 is_app_tree = app_tree
206 if logger_proc == p.cmd and not app_tree:
207 is_app_tree = True
208 num_removed += self.merge_logger(p.child_list, logger_proc, monitored_app, is_app_tree)
209 # don't remove the logger itself
210 continue
211
212 if app_tree and monitored_app != None and monitored_app == p.cmd:
213 is_app_tree = False
214
215 if is_app_tree:
216 for child in p.child_list:
217 self.merge_processes(p, child)
218 num_removed += 1
219 p.child_list = []
220 else:
221 num_removed += self.merge_logger(p.child_list, logger_proc, monitored_app, is_app_tree)
222 return num_removed
223
224 def merge_exploders(self, process_subtree, processes):
225 """Merges specific process subtrees (used for processes which usually
226 spawn huge meaningless process trees).
227
228 """
229 num_removed = 0
230 for p in process_subtree:
231 if processes in processes and len(p.child_list) > 0:
232 subtreemap = self.getProcessMap(p.child_list)
233 for child in subtreemap.values():
234 self.merge_processes(p, child)
235 num_removed += len(subtreemap)
236 p.child_list = []
237 p.cmd += " (+)"
238 else:
239 num_removed += self.merge_exploders(p.child_list, processes)
240 return num_removed
241
242 def merge_siblings(self, process_subtree):
243 """Merges thread processes. Sibling processes with the same command
244 line are merged together.
245
246 """
247 num_removed = 0
248 idx = 0
249 while idx < len(process_subtree)-1:
250 p = process_subtree[idx]
251 nextp = process_subtree[idx+1]
252 if nextp.cmd == p.cmd:
253 process_subtree.pop(idx+1)
254 idx -= 1
255 num_removed += 1
256 p.child_list.extend(nextp.child_list)
257 self.merge_processes(p, nextp)
258 num_removed += self.merge_siblings(p.child_list)
259 idx += 1
260 if len(process_subtree) > 0:
261 p = process_subtree[-1]
262 num_removed += self.merge_siblings(p.child_list)
263 return num_removed
264
265 def merge_runs(self, process_subtree):
266 """Merges process runs. Single child processes which share the same
267 command line with the parent are merged.
268
269 """
270 num_removed = 0
271 idx = 0
272 while idx < len(process_subtree):
273 p = process_subtree[idx]
274 if len(p.child_list) == 1 and p.child_list[0].cmd == p.cmd:
275 child = p.child_list[0]
276 p.child_list = list(child.child_list)
277 self.merge_processes(p, child)
278 num_removed += 1
279 continue
280 num_removed += self.merge_runs(p.child_list)
281 idx += 1
282 return num_removed
283
284 def merge_processes(self, p1, p2):
285 """Merges two process' samples."""
286 p1.samples.extend(p2.samples)
287 p1.samples.sort( key = lambda p: p.time )
288 p1time = p1.start_time
289 p2time = p2.start_time
290 p1.start_time = min(p1time, p2time)
291 pendtime = max(p1time + p1.duration, p2time + p2.duration)
292 p1.duration = pendtime - p1.start_time
diff --git a/scripts/pybootchartgui/pybootchartgui/samples.py b/scripts/pybootchartgui/pybootchartgui/samples.py
new file mode 100644
index 0000000000..015d743aa0
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/samples.py
@@ -0,0 +1,151 @@
1# This file is part of pybootchartgui.
2
3# pybootchartgui is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7
8# pybootchartgui is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12
13# You should have received a copy of the GNU General Public License
14# along with pybootchartgui. If not, see <http://www.gnu.org/licenses/>.
15
16
17class DiskStatSample:
18 def __init__(self, time):
19 self.time = time
20 self.diskdata = [0, 0, 0]
21 def add_diskdata(self, new_diskdata):
22 self.diskdata = [ a + b for a, b in zip(self.diskdata, new_diskdata) ]
23
24class CPUSample:
25 def __init__(self, time, user, sys, io = 0.0, swap = 0.0):
26 self.time = time
27 self.user = user
28 self.sys = sys
29 self.io = io
30 self.swap = swap
31
32 @property
33 def cpu(self):
34 return self.user + self.sys
35
36 def __str__(self):
37 return str(self.time) + "\t" + str(self.user) + "\t" + \
38 str(self.sys) + "\t" + str(self.io) + "\t" + str (self.swap)
39
40class MemSample:
41 used_values = ('MemTotal', 'MemFree', 'Buffers', 'Cached', 'SwapTotal', 'SwapFree',)
42
43 def __init__(self, time):
44 self.time = time
45 self.records = {}
46
47 def add_value(self, name, value):
48 if name in MemSample.used_values:
49 self.records[name] = value
50
51 def valid(self):
52 keys = self.records.keys()
53 # discard incomplete samples
54 return [v for v in MemSample.used_values if v not in keys] == []
55
56class ProcessSample:
57 def __init__(self, time, state, cpu_sample):
58 self.time = time
59 self.state = state
60 self.cpu_sample = cpu_sample
61
62 def __str__(self):
63 return str(self.time) + "\t" + str(self.state) + "\t" + str(self.cpu_sample)
64
65class ProcessStats:
66 def __init__(self, writer, process_map, sample_count, sample_period, start_time, end_time):
67 self.process_map = process_map
68 self.sample_count = sample_count
69 self.sample_period = sample_period
70 self.start_time = start_time
71 self.end_time = end_time
72 writer.info ("%d samples, avg. sample length %f" % (self.sample_count, self.sample_period))
73 writer.info ("process list size: %d" % len (self.process_map.values()))
74
75class Process:
76 def __init__(self, writer, pid, cmd, ppid, start_time):
77 self.writer = writer
78 self.pid = pid
79 self.cmd = cmd
80 self.exe = cmd
81 self.args = []
82 self.ppid = ppid
83 self.start_time = start_time
84 self.duration = 0
85 self.samples = []
86 self.parent = None
87 self.child_list = []
88
89 self.active = None
90 self.last_user_cpu_time = None
91 self.last_sys_cpu_time = None
92
93 self.last_cpu_ns = 0
94 self.last_blkio_delay_ns = 0
95 self.last_swapin_delay_ns = 0
96
97 # split this process' run - triggered by a name change
98 def split(self, writer, pid, cmd, ppid, start_time):
99 split = Process (writer, pid, cmd, ppid, start_time)
100
101 split.last_cpu_ns = self.last_cpu_ns
102 split.last_blkio_delay_ns = self.last_blkio_delay_ns
103 split.last_swapin_delay_ns = self.last_swapin_delay_ns
104
105 return split
106
107 def __str__(self):
108 return " ".join([str(self.pid), self.cmd, str(self.ppid), '[ ' + str(len(self.samples)) + ' samples ]' ])
109
110 def calc_stats(self, samplePeriod):
111 if self.samples:
112 firstSample = self.samples[0]
113 lastSample = self.samples[-1]
114 self.start_time = min(firstSample.time, self.start_time)
115 self.duration = lastSample.time - self.start_time + samplePeriod
116
117 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] )
118 activeCount = activeCount + sum( [1 for sample in self.samples if sample.state == 'D'] )
119 self.active = (activeCount>2)
120
121 def calc_load(self, userCpu, sysCpu, interval):
122 userCpuLoad = float(userCpu - self.last_user_cpu_time) / interval
123 sysCpuLoad = float(sysCpu - self.last_sys_cpu_time) / interval
124 cpuLoad = userCpuLoad + sysCpuLoad
125 # normalize
126 if cpuLoad > 1.0:
127 userCpuLoad = userCpuLoad / cpuLoad
128 sysCpuLoad = sysCpuLoad / cpuLoad
129 return (userCpuLoad, sysCpuLoad)
130
131 def set_parent(self, processMap):
132 if self.ppid != None:
133 self.parent = processMap.get (self.ppid)
134 if self.parent == None and self.pid // 1000 > 1 and \
135 not (self.ppid == 2000 or self.pid == 2000): # kernel threads: ppid=2
136 self.writer.warn("Missing CONFIG_PROC_EVENTS: no parent for pid '%i' ('%s') with ppid '%i'" \
137 % (self.pid,self.cmd,self.ppid))
138
139 def get_end_time(self):
140 return self.start_time + self.duration
141
142class DiskSample:
143 def __init__(self, time, read, write, util):
144 self.time = time
145 self.read = read
146 self.write = write
147 self.util = util
148 self.tput = read + write
149
150 def __str__(self):
151 return "\t".join([str(self.time), str(self.read), str(self.write), str(self.util)])
diff --git a/scripts/pybootchartgui/pybootchartgui/tests/parser_test.py b/scripts/pybootchartgui/pybootchartgui/tests/parser_test.py
new file mode 100644
index 0000000000..00fb3bf797
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/tests/parser_test.py
@@ -0,0 +1,105 @@
1import sys, os, re, struct, operator, math
2from collections import defaultdict
3import unittest
4
5sys.path.insert(0, os.getcwd())
6
7import pybootchartgui.parsing as parsing
8import pybootchartgui.main as main
9
10debug = False
11
12def floatEq(f1, f2):
13 return math.fabs(f1-f2) < 0.00001
14
15bootchart_dir = os.path.join(os.path.dirname(sys.argv[0]), '../../examples/1/')
16parser = main._mk_options_parser()
17options, args = parser.parse_args(['--q', bootchart_dir])
18writer = main._mk_writer(options)
19
20class TestBCParser(unittest.TestCase):
21
22 def setUp(self):
23 self.name = "My first unittest"
24 self.rootdir = bootchart_dir
25
26 def mk_fname(self,f):
27 return os.path.join(self.rootdir, f)
28
29 def testParseHeader(self):
30 trace = parsing.Trace(writer, args, options)
31 state = parsing.parse_file(writer, trace, self.mk_fname('header'))
32 self.assertEqual(6, len(state.headers))
33 self.assertEqual(2, parsing.get_num_cpus(state.headers))
34
35 def test_parseTimedBlocks(self):
36 trace = parsing.Trace(writer, args, options)
37 state = parsing.parse_file(writer, trace, self.mk_fname('proc_diskstats.log'))
38 self.assertEqual(141, len(state.disk_stats))
39
40 def testParseProcPsLog(self):
41 trace = parsing.Trace(writer, args, options)
42 state = parsing.parse_file(writer, trace, self.mk_fname('proc_ps.log'))
43 samples = state.ps_stats
44 processes = samples.process_map
45 sorted_processes = [processes[k] for k in sorted(processes.keys())]
46
47 ps_data = open(self.mk_fname('extract2.proc_ps.log'))
48 for index, line in enumerate(ps_data):
49 tokens = line.split();
50 process = sorted_processes[index]
51 if debug:
52 print(tokens[0:4])
53 print(process.pid / 1000, process.cmd, process.ppid, len(process.samples))
54 print('-------------------')
55
56 self.assertEqual(tokens[0], str(process.pid // 1000))
57 self.assertEqual(tokens[1], str(process.cmd))
58 self.assertEqual(tokens[2], str(process.ppid // 1000))
59 self.assertEqual(tokens[3], str(len(process.samples)))
60 ps_data.close()
61
62 def testparseProcDiskStatLog(self):
63 trace = parsing.Trace(writer, args, options)
64 state_with_headers = parsing.parse_file(writer, trace, self.mk_fname('header'))
65 state_with_headers.headers['system.cpu'] = 'xxx (2)'
66 samples = parsing.parse_file(writer, state_with_headers, self.mk_fname('proc_diskstats.log')).disk_stats
67 self.assertEqual(141, len(samples))
68
69 diskstats_data = open(self.mk_fname('extract.proc_diskstats.log'))
70 for index, line in enumerate(diskstats_data):
71 tokens = line.split('\t')
72 sample = samples[index]
73 if debug:
74 print(line.rstrip())
75 print(sample)
76 print('-------------------')
77
78 self.assertEqual(tokens[0], str(sample.time))
79 self.assert_(floatEq(float(tokens[1]), sample.read))
80 self.assert_(floatEq(float(tokens[2]), sample.write))
81 self.assert_(floatEq(float(tokens[3]), sample.util))
82 diskstats_data.close()
83
84 def testparseProcStatLog(self):
85 trace = parsing.Trace(writer, args, options)
86 samples = parsing.parse_file(writer, trace, self.mk_fname('proc_stat.log')).cpu_stats
87 self.assertEqual(141, len(samples))
88
89 stat_data = open(self.mk_fname('extract.proc_stat.log'))
90 for index, line in enumerate(stat_data):
91 tokens = line.split('\t')
92 sample = samples[index]
93 if debug:
94 print(line.rstrip())
95 print(sample)
96 print('-------------------')
97 self.assert_(floatEq(float(tokens[0]), sample.time))
98 self.assert_(floatEq(float(tokens[1]), sample.user))
99 self.assert_(floatEq(float(tokens[2]), sample.sys))
100 self.assert_(floatEq(float(tokens[3]), sample.io))
101 stat_data.close()
102
103if __name__ == '__main__':
104 unittest.main()
105
diff --git a/scripts/pybootchartgui/pybootchartgui/tests/process_tree_test.py b/scripts/pybootchartgui/pybootchartgui/tests/process_tree_test.py
new file mode 100644
index 0000000000..6f46a1c03d
--- /dev/null
+++ b/scripts/pybootchartgui/pybootchartgui/tests/process_tree_test.py
@@ -0,0 +1,92 @@
1import sys
2import os
3import unittest
4
5sys.path.insert(0, os.getcwd())
6
7import pybootchartgui.parsing as parsing
8import pybootchartgui.process_tree as process_tree
9import pybootchartgui.main as main
10
11if sys.version_info >= (3, 0):
12 long = int
13
14class TestProcessTree(unittest.TestCase):
15
16 def setUp(self):
17 self.name = "Process tree unittest"
18 self.rootdir = os.path.join(os.path.dirname(sys.argv[0]), '../../examples/1/')
19
20 parser = main._mk_options_parser()
21 options, args = parser.parse_args(['--q', self.rootdir])
22 writer = main._mk_writer(options)
23 trace = parsing.Trace(writer, args, options)
24
25 parsing.parse_file(writer, trace, self.mk_fname('proc_ps.log'))
26 trace.compile(writer)
27 self.processtree = process_tree.ProcessTree(writer, None, trace.ps_stats, \
28 trace.ps_stats.sample_period, None, options.prune, None, None, False, for_testing = True)
29
30 def mk_fname(self,f):
31 return os.path.join(self.rootdir, f)
32
33 def flatten(self, process_tree):
34 flattened = []
35 for p in process_tree:
36 flattened.append(p)
37 flattened.extend(self.flatten(p.child_list))
38 return flattened
39
40 def checkAgainstJavaExtract(self, filename, process_tree):
41 test_data = open(filename)
42 for expected, actual in zip(test_data, self.flatten(process_tree)):
43 tokens = expected.split('\t')
44 self.assertEqual(int(tokens[0]), actual.pid // 1000)
45 self.assertEqual(tokens[1], actual.cmd)
46 self.assertEqual(long(tokens[2]), 10 * actual.start_time)
47 self.assert_(long(tokens[3]) - 10 * actual.duration < 5, "duration")
48 self.assertEqual(int(tokens[4]), len(actual.child_list))
49 self.assertEqual(int(tokens[5]), len(actual.samples))
50 test_data.close()
51
52 def testBuild(self):
53 process_tree = self.processtree.process_tree
54 self.checkAgainstJavaExtract(self.mk_fname('extract.processtree.1.log'), process_tree)
55
56 def testMergeLogger(self):
57 self.processtree.merge_logger(self.processtree.process_tree, 'bootchartd', None, False)
58 process_tree = self.processtree.process_tree
59 self.checkAgainstJavaExtract(self.mk_fname('extract.processtree.2.log'), process_tree)
60
61 def testPrune(self):
62 self.processtree.merge_logger(self.processtree.process_tree, 'bootchartd', None, False)
63 self.processtree.prune(self.processtree.process_tree, None)
64 process_tree = self.processtree.process_tree
65 self.checkAgainstJavaExtract(self.mk_fname('extract.processtree.3b.log'), process_tree)
66
67 def testMergeExploders(self):
68 self.processtree.merge_logger(self.processtree.process_tree, 'bootchartd', None, False)
69 self.processtree.prune(self.processtree.process_tree, None)
70 self.processtree.merge_exploders(self.processtree.process_tree, set(['hwup']))
71 process_tree = self.processtree.process_tree
72 self.checkAgainstJavaExtract(self.mk_fname('extract.processtree.3c.log'), process_tree)
73
74 def testMergeSiblings(self):
75 self.processtree.merge_logger(self.processtree.process_tree, 'bootchartd', None, False)
76 self.processtree.prune(self.processtree.process_tree, None)
77 self.processtree.merge_exploders(self.processtree.process_tree, set(['hwup']))
78 self.processtree.merge_siblings(self.processtree.process_tree)
79 process_tree = self.processtree.process_tree
80 self.checkAgainstJavaExtract(self.mk_fname('extract.processtree.3d.log'), process_tree)
81
82 def testMergeRuns(self):
83 self.processtree.merge_logger(self.processtree.process_tree, 'bootchartd', None, False)
84 self.processtree.prune(self.processtree.process_tree, None)
85 self.processtree.merge_exploders(self.processtree.process_tree, set(['hwup']))
86 self.processtree.merge_siblings(self.processtree.process_tree)
87 self.processtree.merge_runs(self.processtree.process_tree)
88 process_tree = self.processtree.process_tree
89 self.checkAgainstJavaExtract(self.mk_fname('extract.processtree.3e.log'), process_tree)
90
91if __name__ == '__main__':
92 unittest.main()