From 3b1d0ce41f940f9b81ae48438c1d20bfee6d30fa Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:49 -0800 Subject: [PATCH 001/182] command line -- nonsense file path is fatal --- pybootchartgui/parsing.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 16d0d24..90e6276 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -655,8 +655,7 @@ def parse_paths(writer, state, paths): for path in paths: root, extension = os.path.splitext(path) if not(os.path.exists(path)): - writer.warn("warning: path '%s' does not exist, ignoring." % path) - continue + raise ParseError("\n\tpath '%s' does not exist" % path) state.filename = path if os.path.isdir(path): files = [ f for f in [os.path.join(path, f) for f in os.listdir(path)] if os.path.isfile(f) ] From d1c771da87d1bfac3ae5c4d6e58ff6ccc02f4035 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:49 -0800 Subject: [PATCH 002/182] FIX panning --- pybootchartgui/gui.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index ddeb88c..f80b111 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -171,6 +171,8 @@ def on_area_motion_notify(self, area, event): state = event.state if state & gtk.gdk.BUTTON2_MASK or state & gtk.gdk.BUTTON1_MASK: x, y = int(event.x), int(event.y) + if self.prevmousex==None or self.prevmousey==None: + return True # pan the image self.x += (self.prevmousex - x)/self.zoom_ratio self.y += (self.prevmousey - y)/self.zoom_ratio From baa70cfcbc5d366c2855c3a897e7ee1b1ca5b373 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:50 -0800 Subject: [PATCH 003/182] command line abort if no bootchart.tgz named --- pybootchartgui/main.py.in | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 57f71dc..cb8dc91 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -118,8 +118,7 @@ def main(argv=None): writer = _mk_writer(options) if len(args) == 0: - print("No path given, trying /var/log/bootchart.tgz") - args = [ "/var/log/bootchart.tgz" ] + raise parsing.ParseError("\n\tNo path to a bootchart.tgz found on command line") res = parsing.Trace(writer, args, options) From 7e704a49165462f687ad90ee90e6c62a523bb729 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:50 -0800 Subject: [PATCH 004/182] GUI finer xscale steps --- pybootchartgui/gui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index f80b111..2df30b4 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -101,10 +101,10 @@ def set_xscale(self, xscale): self.zoom_image (self.zoom_ratio) def on_expand(self, action): - self.set_xscale (self.xscale * 1.5) + self.set_xscale (self.xscale * 1.1) def on_contract(self, action): - self.set_xscale (self.xscale / 1.5) + self.set_xscale (self.xscale / 1.1) def on_zoom_in(self, action): self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT) From bcfdb81753c67925d7d69191b6d2788ef95680e6 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:50 -0800 Subject: [PATCH 005/182] comment expanded --- pybootchartgui/parsing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 90e6276..92f5bd9 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -124,7 +124,8 @@ def find_parent_id_for(pid): if ppid: process.ppid = ppid * 1000 - # stitch the tree together with pointers + # Init the upward "parent" pointers. + # Downward child pointers stored in a list -- remains empty until the ProcessTree is inited. for process in self.ps_stats.process_map.values(): process.set_parent (self.ps_stats.process_map) From a0a2a34f44e4608615dd49cb152c9e9b16eb796f Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:50 -0800 Subject: [PATCH 006/182] FIX charts -- doubtful fix for divide by zero --- pybootchartgui/draw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 3a4da25..1fc3349 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -236,7 +236,7 @@ def transform_point_coords(point, x_base, y_base, \ # If data_range is given, scale the chart so that the value range in # data_range matches the chart bounds exactly. # Otherwise, scale so that the actual data matches the chart bounds. - if data_range: + if data_range and (data_range[1] - data_range[0]) > 0: yscale = float(chart_bounds[3]) / (data_range[1] - data_range[0]) ybase = data_range[0] else: From d47244da1fd45c40a4a469eb51260caaca8bc36d Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:51 -0800 Subject: [PATCH 007/182] FIX -- show final thread name -- from prctl -- when taskstats is absent --- pybootchartgui/draw.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 1fc3349..17ef64e 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -503,17 +503,17 @@ def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) : draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip) draw_rect(ctx, PROC_BORDER_COLOR, (x, y, w, proc_h)) ipid = int(proc.pid) - if not OPTIONS.show_all: - cmdString = proc.cmd - else: + if proc_tree.taskstats and OPTIONS.show_all: cmdString = '' + else: + cmdString = proc.cmd if (OPTIONS.show_pid or OPTIONS.show_all) and ipid is not 0: cmdString = cmdString + " [" + str(ipid / 1000) + "]" if OPTIONS.show_all: if proc.args: cmdString = cmdString + " '" + "' '".join(proc.args) + "'" else: - cmdString = cmdString + " " + proc.exe + cmdString = cmdString draw_label_in_box(ctx, PROC_TEXT_COLOR, cmdString, x, y + proc_h - 4, w, rect[0] + rect[2]) From 74a77ef9fcc904ade67797c12fbb27f26f69e381 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:51 -0800 Subject: [PATCH 008/182] FIX do not overfill charts --- pybootchartgui/draw.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 17ef64e..8986f77 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -255,6 +255,7 @@ def transform_point_coords(point, x_base, y_base, \ chart_bounds[0], chart_bounds[1]) ctx.line_to(x, y) if fill: + ctx.set_line_width(0.0) ctx.stroke_preserve() ctx.line_to(last[0], chart_bounds[1]+chart_bounds[3]) ctx.line_to(first[0], chart_bounds[1]+chart_bounds[3]) From 64f128c11bca78bcd05f32a85e413c0f4ff722eb Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:51 -0800 Subject: [PATCH 009/182] mouse wheel drives xscale --- pybootchartgui/gui.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index 2df30b4..b4c724a 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -159,13 +159,19 @@ def on_area_button_release(self, area, event): def on_area_scroll_event(self, area, event): if event.state & gtk.gdk.CONTROL_MASK: + if event.direction == gtk.gdk.SCROLL_UP: + self.on_expand(None) + return True + if event.direction == gtk.gdk.SCROLL_DOWN: + self.on_contract(None) + return True + elif event.state & gtk.gdk.MOD1_MASK: if event.direction == gtk.gdk.SCROLL_UP: self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT) return True if event.direction == gtk.gdk.SCROLL_DOWN: self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT) return True - return False def on_area_motion_notify(self, area, event): state = event.state From 3ca5e79a3c89f71c4450c7f578fbdd93b90119df Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:51 -0800 Subject: [PATCH 010/182] TODO --- TODO | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO b/TODO index 178423c..7ae09f8 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,7 @@ ** Bugs: +* Don't step on distro's /usr/bin/pybootchartgui -- use /usr/local/ + * FIXME: are we merging / glupping threads properly ? + how can sreadahead apparently be doing no I/O ? :-) From 71fcd8cd54e507ad683a39cd6beae188f4b8845b Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:52 -0800 Subject: [PATCH 011/182] pruning -- add option to select policy -- default heavy --- pybootchartgui/main.py.in | 8 ++++++-- pybootchartgui/process_tree.py | 23 +++++++++++------------ pybootchartgui/samples.py | 2 +- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index cb8dc91..9b8237a 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -36,8 +36,12 @@ def _mk_options_parser(): help="image format (png, svg, pdf); default format png") parser.add_option("-o", "--output", dest="output", metavar="PATH", default=None, help="output path (file or directory) where charts are stored") - parser.add_option("-n", "--no-prune", action="store_false", dest="prune", default=True, - help="do not prune the process tree") + parser.add_option("-n", "--prune", dest="prune", default="heavy", + choices=["heavy", "light", "lightest"], + help="prune the process tree: " + + "heavy, (default); " + + "light, prune only idle processes from the tree; " + + "lightest") parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="suppress informational messages") parser.add_option("-t", "--boot-time", action="store_true", dest="boottime", default=False, diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index 58bc2dd..233c904 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -38,7 +38,7 @@ class ProcessTree: EXPLODER_PROCESSES = set(['hwup']) def __init__(self, writer, kernel, psstats, sample_period, - monitoredApp, prune, idle, taskstats, + monitoredApp, option_prune, idle, taskstats, accurate_parentage, for_testing = False): self.writer = writer self.process_tree = [] @@ -67,11 +67,17 @@ def __init__(self, writer, kernel, psstats, sample_period, removed = self.merge_logger(self.process_tree, self.LOGGER_PROC, monitoredApp, False) writer.status("merged %i logger processes" % removed) - if prune: + if option_prune != "lightest": p_processes = self.prune(self.process_tree, None) - p_exploders = self.merge_exploders(self.process_tree, self.EXPLODER_PROCESSES) - p_threads = self.merge_siblings(self.process_tree) - p_runs = self.merge_runs(self.process_tree) + if option_prune == "light": + p_exploders = 0 + p_threads = 0 + p_runs = 0 + else: + p_exploders = self.merge_exploders(self.process_tree, self.EXPLODER_PROCESSES) + p_threads = self.merge_siblings(self.process_tree) + p_runs = self.merge_runs(self.process_tree) + writer.status("pruned %i process, %i exploders, %i threads, and %i runs" % (p_processes, p_exploders, p_threads, p_runs)) self.sort(self.process_tree) @@ -160,11 +166,7 @@ def prune(self, process_subtree, parent): processes and bootcharts' analysis tools. """ def is_idle_background_process_without_children(p): - process_end = p.start_time + p.duration return not p.active and \ - process_end >= self.start_time + self.duration and \ - p.start_time > self.start_time and \ - p.duration > 0.9 * self.duration and \ self.num_nodes(p.child_list) == 0 num_removed = 0 @@ -176,9 +178,6 @@ def is_idle_background_process_without_children(p): prune = False if is_idle_background_process_without_children(p): prune = True - elif p.duration <= 2 * self.sample_period: - # short-lived process - prune = True if prune: process_subtree.pop(idx) diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index ce703b8..f6a2b73 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -108,7 +108,7 @@ def calc_stats(self, samplePeriod): activeCount = sum( [1 for sample in self.samples if sample.cpu_sample and sample.cpu_sample.sys + sample.cpu_sample.user + sample.cpu_sample.io > 0.0] ) activeCount = activeCount + sum( [1 for sample in self.samples if sample.state == 'D'] ) - self.active = (activeCount>2) + self.active = (activeCount>0) # controls pruning during process_tree creation time def calc_load(self, userCpu, sysCpu, interval): userCpuLoad = float(userCpu - self.last_user_cpu_time) / interval From ade8e3d2f5cbbee843e5ce667a731bd72570a8f4 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:52 -0800 Subject: [PATCH 012/182] FOLD -- pruning -- do NOT elide bootchart collector --- pybootchartgui/draw.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 8986f77..34d807a 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -582,10 +582,6 @@ def draw_process_connecting_lines(ctx, px, py, x, y, proc_h): ctx.stroke() ctx.set_dash([]) -# elide the bootchart collector - it is quite distorting -def elide_bootchart(proc): - return proc.cmd == 'bootchartd' or proc.cmd == 'bootchart-colle' - class CumlSample: def __init__(self, proc): self.cmd = proc.cmd @@ -628,9 +624,6 @@ def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, sec_w, stat_type): else: sample_value = 'io' for proc in proc_tree.process_list: - if elide_bootchart(proc): - continue - for sample in proc.samples: total_time += getattr(sample.cpu_sample, sample_value) if not sample.time in time_hash: From 2872bd606cf8b3fe372458709de32223210f5633 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:52 -0800 Subject: [PATCH 013/182] FOLD -- pruning -- do NOT hide really tiny processes --- pybootchartgui/draw.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 34d807a..698e394 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -673,10 +673,6 @@ def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, sec_w, stat_type): process_total_time = cuml - # hide really tiny processes - if cuml * pix_per_ns <= 2: - continue - last_time = times[0] y = last_below = below[last_time] last_cuml = cuml = 0.0 From 95cc0afdffb47b48fe2b5b0dce3341920004d4dc Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:52 -0800 Subject: [PATCH 014/182] DOC group options --- pybootchartgui/main.py.in | 41 +++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 9b8237a..0aca0cf 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -29,13 +29,11 @@ def _mk_options_parser(): """Make an options parser.""" usage = "%prog [options] PATH, ..., PATH" version = "%prog v@VER@" - parser = optparse.OptionParser(usage, version=version) + parser = optparse.OptionParser(usage, version=version, + description="PATH must point to a bootchart*.tgz", + epilog=None) parser.add_option("-i", "--interactive", action="store_true", dest="interactive", default=False, help="start in active mode") - parser.add_option("-f", "--format", dest="format", default="png", choices=["png", "svg", "pdf"], - help="image format (png, svg, pdf); default format png") - parser.add_option("-o", "--output", dest="output", metavar="PATH", default=None, - help="output path (file or directory) where charts are stored") parser.add_option("-n", "--prune", dest="prune", default="heavy", choices=["heavy", "light", "lightest"], help="prune the process tree: " + @@ -44,25 +42,38 @@ def _mk_options_parser(): "lightest") parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="suppress informational messages") - parser.add_option("-t", "--boot-time", action="store_true", dest="boottime", default=False, - help="only display the boot time of the boot in text format (stdout)") - parser.add_option("--very-quiet", action="store_true", dest="veryquiet", default=False, - help="suppress all messages except errors") parser.add_option("--verbose", action="store_true", dest="verbose", default=False, help="print all messages") - parser.add_option("--profile", action="store_true", dest="profile", default=False, - help="profile rendering of chart (only useful when in batch mode indicated by -f)") parser.add_option("--show-pid", action="store_true", dest="show_pid", default=False, help="show process ids in the bootchart as 'processname [pid]'") - parser.add_option("--show-all", action="store_true", dest="show_all", default=False, + + pg_Bootchart2 = optparse.OptionGroup(parser,"Bootchart2-specific", + "Options effective only for logs coming from the Bootchart2 binary-format collector") + pg_Bootchart2.add_option("--show-all", action="store_true", dest="show_all", default=False, help="show all process information in the bootchart as '/process/path/exe [pid] [args]'") - parser.add_option("--crop-after", dest="crop_after", metavar="PROCESS", default=None, + parser.add_option_group(pg_Bootchart2) + + pg_Scripting = optparse.OptionGroup(parser,"Scripting support", + "Options most useful in scripted processing of tgz batches") + + pg_Scripting.add_option("-t", "--boot-time", action="store_true", dest="boottime", default=False, + help="only display the boot time of the boot in text format (stdout)") + pg_Scripting.add_option("-f", "--format", dest="format", default="png", choices=["png", "svg", "pdf"], + help="image format (png, svg, pdf); default format png") + pg_Scripting.add_option("--very-quiet", action="store_true", dest="veryquiet", default=False, + help="suppress all messages except errors") + pg_Scripting.add_option("-o", "--output", dest="output", metavar="PATH", default=None, + help="output path (file or directory) where charts are stored") + pg_Scripting.add_option("--profile", action="store_true", dest="profile", default=False, + help="profile rendering of chart (only useful when in batch mode indicated by -f)") + pg_Scripting.add_option("--crop-after", dest="crop_after", metavar="PROCESS", default=None, help="crop chart when idle after PROCESS is started") - parser.add_option("--annotate", action="append", dest="annotate", metavar="PROCESS", default=None, + pg_Scripting.add_option("--annotate", action="append", dest="annotate", metavar="PROCESS", default=None, help="annotate position where PROCESS is started; can be specified multiple times. " + "To create a single annotation when any one of a set of processes is started, use commas to separate the names") - parser.add_option("--annotate-file", dest="annotate_file", metavar="FILENAME", default=None, + pg_Scripting.add_option("--annotate-file", dest="annotate_file", metavar="FILENAME", default=None, help="filename to write annotation points to") + parser.add_option_group(pg_Scripting) return parser class Writer: From d2eac5fb8f24994ba8b75de215292e087fb131f5 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:53 -0800 Subject: [PATCH 015/182] COMMENT only -- taskstats -- treat process labels as implicit prctl events --- pybootchartgui/samples.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index f6a2b73..bbea9bd 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -87,6 +87,7 @@ def __init__(self, writer, pid, cmd, ppid, start_time): self.last_swapin_delay_ns = 0 # split this process' run - triggered by a name change + # XX called only if taskstats.log is provided (bootchart2 daemon) def split(self, writer, pid, cmd, ppid, start_time): split = Process (writer, pid, cmd, ppid, start_time) From 7aaa26b315f411d7ead551af9e596505354bdc8b Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:53 -0800 Subject: [PATCH 016/182] IO charts per partition -- generalize to sequence of charts 3.partitions on command line --- pybootchartgui/draw.py | 32 +++++++++++--------- pybootchartgui/parsing.py | 64 ++++++++++++++++++++++----------------- pybootchartgui/samples.py | 15 ++++++--- 3 files changed, 64 insertions(+), 47 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 698e394..1703df5 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -322,21 +322,25 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w): draw_legend_line(ctx, "Disk throughput", DISK_TPUT_COLOR, off_x, curr_y+20, leg_s) draw_legend_box(ctx, "Disk utilization", IO_COLOR, off_x + 120, curr_y+20, leg_s) - # render I/O utilization - chart_rect = (off_x, curr_y+30, w, bar_h) - if clip_visible (clip, chart_rect): - draw_box_ticks (ctx, chart_rect, sec_w) - draw_annotations (ctx, proc_tree, trace.times, chart_rect) - draw_chart (ctx, IO_COLOR, True, chart_rect, \ - [(sample.time, sample.util) for sample in trace.disk_stats], \ - proc_tree, None) + curr_y += 5 - # render disk throughput - max_sample = max (trace.disk_stats, key = lambda s: s.tput) - if clip_visible (clip, chart_rect): - draw_chart (ctx, DISK_TPUT_COLOR, False, chart_rect, \ - [(sample.time, sample.tput) for sample in trace.disk_stats], \ - proc_tree, None) + # render I/O utilization + for partition in trace.disk_stats: + draw_text(ctx, partition.name, TEXT_COLOR, off_x, curr_y+35) + chart_rect = (off_x, curr_y+30+10, w, bar_h) + if clip_visible (clip, chart_rect): + draw_box_ticks (ctx, chart_rect, sec_w) + draw_annotations (ctx, proc_tree, trace.times, chart_rect) + draw_chart (ctx, IO_COLOR, True, chart_rect, \ + [(sample.time, sample.util) for sample in partition.samples], \ + proc_tree, None) + + # render disk throughput + max_sample = max (partition.samples, key = lambda s: s.tput) + if clip_visible (clip, chart_rect): + draw_chart (ctx, DISK_TPUT_COLOR, False, chart_rect, + [(sample.time, sample.tput) for sample in partition.samples], \ + proc_tree, None) pos_x = off_x + ((max_sample.time - proc_tree.start_time) * w / proc_tree.duration) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 92f5bd9..c2f7df7 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -43,7 +43,7 @@ def __init__(self, writer, paths, options): self.parent_map = None self.mem_stats = None - parse_paths (writer, self, paths) + parse_paths (writer, self, paths, options) if not self.valid(): raise ParseError("empty state: '%s' does not contain a valid bootchart" % ", ".join(paths)) @@ -402,26 +402,46 @@ def _parse_proc_stat_log(file): # skip the rest of statistics lines return samples -def _parse_proc_disk_stat_log(file, numCpu): +def delta_disk_samples(disk_stat_samples, numCpu): + disk_stats = [] + for sample1, sample2 in zip(disk_stat_samples[:-1], disk_stat_samples[1:]): + interval = sample1.time - sample2.time + if interval == 0: + print("time between samples is 0!") + interval = 1 + sums = [ a - b for a, b in zip(sample1.diskdata, sample2.diskdata) ] + readTput = sums[0] / 2.0 * 100.0 / interval # XXX why divide by 2.0 ? + writeTput = sums[1] / 2.0 * 100.0 / interval + util = float( sums[2] ) / 10 / interval / numCpu + util = max(0.0, min(1.0, util)) + disk_stats.append(DiskSample(sample2.time, readTput, writeTput, util)) + return disk_stats + +def _parse_proc_disk_stat_log(file, options, numCpu): """ Parse file for disk stats, but only look at the whole device, eg. sda, - not sda1, sda2 etc. The format of relevant lines should be: + not sda1, sda2 etc, unless show_partitions is True. + The format of relevant lines should be: {major minor name rio rmerge rsect ruse wio wmerge wsect wuse running use aveq} + The file is generated by block/genhd.c + FIXME: for Flash devices, rio/wio may have more usefulness than rsect/wsect. """ - disk_regex_re = re.compile ('^([hsv]d.|mtdblock\d|mmcblk\d|cciss/c\d+d\d+.*)$') + disk_regex_re = re.compile ('^([hsv]d.|mtdblock\d|mmcblk\d|cciss/c\d+d\d+.*)$') # this gets called an awful lot. - def is_relevant_line(linetokens): + def is_relevant_line(linetokens, regex_re): if len(linetokens) != 14: return False disk = linetokens[2] - return disk_regex_re.match(disk) + return regex_re.match(disk) disk_stat_samples = [] for time, lines in _parse_timed_blocks(file): sample = DiskStatSample(time) - relevant_tokens = [linetokens for linetokens in map (lambda x: x.split(),lines) if is_relevant_line(linetokens)] + relevant_tokens = [linetokens for linetokens in map (lambda x: x.split(),lines) + if is_relevant_line(linetokens, + disk_regex_re)] for tokens in relevant_tokens: disk, rsect, wsect, use = tokens[2], int(tokens[5]), int(tokens[9]), int(tokens[12]) @@ -429,19 +449,7 @@ def is_relevant_line(linetokens): disk_stat_samples.append(sample) - disk_stats = [] - for sample1, sample2 in zip(disk_stat_samples[:-1], disk_stat_samples[1:]): - interval = sample1.time - sample2.time - if interval == 0: - interval = 1 - sums = [ a - b for a, b in zip(sample1.diskdata, sample2.diskdata) ] - readTput = sums[0] / 2.0 * 100.0 / interval - writeTput = sums[1] / 2.0 * 100.0 / interval - util = float( sums[2] ) / 10 / interval / numCpu - util = max(0.0, min(1.0, util)) - disk_stats.append(DiskSample(sample2.time, readTput, writeTput, util)) - - return disk_stats + return [DiskSamples("Sum over all disks and partitions", delta_disk_samples(disk_stat_samples, numCpu))] def _parse_proc_meminfo_log(file): """ @@ -617,13 +625,13 @@ def get_num_cpus(headers): return 1 return max (int(mat.group(1)), 1) -def _do_parse(writer, state, name, file): +def _do_parse(writer, state, name, file, options): writer.status("parsing '%s'" % name) t1 = clock() if name == "header": state.headers = _parse_headers(file) elif name == "proc_diskstats.log": - state.disk_stats = _parse_proc_disk_stat_log(file, get_num_cpus(state.headers)) + state.disk_stats = _parse_proc_disk_stat_log(file, options, get_num_cpus(state.headers)) elif name == "taskstats.log": state.ps_stats = _parse_taskstats_log(writer, file) state.taskstats = True @@ -645,14 +653,14 @@ def _do_parse(writer, state, name, file): writer.info(" %s seconds" % str(t2-t1)) return state -def parse_file(writer, state, filename): +def parse_file(writer, state, filename, options): if state.filename is None: state.filename = filename basename = os.path.basename(filename) with open(filename, "rb") as file: - return _do_parse(writer, state, basename, file) + return _do_parse(writer, state, basename, file, options) -def parse_paths(writer, state, paths): +def parse_paths(writer, state, paths, options): for path in paths: root, extension = os.path.splitext(path) if not(os.path.exists(path)): @@ -661,7 +669,7 @@ def parse_paths(writer, state, paths): if os.path.isdir(path): files = [ f for f in [os.path.join(path, f) for f in os.listdir(path)] if os.path.isfile(f) ] files.sort() - state = parse_paths(writer, state, files) + state = parse_paths(writer, state, files, options) elif extension in [".tar", ".tgz", ".gz"]: if extension == ".gz": root, extension = os.path.splitext(root) @@ -673,12 +681,12 @@ def parse_paths(writer, state, paths): writer.status("parsing '%s'" % path) tf = tarfile.open(path, 'r:*') for name in tf.getnames(): - state = _do_parse(writer, state, name, tf.extractfile(name)) + state = _do_parse(writer, state, name, tf.extractfile(name), options) except tarfile.ReadError as error: raise ParseError("error: could not read tarfile '%s': %s." % (path, error)) finally: if tf != None: tf.close() else: - state = parse_file(writer, state, path) + state = parse_file(writer, state, path, options) return state diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index bbea9bd..07a5194 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -135,10 +135,15 @@ def get_end_time(self): class DiskSample: def __init__(self, time, read, write, util): self.time = time - self.read = read - self.write = write - self.util = util + self.read = read # a delta relative to the preceding time + self.write = write # ~ + self.util = util # ~ self.tput = read + write - def __str__(self): - return "\t".join([str(self.time), str(self.read), str(self.write), str(self.util)]) +class DiskSamples: + def __init__(self, name, samples): + self.name = name + self.samples = samples +# +# def __str__(self): +# return "\t".join([str(self.time), str(self.read), str(self.write), str(self.util)]) From f627f8beaf5d80dce4a01af3c59058e357a34b13 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:53 -0800 Subject: [PATCH 017/182] IO charts per partition 9.partitions on command line --- pybootchartgui/draw.py | 22 +++++------ pybootchartgui/main.py.in | 22 ++++++++++- pybootchartgui/parsing.py | 81 +++++++++++++++++++++++++-------------- 3 files changed, 84 insertions(+), 41 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 1703df5..aca7824 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -267,7 +267,6 @@ def transform_point_coords(point, x_base, y_base, \ bar_h = 55 meminfo_bar_h = 2 * bar_h -header_h = 110 + 2 * (30 + bar_h) + 1 * (30 + meminfo_bar_h) # offsets off_x, off_y = 10, 10 sec_w_base = 50 # the width of a second @@ -282,7 +281,7 @@ def extents(options, xscale, trace): w = int (proc_tree.duration * sec_w_base * xscale / 100) + 2*off_x h = proc_h * proc_tree.num_proc + 2 * off_y if options.charts: - h += header_h + h += 110 + (2 + len(trace.disk_stats)) * (30 + bar_h) + 1 * (30 + meminfo_bar_h) if proc_tree.taskstats and options.cumulative: h += CUML_HEIGHT + 4 * off_y return (w, h) @@ -326,8 +325,9 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w): # render I/O utilization for partition in trace.disk_stats: - draw_text(ctx, partition.name, TEXT_COLOR, off_x, curr_y+35) - chart_rect = (off_x, curr_y+30+10, w, bar_h) + draw_text(ctx, partition.name, TEXT_COLOR, off_x, curr_y+30) + + chart_rect = (off_x, curr_y+30+5, w, bar_h) if clip_visible (clip, chart_rect): draw_box_ticks (ctx, chart_rect, sec_w) draw_annotations (ctx, proc_tree, trace.times, chart_rect) @@ -342,16 +342,16 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w): [(sample.time, sample.tput) for sample in partition.samples], \ proc_tree, None) - pos_x = off_x + ((max_sample.time - proc_tree.start_time) * w / proc_tree.duration) + pos_x = off_x + ((max_sample.time - proc_tree.start_time) * w / proc_tree.duration) - shift_x, shift_y = -20, 20 - if (pos_x < off_x + 245): - shift_x, shift_y = 5, 40 + shift_x, shift_y = -20, 20 + if (pos_x < off_x + 245): + shift_x, shift_y = 5, 40 - label = "%dMB/s" % round ((max_sample.tput) / 1024.0) - draw_text (ctx, label, DISK_TPUT_COLOR, pos_x + shift_x, curr_y + shift_y) + label = "%dMB/s" % round ((max_sample.tput) / 1024.0) + draw_text (ctx, label, DISK_TPUT_COLOR, pos_x + shift_x, curr_y + shift_y) - curr_y = curr_y + 30 + bar_h + curr_y = curr_y + 30 + bar_h # render mem usage chart_rect = (off_x, curr_y+30, w, meminfo_bar_h) diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 0aca0cf..939e249 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -22,16 +22,34 @@ import sys import os import optparse +from copy import copy +from optparse import Option, OptionValueError + from . import parsing from . import batch +class ExtendOption(Option): + ACTIONS = Option.ACTIONS + ("extend",) + STORE_ACTIONS = Option.STORE_ACTIONS + ("extend",) + TYPED_ACTIONS = Option.TYPED_ACTIONS + ("extend",) + ALWAYS_TYPED_ACTIONS = Option.ALWAYS_TYPED_ACTIONS + ("extend",) + + def take_action(self, action, dest, opt, value, values, parser): + if action == "extend": + lvalue = value.split(",") + values.ensure_value(dest, []).extend(lvalue) + else: + Option.take_action( + self, action, dest, opt, value, values, parser) + def _mk_options_parser(): """Make an options parser.""" usage = "%prog [options] PATH, ..., PATH" version = "%prog v@VER@" parser = optparse.OptionParser(usage, version=version, description="PATH must point to a bootchart*.tgz", - epilog=None) + epilog=None, + option_class=ExtendOption) parser.add_option("-i", "--interactive", action="store_true", dest="interactive", default=False, help="start in active mode") parser.add_option("-n", "--prune", dest="prune", default="heavy", @@ -44,6 +62,8 @@ def _mk_options_parser(): help="suppress informational messages") parser.add_option("--verbose", action="store_true", dest="verbose", default=False, help="print all messages") + parser.add_option("-p", "--show-partitions", action="extend", dest="partitions", type="string", default=[], + help="draw a disk stat chart for any block device partitions in this list") parser.add_option("--show-pid", action="store_true", dest="show_pid", default=False, help="show process ids in the bootchart as 'processname [pid]'") diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index c2f7df7..301e52b 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -402,46 +402,46 @@ def _parse_proc_stat_log(file): # skip the rest of statistics lines return samples -def delta_disk_samples(disk_stat_samples, numCpu): - disk_stats = [] - for sample1, sample2 in zip(disk_stat_samples[:-1], disk_stat_samples[1:]): - interval = sample1.time - sample2.time - if interval == 0: - print("time between samples is 0!") - interval = 1 - sums = [ a - b for a, b in zip(sample1.diskdata, sample2.diskdata) ] - readTput = sums[0] / 2.0 * 100.0 / interval # XXX why divide by 2.0 ? - writeTput = sums[1] / 2.0 * 100.0 / interval - util = float( sums[2] ) / 10 / interval / numCpu - util = max(0.0, min(1.0, util)) - disk_stats.append(DiskSample(sample2.time, readTput, writeTput, util)) - return disk_stats - def _parse_proc_disk_stat_log(file, options, numCpu): """ - Parse file for disk stats, but only look at the whole device, eg. sda, - not sda1, sda2 etc, unless show_partitions is True. + Parse file for disk stats, summing over all physical storage devices, eg. sda, sdb. + Also parse stats for individual devices or partitions indicated on the command line. The format of relevant lines should be: {major minor name rio rmerge rsect ruse wio wmerge wsect wuse running use aveq} The file is generated by block/genhd.c FIXME: for Flash devices, rio/wio may have more usefulness than rsect/wsect. """ - disk_regex_re = re.compile ('^([hsv]d.|mtdblock\d|mmcblk\d|cciss/c\d+d\d+.*)$') - # this gets called an awful lot. - def is_relevant_line(linetokens, regex_re): - if len(linetokens) != 14: - return False - disk = linetokens[2] - return regex_re.match(disk) + def delta_disk_samples(disk_stat_samples, numCpu): + disk_stats = [] + for sample1, sample2 in zip(disk_stat_samples[:-1], disk_stat_samples[1:]): + interval = sample1.time - sample2.time + if interval == 0: + print("time between samples is 0!") + interval = 1 + sums = [ a - b for a, b in zip(sample1.diskdata, sample2.diskdata) ] + readTput = sums[0] / 2.0 * 100.0 / interval # XXX why divide by 2.0 ? + writeTput = sums[1] / 2.0 * 100.0 / interval + util = float( sums[2] ) / 10 / interval / numCpu + util = max(0.0, min(1.0, util)) + disk_stats.append(DiskSample(sample2.time, readTput, writeTput, util)) + return disk_stats + + def get_relevant_tokens(lines, regex): + def is_relevant_line(linetokens, regex): + if len(linetokens) != 14: + return False + return regex.match(linetokens[2]) + return [linetokens for linetokens in map (lambda x: x.split(),lines) + if is_relevant_line(linetokens, regex)] + + # matched not against whole line, but field only + disk_regex_re = re.compile('^([hsv]d.|mtdblock\d|mmcblk\d|cciss/c\d+d\d+.*)$') disk_stat_samples = [] - for time, lines in _parse_timed_blocks(file): sample = DiskStatSample(time) - relevant_tokens = [linetokens for linetokens in map (lambda x: x.split(),lines) - if is_relevant_line(linetokens, - disk_regex_re)] + relevant_tokens = get_relevant_tokens( lines, disk_regex_re) for tokens in relevant_tokens: disk, rsect, wsect, use = tokens[2], int(tokens[5]), int(tokens[9]), int(tokens[12]) @@ -449,7 +449,30 @@ def is_relevant_line(linetokens, regex_re): disk_stat_samples.append(sample) - return [DiskSamples("Sum over all disks and partitions", delta_disk_samples(disk_stat_samples, numCpu))] + partition_samples = [DiskSamples("Sum over all disks", + delta_disk_samples(disk_stat_samples, numCpu))] + + strip_slash_dev_slash = re.compile("/dev/(.*)$") + if options.partitions: + for part in options.partitions: + file.seek(0) + disk_stat_samples = [] + this_partition_regex_re = re.compile('^' + part + '.*$') + + # for every timed_block + disk_stat_samples = [] + for time, lines in _parse_timed_blocks(file): + sample = DiskStatSample(time) + relevant_tokens = get_relevant_tokens( lines, this_partition_regex_re) + token = relevant_tokens[0] + disk_name, rsect, wsect, use = token[2], int(token[5]), int(token[9]), int(token[12]) + sample.add_diskdata([rsect, wsect, use]) + disk_stat_samples.append(sample) + + partition_samples.append(DiskSamples(disk_name, + delta_disk_samples(disk_stat_samples, numCpu))) + + return partition_samples def _parse_proc_meminfo_log(file): """ From f82a813943a9c1e1721fd4c6ec64eccde5a1681c Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:53 -0800 Subject: [PATCH 018/182] gui -- bigger window --- pybootchartgui/gui.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index b4c724a..9158aca 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -327,7 +327,9 @@ def __init__(self, trace, app_options): window = self window.set_title("Bootchart %s" % trace.filename) - window.set_default_size(750, 550) + screen = window.get_screen() + window.set_default_size(screen.get_width() * 95/100, + screen.get_height() * 95/100) tab_page = gtk.Notebook() tab_page.show() From f31d94cee389d7e3bf9e8b376d9a5954e1aa4582 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:53 -0800 Subject: [PATCH 019/182] gui -- xscale cosmetics --- pybootchartgui/gui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index 9158aca..c0cd108 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -281,8 +281,8 @@ def __init__(self, window, trace, options, xscale): # Create actions actiongroup.add_actions(( - ('Expand', gtk.STOCK_ADD, None, None, None, self.widget.on_expand), - ('Contract', gtk.STOCK_REMOVE, None, None, None, self.widget.on_contract), + ('Expand', gtk.STOCK_ORIENTATION_LANDSCAPE, None, None, None, self.widget.on_expand), + ('Contract', gtk.STOCK_ORIENTATION_PORTRAIT, None, None, None, self.widget.on_contract), ('ZoomIn', gtk.STOCK_ZOOM_IN, None, None, None, self.widget.on_zoom_in), ('ZoomOut', gtk.STOCK_ZOOM_OUT, None, None, None, self.widget.on_zoom_out), ('ZoomFit', gtk.STOCK_ZOOM_FIT, 'Fit Width', None, None, self.widget.on_zoom_fit), From ac70c0988400f6fa27cdb08281c876aa39b8839b Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:54 -0800 Subject: [PATCH 020/182] add option to left justify process labels -- default to left --- pybootchartgui/draw.py | 7 ++++++- pybootchartgui/main.py.in | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index aca7824..aeac78a 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -130,6 +130,8 @@ def proc_tree (self, trace): STATE_COLORS = [(0, 0, 0, 0), PROC_COLOR_R, PROC_COLOR_S, PROC_COLOR_D, \ PROC_COLOR_T, PROC_COLOR_Z, PROC_COLOR_X, PROC_COLOR_W] +JUSTIFY_LEFT = "left" + # CumulativeStats Types STAT_TYPE_CPU = 0 STAT_TYPE_IO = 1 @@ -166,7 +168,10 @@ def draw_legend_line(ctx, label, fill_color, x, y, s): def draw_label_in_box(ctx, color, label, x, y, w, maxx): label_w = ctx.text_extents(label)[2] - label_x = x + w / 2 - label_w / 2 + if OPTIONS.justify == JUSTIFY_LEFT: + label_x = x + else: + label_x = x + w / 2 - label_w / 2 # CENTER if label_w + 10 > w: label_x = x + w + 5 if label_x + label_w > maxx: diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 939e249..fd8d7c2 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -66,6 +66,8 @@ def _mk_options_parser(): help="draw a disk stat chart for any block device partitions in this list") parser.add_option("--show-pid", action="store_true", dest="show_pid", default=False, help="show process ids in the bootchart as 'processname [pid]'") + parser.add_option("-j", "--justify", dest="justify", default="left", choices=["left", "center"], + help="relocate the text within process bars (left, center)") pg_Bootchart2 = optparse.OptionGroup(parser,"Bootchart2-specific", "Options effective only for logs coming from the Bootchart2 binary-format collector") From 900ad41bd9197a01d611c2a370d2ae6079d55429 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:54 -0800 Subject: [PATCH 021/182] add option to relabel partitions --- pybootchartgui/main.py.in | 2 ++ pybootchartgui/parsing.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index fd8d7c2..c30367f 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -64,6 +64,8 @@ def _mk_options_parser(): help="print all messages") parser.add_option("-p", "--show-partitions", action="extend", dest="partitions", type="string", default=[], help="draw a disk stat chart for any block device partitions in this list") + parser.add_option("-P", "--relabel-partitions", action="extend", dest="partition_labels", default=[], + help="list of per-partition strings, to be drawn instead of the raw per-partition device names") parser.add_option("--show-pid", action="store_true", dest="show_pid", default=False, help="show process ids in the bootchart as 'processname [pid]'") parser.add_option("-j", "--justify", dest="justify", default="left", choices=["left", "center"], diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 301e52b..9b43e40 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -469,6 +469,9 @@ def is_relevant_line(linetokens, regex): sample.add_diskdata([rsect, wsect, use]) disk_stat_samples.append(sample) + if options.partition_labels: + disk_name = options.partition_labels[0] + options.partition_labels = options.partition_labels[1:] partition_samples.append(DiskSamples(disk_name, delta_disk_samples(disk_stat_samples, numCpu))) From ad22b4968617bc9f00384ef562984ce8f67c37da Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:55 -0800 Subject: [PATCH 022/182] CPU chart -- add procs_running procs_blocked to CPU chart --- pybootchartgui/draw.py | 17 ++++++++++++++++- pybootchartgui/parsing.py | 20 +++++++++++++------- pybootchartgui/samples.py | 4 +++- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index aeac78a..ba3f686 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -65,6 +65,9 @@ def proc_tree (self, trace): CPU_COLOR = (0.40, 0.55, 0.70, 1.0) # IO wait chart color. IO_COLOR = (0.76, 0.48, 0.48, 0.5) +PROCS_RUNNING_COLOR = (0.0, 1.0, 0.0, 1.0) +PROCS_BLOCKED_COLOR = (0.7, 0.0, 0.0, 1.0) + # Disk throughput color. DISK_TPUT_COLOR = (0.20, 0.71, 0.20, 1.0) # CPU load chart color. @@ -223,7 +226,7 @@ def draw_annotations(ctx, proc_tree, times, rect): ctx.set_dash([]) def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree, data_range): - ctx.set_line_width(0.5) + ctx.set_line_width(1.0) x_shift = proc_tree.start_time def transform_point_coords(point, x_base, y_base, \ @@ -306,6 +309,10 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w): draw_legend_box(ctx, "CPU (user+sys)", CPU_COLOR, off_x, curr_y+20, leg_s) draw_legend_box(ctx, "I/O (wait)", IO_COLOR, off_x + 120, curr_y+20, leg_s) + draw_legend_box(ctx, "Runnable threads", PROCS_RUNNING_COLOR, + off_x +120 +80, curr_y+20, leg_s) + draw_legend_box(ctx, "Blocked threads -- uninterruptible sleep (I/O)", PROCS_BLOCKED_COLOR, + off_x +120 +80 +140, curr_y+20, leg_s) # render I/O wait chart_rect = (off_x, curr_y+30, w, bar_h) @@ -320,6 +327,14 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w): [(sample.time, sample.user + sample.sys) for sample in trace.cpu_stats], \ proc_tree, None) + draw_chart (ctx, PROCS_RUNNING_COLOR, False, chart_rect, + [(sample.time, sample.procs_running) for sample in trace.cpu_stats], \ + proc_tree, [0, 9]) + + draw_chart (ctx, PROCS_BLOCKED_COLOR, False, chart_rect, + [(sample.time, sample.procs_blocked) for sample in trace.cpu_stats], \ + proc_tree, [0, 9]) + curr_y = curr_y + 30 + bar_h # render second chart diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 9b43e40..7b96b42 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -280,7 +280,7 @@ def _parse_proc_ps_log(writer, file): if process.last_user_cpu_time is not None and process.last_sys_cpu_time is not None and ltime is not None: userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) - cpuSample = CPUSample('null', userCpuLoad, sysCpuLoad, 0.0) + cpuSample = CPUSample('null', userCpuLoad, sysCpuLoad, 0.0, 0.0) process.samples.append(ProcessSample(time, state, cpuSample)) process.last_user_cpu_time = userCpu @@ -383,10 +383,10 @@ def _parse_proc_stat_log(file): samples = [] ltimes = None for time, lines in _parse_timed_blocks(file): - # skip emtpy lines + # skip empty lines if not lines: continue - # CPU times {user, nice, system, idle, io_wait, irq, softirq} + # CPU times {user, nice, system, idle, io_wait, irq, softirq} summed over all cores. tokens = lines[0].split() times = [ int(token) for token in tokens[1:] ] if ltimes: @@ -394,12 +394,18 @@ def _parse_proc_stat_log(file): system = float((times[2] + times[5] + times[6]) - (ltimes[2] + ltimes[5] + ltimes[6])) idle = float(times[3] - ltimes[3]) iowait = float(times[4] - ltimes[4]) - aSum = max(user + system + idle + iowait, 1) - samples.append( CPUSample(time, user/aSum, system/aSum, iowait/aSum) ) - + procs_running = 0 + procs_blocked = 0 + + for line in lines: + tokens = line.split() + if tokens[0] == 'procs_running': + procs_running = int(tokens[1]) + if tokens[0] == 'procs_blocked': + procs_blocked = int(tokens[1]) + samples.append( CPUSample(time, user/aSum, system/aSum, iowait/aSum, 0.0, procs_running, procs_blocked) ) ltimes = times - # skip the rest of statistics lines return samples def _parse_proc_disk_stat_log(file, options, numCpu): diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 07a5194..cf1f153 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -22,12 +22,14 @@ def add_diskdata(self, new_diskdata): self.diskdata = [ a + b for a, b in zip(self.diskdata, new_diskdata) ] class CPUSample: - def __init__(self, time, user, sys, io = 0.0, swap = 0.0): + def __init__(self, time, user, sys, io, swap = 0.0, procs_running = 0, procs_blocked = 0): self.time = time self.user = user self.sys = sys self.io = io self.swap = swap + self.procs_running = procs_running + self.procs_blocked = procs_blocked @property def cpu(self): From ae2c3b3db388825fcd68f12edea108225b2584e9 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:55 -0800 Subject: [PATCH 023/182] FIX save first per process sample too --- pybootchartgui/parsing.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 7b96b42..51712fe 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -278,8 +278,11 @@ def _parse_proc_ps_log(writer, file): process = Process(writer, pid, cmd.strip('()'), ppid, min(time, stime)) processMap[pid] = process - if process.last_user_cpu_time is not None and process.last_sys_cpu_time is not None and ltime is not None: - userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) + if process.last_user_cpu_time is not None and process.last_sys_cpu_time is not None: + if ltime is None: + userCpuLoad, sysCpuLoad = 0, 0 + else: + userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) cpuSample = CPUSample('null', userCpuLoad, sysCpuLoad, 0.0, 0.0) process.samples.append(ProcessSample(time, state, cpuSample)) From c32780bfe531471f83cea8b4f6beba1c1e4199df Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:55 -0800 Subject: [PATCH 024/182] FIX save first CPU sample --- pybootchartgui/parsing.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 51712fe..b5205c7 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -398,16 +398,19 @@ def _parse_proc_stat_log(file): idle = float(times[3] - ltimes[3]) iowait = float(times[4] - ltimes[4]) aSum = max(user + system + idle + iowait, 1) - procs_running = 0 - procs_blocked = 0 - - for line in lines: - tokens = line.split() - if tokens[0] == 'procs_running': - procs_running = int(tokens[1]) - if tokens[0] == 'procs_blocked': - procs_blocked = int(tokens[1]) + + procs_running = 0 + procs_blocked = 0 + for line in lines: + tokens = line.split() + if tokens[0] == 'procs_running': + procs_running = int(tokens[1]) + if tokens[0] == 'procs_blocked': + procs_blocked = int(tokens[1]) + if ltimes: samples.append( CPUSample(time, user/aSum, system/aSum, iowait/aSum, 0.0, procs_running, procs_blocked) ) + else: + samples.append( CPUSample(time, 0.0, 0.0, 0.0, 0.0, procs_running, procs_blocked) ) ltimes = times return samples From 757032e535ff69d3bada20cd6f6166afe0919fb3 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:55 -0800 Subject: [PATCH 025/182] charts -- square charts --- pybootchartgui/draw.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index ba3f686..5ba6de0 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -225,7 +225,7 @@ def draw_annotations(ctx, proc_tree, times, rect): ctx.set_line_cap(cairo.LINE_CAP_BUTT) ctx.set_dash([]) -def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree, data_range): +def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree, data_range, square = False): ctx.set_line_width(1.0) x_shift = proc_tree.start_time @@ -258,9 +258,13 @@ def transform_point_coords(point, x_base, y_base, \ ctx.set_source_rgba(*color) ctx.move_to(*first) + prev_y = first[1] for point in data: x, y = transform_point_coords (point, x_shift, ybase, xscale, yscale, \ chart_bounds[0], chart_bounds[1]) + if square: + ctx.line_to(x, prev_y) + prev_y = y ctx.line_to(x, y) if fill: ctx.set_line_width(0.0) @@ -329,11 +333,11 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w): draw_chart (ctx, PROCS_RUNNING_COLOR, False, chart_rect, [(sample.time, sample.procs_running) for sample in trace.cpu_stats], \ - proc_tree, [0, 9]) + proc_tree, [0, 9], True) draw_chart (ctx, PROCS_BLOCKED_COLOR, False, chart_rect, [(sample.time, sample.procs_blocked) for sample in trace.cpu_stats], \ - proc_tree, [0, 9]) + proc_tree, [0, 9], True) curr_y = curr_y + 30 + bar_h From dcce7fcdbb30bb6c1e6e1b69022c04f39f03c763 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:55 -0800 Subject: [PATCH 026/182] charts -- always square charts --- pybootchartgui/draw.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 5ba6de0..962d8ea 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -225,7 +225,7 @@ def draw_annotations(ctx, proc_tree, times, rect): ctx.set_line_cap(cairo.LINE_CAP_BUTT) ctx.set_dash([]) -def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree, data_range, square = False): +def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree, data_range, square = True): ctx.set_line_width(1.0) x_shift = proc_tree.start_time @@ -325,7 +325,7 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w): draw_annotations (ctx, proc_tree, trace.times, chart_rect) draw_chart (ctx, IO_COLOR, True, chart_rect, \ [(sample.time, sample.user + sample.sys + sample.io) for sample in trace.cpu_stats], \ - proc_tree, None) + proc_tree, None, True) # render CPU load draw_chart (ctx, CPU_COLOR, True, chart_rect, \ [(sample.time, sample.user + sample.sys) for sample in trace.cpu_stats], \ From 61709261fd45cbefbb170b6376fc512e58bcce4a Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:56 -0800 Subject: [PATCH 027/182] charts -- draw.py -- draw non zero horizontals only -- for square unfilled charts --- pybootchartgui/draw.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 962d8ea..4e7aeb4 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -226,7 +226,10 @@ def draw_annotations(ctx, proc_tree, times, rect): ctx.set_dash([]) def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree, data_range, square = True): - ctx.set_line_width(1.0) + if square and not fill: + ctx.set_line_width(2.0) + else: + ctx.set_line_width(1.0) x_shift = proc_tree.start_time def transform_point_coords(point, x_base, y_base, \ @@ -259,13 +262,24 @@ def transform_point_coords(point, x_base, y_base, \ ctx.set_source_rgba(*color) ctx.move_to(*first) prev_y = first[1] + prev_point = data[0] for point in data: x, y = transform_point_coords (point, x_shift, ybase, xscale, yscale, \ chart_bounds[0], chart_bounds[1]) if square: - ctx.line_to(x, prev_y) + if fill: + ctx.line_to(x, prev_y) # rightward + ctx.line_to(x, y) # upward or downward + else: + # draw horizontals only, and then only if non-zero -- result cannot be "filled" + if prev_point[1] > 0: + ctx.line_to(x, prev_y) # rightward + ctx.move_to(x, y) # upward or downward, maybe rightward + prev_point = point prev_y = y - ctx.line_to(x, y) + else: + ctx.line_to(x, y) + if fill: ctx.set_line_width(0.0) ctx.stroke_preserve() From 53b847968363683de1da85dd4af9faa5a95cc810 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:56 -0800 Subject: [PATCH 028/182] IO charts add written bytes chart --- pybootchartgui/draw.py | 33 ++++++++++++++++++++++++++------- pybootchartgui/parsing.py | 10 +++++----- pybootchartgui/samples.py | 7 ++++--- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 4e7aeb4..8419a1a 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -70,6 +70,8 @@ def proc_tree (self, trace): # Disk throughput color. DISK_TPUT_COLOR = (0.20, 0.71, 0.20, 1.0) +# Disk throughput color. +DISK_WRITE_COLOR = (0.7, 0.0, 0.7, 1.0) # CPU load chart color. FILE_OPEN_COLOR = (0.20, 0.71, 0.71, 1.0) # Mem cached color @@ -356,29 +358,45 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w): curr_y = curr_y + 30 + bar_h # render second chart - draw_legend_line(ctx, "Disk throughput", DISK_TPUT_COLOR, off_x, curr_y+20, leg_s) - draw_legend_box(ctx, "Disk utilization", IO_COLOR, off_x + 120, curr_y+20, leg_s) + draw_legend_box(ctx, "Disk utilization -- fraction of sample interval I/O queue was not empty", + IO_COLOR, off_x, curr_y+20, leg_s) + draw_legend_line(ctx, "Disk writes -- bytes per sample", + DISK_WRITE_COLOR, off_x + 500, curr_y+20, leg_s) + draw_legend_line(ctx, "Disk reads+writes -- bytes per sample", + DISK_TPUT_COLOR, off_x + 500 + 120 * 2, curr_y+20, leg_s) curr_y += 5 + # render disk throughput + max_sample = None + # render I/O utilization for partition in trace.disk_stats: draw_text(ctx, partition.name, TEXT_COLOR, off_x, curr_y+30) + # utilization -- inherently normalized [0,1] chart_rect = (off_x, curr_y+30+5, w, bar_h) if clip_visible (clip, chart_rect): draw_box_ticks (ctx, chart_rect, sec_w) draw_annotations (ctx, proc_tree, trace.times, chart_rect) draw_chart (ctx, IO_COLOR, True, chart_rect, \ [(sample.time, sample.util) for sample in partition.samples], \ - proc_tree, None) + proc_tree, [0, 1]) # render disk throughput - max_sample = max (partition.samples, key = lambda s: s.tput) + # XXX assume single block device, for now + if not max_sample: + # XXX correction for non-constant sample.time? + max_sample = max (partition.samples, key = lambda s: s.tput) if clip_visible (clip, chart_rect): draw_chart (ctx, DISK_TPUT_COLOR, False, chart_rect, [(sample.time, sample.tput) for sample in partition.samples], \ - proc_tree, None) + proc_tree, [0, max_sample.tput]) + + # overlay write throughput + draw_chart (ctx, DISK_WRITE_COLOR, False, chart_rect, + [(sample.time, sample.write) for sample in partition.samples], \ + proc_tree, [0, max_sample.tput]) pos_x = off_x + ((max_sample.time - proc_tree.start_time) * w / proc_tree.duration) @@ -386,8 +404,9 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w): if (pos_x < off_x + 245): shift_x, shift_y = 5, 40 - label = "%dMB/s" % round ((max_sample.tput) / 1024.0) - draw_text (ctx, label, DISK_TPUT_COLOR, pos_x + shift_x, curr_y + shift_y) + # DISK_BLOCK_SIZE = 1024 + # label = "%.1fMB/s" % round ((max_sample.tput) / DISK_BLOCK_SIZE) + # draw_text (ctx, label, DISK_TPUT_COLOR, pos_x + shift_x, curr_y + shift_y) curr_y = curr_y + 30 + bar_h diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index b5205c7..8f043ed 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -419,7 +419,7 @@ def _parse_proc_disk_stat_log(file, options, numCpu): Parse file for disk stats, summing over all physical storage devices, eg. sda, sdb. Also parse stats for individual devices or partitions indicated on the command line. The format of relevant lines should be: - {major minor name rio rmerge rsect ruse wio wmerge wsect wuse running use aveq} + {major minor name rio rmerge rsect ruse wio wmerge wsect wuse running io_ticks aveq} The file is generated by block/genhd.c FIXME: for Flash devices, rio/wio may have more usefulness than rsect/wsect. """ @@ -456,8 +456,8 @@ def is_relevant_line(linetokens, regex): relevant_tokens = get_relevant_tokens( lines, disk_regex_re) for tokens in relevant_tokens: - disk, rsect, wsect, use = tokens[2], int(tokens[5]), int(tokens[9]), int(tokens[12]) - sample.add_diskdata([rsect, wsect, use]) + disk, rsect, wsect, io_ticks = tokens[2], int(tokens[5]), int(tokens[9]), int(tokens[12]) + sample.add_diskdata([rsect, wsect, io_ticks]) disk_stat_samples.append(sample) @@ -477,8 +477,8 @@ def is_relevant_line(linetokens, regex): sample = DiskStatSample(time) relevant_tokens = get_relevant_tokens( lines, this_partition_regex_re) token = relevant_tokens[0] - disk_name, rsect, wsect, use = token[2], int(token[5]), int(token[9]), int(token[12]) - sample.add_diskdata([rsect, wsect, use]) + disk_name, rsect, wsect, io_ticks = token[2], int(token[5]), int(token[9]), int(token[12]) + sample.add_diskdata([rsect, wsect, io_ticks]) disk_stat_samples.append(sample) if options.partition_labels: diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index cf1f153..1bab713 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -134,12 +134,13 @@ def set_parent(self, processMap): def get_end_time(self): return self.start_time + self.duration +# To understand 'io_ticks', see the kernel's part_round_stats_single() and part_round_stats() class DiskSample: - def __init__(self, time, read, write, util): + def __init__(self, time, read, write, io_ticks): self.time = time - self.read = read # a delta relative to the preceding time + self.read = read # sectors, a delta relative to the preceding time self.write = write # ~ - self.util = util # ~ + self.util = io_ticks # a delta, units of msec self.tput = read + write class DiskSamples: From f01afeb375252a9356d1de6347dce0d213f69200 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:56 -0800 Subject: [PATCH 029/182] process -- touch up spacing and CPU colors --- pybootchartgui/draw.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 8419a1a..4ea9f6a 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -62,7 +62,7 @@ def proc_tree (self, trace): LEGEND_FONT_SIZE = 12 # CPU load chart color. -CPU_COLOR = (0.40, 0.55, 0.70, 1.0) +CPU_COLOR = (0.60, 0.65, 0.75, 1.0) # IO wait chart color. IO_COLOR = (0.76, 0.48, 0.48, 0.5) PROCS_RUNNING_COLOR = (0.0, 1.0, 0.0, 1.0) @@ -72,8 +72,6 @@ def proc_tree (self, trace): DISK_TPUT_COLOR = (0.20, 0.71, 0.20, 1.0) # Disk throughput color. DISK_WRITE_COLOR = (0.7, 0.0, 0.7, 1.0) -# CPU load chart color. -FILE_OPEN_COLOR = (0.20, 0.71, 0.71, 1.0) # Mem cached color MEM_CACHED_COLOR = CPU_COLOR # Mem used color @@ -88,7 +86,7 @@ def proc_tree (self, trace): # Waiting process color. PROC_COLOR_D = (0.76, 0.48, 0.48, 0.5) # Running process color. -PROC_COLOR_R = CPU_COLOR +PROC_COLOR_R = (0.20, 0.50, 0.70, 1.0) # Sleeping process color. PROC_COLOR_S = (0.94, 0.94, 0.94, 1.0) # Stopped process color. @@ -355,15 +353,15 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w): [(sample.time, sample.procs_blocked) for sample in trace.cpu_stats], \ proc_tree, [0, 9], True) - curr_y = curr_y + 30 + bar_h + curr_y = curr_y + 50 + bar_h # render second chart draw_legend_box(ctx, "Disk utilization -- fraction of sample interval I/O queue was not empty", IO_COLOR, off_x, curr_y+20, leg_s) draw_legend_line(ctx, "Disk writes -- bytes per sample", - DISK_WRITE_COLOR, off_x + 500, curr_y+20, leg_s) + DISK_WRITE_COLOR, off_x+470, curr_y+20, leg_s) draw_legend_line(ctx, "Disk reads+writes -- bytes per sample", - DISK_TPUT_COLOR, off_x + 500 + 120 * 2, curr_y+20, leg_s) + DISK_TPUT_COLOR, off_x+470+120*2, curr_y+20, leg_s) curr_y += 5 From 818f5e16ac2b8a46f6b6d27721c19b4ec65eec47 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:56 -0800 Subject: [PATCH 030/182] tweak colors --- pybootchartgui/draw.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 4ea9f6a..eeff536 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -86,9 +86,9 @@ def proc_tree (self, trace): # Waiting process color. PROC_COLOR_D = (0.76, 0.48, 0.48, 0.5) # Running process color. -PROC_COLOR_R = (0.20, 0.50, 0.70, 1.0) +PROC_COLOR_R = CPU_COLOR # (0.40, 0.50, 0.80, 1.0) # should look similar to CPU_COLOR # Sleeping process color. -PROC_COLOR_S = (0.94, 0.94, 0.94, 1.0) +PROC_COLOR_S = (0.95, 0.95, 0.95, 1.0) # Stopped process color. PROC_COLOR_T = (0.94, 0.50, 0.50, 1.0) # Zombie process color. From e6ab7d6a44c9d1df646dde03d9910f12e87a3408 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:56 -0800 Subject: [PATCH 031/182] process -- draw CPU color for even sleeping processes --- pybootchartgui/draw.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index eeff536..c55fe60 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -615,12 +615,10 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, cl state = get_proc_state( sample.state ) color = STATE_COLORS[state] - if state == STATE_RUNNING: + if state == STATE_RUNNING or state == STATE_SLEEPING: alpha = min (sample.cpu_sample.user + sample.cpu_sample.sys, 1.0) color = tuple(list(PROC_COLOR_R[0:3]) + [alpha]) # print "render time %d [ tx %d tw %d ], sample state %s color %s alpha %g" % (sample.time, tx, tw, state, color, alpha) - elif state == STATE_SLEEPING: - continue draw_fill_rect(ctx, color, (tx, y, tw, proc_h)) From 44806e0d673f683cf8c3e245eabf828c290baab9 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:57 -0800 Subject: [PATCH 032/182] process -- split per process bars --- pybootchartgui/draw.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index c55fe60..95b3fd7 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -84,7 +84,7 @@ def proc_tree (self, trace): # Process border color. PROC_BORDER_COLOR = (0.71, 0.71, 0.71, 1.0) # Waiting process color. -PROC_COLOR_D = (0.76, 0.48, 0.48, 0.5) +PROC_COLOR_D = (0.76, 0.45, 0.35, 1.0) # Running process color. PROC_COLOR_R = CPU_COLOR # (0.40, 0.50, 0.80, 1.0) # should look similar to CPU_COLOR # Sleeping process color. @@ -130,7 +130,7 @@ def proc_tree (self, trace): STATE_STOPPED = 4 STATE_ZOMBIE = 5 -STATE_COLORS = [(0, 0, 0, 0), PROC_COLOR_R, PROC_COLOR_S, PROC_COLOR_D, \ +STATE_COLORS = [(0, 0, 0, 0), PROC_COLOR_S, PROC_COLOR_S, PROC_COLOR_D, \ PROC_COLOR_T, PROC_COLOR_Z, PROC_COLOR_X, PROC_COLOR_W] JUSTIFY_LEFT = "left" @@ -614,13 +614,13 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, cl last_tx = tx + tw state = get_proc_state( sample.state ) - color = STATE_COLORS[state] - if state == STATE_RUNNING or state == STATE_SLEEPING: - alpha = min (sample.cpu_sample.user + sample.cpu_sample.sys, 1.0) - color = tuple(list(PROC_COLOR_R[0:3]) + [alpha]) -# print "render time %d [ tx %d tw %d ], sample state %s color %s alpha %g" % (sample.time, tx, tw, state, color, alpha) + alpha = min (sample.cpu_sample.user + sample.cpu_sample.sys, 1.0) + cpu_color = tuple(list(PROC_COLOR_R[0:3]) + [alpha]) +# print "render time %d [ tx %d tw %d ], sample state %s color %s alpha %g" % (sample.time, tx, tw, state, color, alpha) + draw_fill_rect(ctx, cpu_color, (tx, y, tw, proc_h * 7 / 8)) - draw_fill_rect(ctx, color, (tx, y, tw, proc_h)) + color = STATE_COLORS[state] + draw_fill_rect(ctx, color, (tx, y + proc_h * 7 / 8, tw, proc_h / 8)) def draw_process_connecting_lines(ctx, px, py, x, y, proc_h): ctx.set_source_rgba(*DEP_COLOR) From f8d43d04a02b7ae80ff94fa57c6d77761c40ce31 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Fri, 7 Dec 2012 16:23:28 -0800 Subject: [PATCH 033/182] IO charts refactor --- pybootchartgui/parsing.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 8f043ed..79a4f25 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -440,12 +440,17 @@ def delta_disk_samples(disk_stat_samples, numCpu): return disk_stats def get_relevant_tokens(lines, regex): - def is_relevant_line(linetokens, regex): - if len(linetokens) != 14: - return False - return regex.match(linetokens[2]) - return [linetokens for linetokens in map (lambda x: x.split(),lines) - if is_relevant_line(linetokens, regex)] + return [ + linetokens + for linetokens in map (lambda x: x.split(),lines) + if len(linetokens) == 14 and regex.match(linetokens[2]) + ] + + def add_tokens_to_sample(sample, tokens): + disk_name, rsect, wsect, io_ticks = tokens[2], int(tokens[5]), int(tokens[9]), int(tokens[12]) + + sample.add_diskdata([rsect, wsect, io_ticks]) + return disk_name # matched not against whole line, but field only disk_regex_re = re.compile('^([hsv]d.|mtdblock\d|mmcblk\d|cciss/c\d+d\d+.*)$') @@ -453,11 +458,10 @@ def is_relevant_line(linetokens, regex): disk_stat_samples = [] for time, lines in _parse_timed_blocks(file): sample = DiskStatSample(time) - relevant_tokens = get_relevant_tokens( lines, disk_regex_re) + relevant_tokens = get_relevant_tokens(lines, disk_regex_re) for tokens in relevant_tokens: - disk, rsect, wsect, io_ticks = tokens[2], int(tokens[5]), int(tokens[9]), int(tokens[12]) - sample.add_diskdata([rsect, wsect, io_ticks]) + add_tokens_to_sample(sample,tokens) disk_stat_samples.append(sample) @@ -470,15 +474,14 @@ def is_relevant_line(linetokens, regex): file.seek(0) disk_stat_samples = [] this_partition_regex_re = re.compile('^' + part + '.*$') + disk_name = '' # for every timed_block disk_stat_samples = [] for time, lines in _parse_timed_blocks(file): sample = DiskStatSample(time) - relevant_tokens = get_relevant_tokens( lines, this_partition_regex_re) - token = relevant_tokens[0] - disk_name, rsect, wsect, io_ticks = token[2], int(token[5]), int(token[9]), int(token[12]) - sample.add_diskdata([rsect, wsect, io_ticks]) + relevant_tokens = get_relevant_tokens(lines, this_partition_regex_re) + disk_name = add_tokens_to_sample(sample,relevant_tokens[0]) # [0] assumes 'part' matched only a single line disk_stat_samples.append(sample) if options.partition_labels: From 48b725ae4f76dd8d386688ae137d1c518a4b1032 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:57 -0800 Subject: [PATCH 034/182] IO charts -- tolerate mismatched partition names -- hack -- needs usage message --- pybootchartgui/parsing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 79a4f25..14cfd1e 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -481,7 +481,8 @@ def add_tokens_to_sample(sample, tokens): for time, lines in _parse_timed_blocks(file): sample = DiskStatSample(time) relevant_tokens = get_relevant_tokens(lines, this_partition_regex_re) - disk_name = add_tokens_to_sample(sample,relevant_tokens[0]) # [0] assumes 'part' matched only a single line + if relevant_tokens: # XX should exit with usage message + disk_name = add_tokens_to_sample(sample,relevant_tokens[0]) # [0] assumes 'part' matched at most a single line disk_stat_samples.append(sample) if options.partition_labels: From 8844f2aacb5db1bd6f6a71a60f878fbe95e42334 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 12 Dec 2012 11:52:41 -0800 Subject: [PATCH 035/182] IO charts -- add option to graph io_ops not byte --- pybootchartgui/draw.py | 8 ++++++-- pybootchartgui/main.py.in | 6 +++++- pybootchartgui/parsing.py | 33 +++++++++++++++++++++------------ 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 95b3fd7..543f00b 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -358,9 +358,13 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w): # render second chart draw_legend_box(ctx, "Disk utilization -- fraction of sample interval I/O queue was not empty", IO_COLOR, off_x, curr_y+20, leg_s) - draw_legend_line(ctx, "Disk writes -- bytes per sample", + if OPTIONS.show_ops_not_bytes: + unit = "ops" + else: + unit = "bytes" + draw_legend_line(ctx, "Disk writes -- " + unit + "/sample", DISK_WRITE_COLOR, off_x+470, curr_y+20, leg_s) - draw_legend_line(ctx, "Disk reads+writes -- bytes per sample", + draw_legend_line(ctx, "Disk reads+writes -- " + unit + "/sample", DISK_TPUT_COLOR, off_x+470+120*2, curr_y+20, leg_s) curr_y += 5 diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index c30367f..a5e0416 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -62,10 +62,14 @@ def _mk_options_parser(): help="suppress informational messages") parser.add_option("--verbose", action="store_true", dest="verbose", default=False, help="print all messages") + # disk stats parser.add_option("-p", "--show-partitions", action="extend", dest="partitions", type="string", default=[], - help="draw a disk stat chart for any block device partitions in this list") + help="draw a disk stat chart for any block device partitions in this comma-separated list") parser.add_option("-P", "--relabel-partitions", action="extend", dest="partition_labels", default=[], help="list of per-partition strings, to be drawn instead of the raw per-partition device names") + parser.add_option("--show-ops-not-bytes", action="store_true", dest="show_ops_not_bytes", default=False, + help="chart number of I/O operations handed to driver, rather than bytes transferred per sample") + parser.add_option("--show-pid", action="store_true", dest="show_pid", default=False, help="show process ids in the bootchart as 'processname [pid]'") parser.add_option("-j", "--justify", dest="justify", default="left", choices=["left", "center"], diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 14cfd1e..824875a 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -426,15 +426,21 @@ def _parse_proc_disk_stat_log(file, options, numCpu): def delta_disk_samples(disk_stat_samples, numCpu): disk_stats = [] - for sample1, sample2 in zip(disk_stat_samples[:-1], disk_stat_samples[1:]): - interval = sample1.time - sample2.time - if interval == 0: - print("time between samples is 0!") - interval = 1 - sums = [ a - b for a, b in zip(sample1.diskdata, sample2.diskdata) ] - readTput = sums[0] / 2.0 * 100.0 / interval # XXX why divide by 2.0 ? - writeTput = sums[1] / 2.0 * 100.0 / interval - util = float( sums[2] ) / 10 / interval / numCpu + + # Very short intervals amplify round-off under division by time delta, so coalesce now. + # XX scaling issue for high-efficiency collector! + disk_stat_samples_coalesced = [(disk_stat_samples[0])] + for sample in disk_stat_samples: + if sample.time - disk_stat_samples_coalesced[-1].time < 5: + continue + disk_stat_samples_coalesced.append(sample) + + for sample1, sample2 in zip(disk_stat_samples_coalesced[:-1], disk_stat_samples_coalesced[1:]): + interval = sample2.time - sample1.time + vector_diff = [ a - b for a, b in zip(sample2.diskdata, sample1.diskdata) ] + readTput = float( vector_diff[0]) / interval + writeTput = float( vector_diff[1]) / interval + util = float( vector_diff[2]) / 10 / interval / numCpu util = max(0.0, min(1.0, util)) disk_stats.append(DiskSample(sample2.time, readTput, writeTput, util)) return disk_stats @@ -447,9 +453,12 @@ def get_relevant_tokens(lines, regex): ] def add_tokens_to_sample(sample, tokens): - disk_name, rsect, wsect, io_ticks = tokens[2], int(tokens[5]), int(tokens[9]), int(tokens[12]) - - sample.add_diskdata([rsect, wsect, io_ticks]) + if options.show_ops_not_bytes: + disk_name, rop, wop, io_ticks = tokens[2], int(tokens[3]), int(tokens[7]), int(tokens[12]) + sample.add_diskdata([rop, wop, io_ticks]) + else: + disk_name, rsect, wsect, io_ticks = tokens[2], int(tokens[3]), int(tokens[7]), int(tokens[12]) + sample.add_diskdata([rsect, wsect, io_ticks]) return disk_name # matched not against whole line, but field only From f4ef20135959182aee39b006f18c0451f23b586d Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:58 -0800 Subject: [PATCH 036/182] IO charts -- dont hide disk util overshoots --- pybootchartgui/parsing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 824875a..9867640 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -441,7 +441,6 @@ def delta_disk_samples(disk_stat_samples, numCpu): readTput = float( vector_diff[0]) / interval writeTput = float( vector_diff[1]) / interval util = float( vector_diff[2]) / 10 / interval / numCpu - util = max(0.0, min(1.0, util)) disk_stats.append(DiskSample(sample2.time, readTput, writeTput, util)) return disk_stats From 9a45de075750cebb0a57bb40e59684c10bf258fe Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 12 Dec 2012 12:05:32 -0800 Subject: [PATCH 037/182] parsing -- events -- log --- pybootchartgui/parsing.py | 31 +++++++++++++++++++++++++++++++ pybootchartgui/process_tree.py | 2 ++ pybootchartgui/samples.py | 10 ++++++++++ 3 files changed, 43 insertions(+) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 9867640..0ab648c 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -43,6 +43,7 @@ def __init__(self, writer, paths, options): self.parent_map = None self.mem_stats = None + # Read in all files, parse each into a time-ordered list parse_paths (writer, self, paths, options) if not self.valid(): raise ParseError("empty state: '%s' does not contain a valid bootchart" % ", ".join(paths)) @@ -604,6 +605,34 @@ def _parse_dmesg(writer, file): return processMap.values() +# +# Input resembles dmesg. Eventual output is per-process lists of events in temporal order. +# +def _parse_events_log(writer, file): + ''' + Parse a generic log format produced by target-specific filters, which + resembles output of `dmesg`, except that the timestamp must be followed + by tid, pid, and a string common to related log messages, otherwise unformatted. + Extracting {timestamp_microseconds_from_boot, tid, pid, group_string, raw_line} from + the target-specific system logs is the responsibility of a target-specific script. + ''' + split_re = re.compile ("^(\S+) (\S+) (\S+) (\S+) (\S+) (\S+)$") + timed_blocks = _parse_timed_blocks(file) + samples = [] + for time, lines in timed_blocks: + for line in lines: + if line is '': + continue + m = split_re.match(line) + time_usec = m.group(1) + pid = m.group(2) + tid = m.group(3) + match = m.group(4) + raw_file = m.group(5) + raw_line_number = m.group(6) + samples.append( EventSample(time, time_usec, tid, pid, match, raw_file, raw_line_number) ) + return samples + # # Parse binary pacct accounting file output if we have one # cf. /usr/include/linux/acct.h @@ -699,6 +728,8 @@ def _do_parse(writer, state, name, file, options): state.ps_stats = _parse_proc_ps_log(writer, file) elif name == "kernel_pacct": # obsoleted by PROC_EVENTS state.parent_map = _parse_pacct(writer, file) + elif name == "events-6.log": # 6 is number of fields -- a crude versioning scheme + state.events = _parse_events_log(writer, file) # writes to just-created process tree t2 = clock() writer.info(" %s seconds" % str(t2-t1)) return state diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index 233c904..0044715 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -64,6 +64,8 @@ def __init__(self, writer, kernel, psstats, sample_period, if for_testing: return + # XX Specific to bootchart2 collector; not executed for known /proc-sampling collectors. + # XX Test bootchart2 with this disabled. removed = self.merge_logger(self.process_tree, self.LOGGER_PROC, monitoredApp, False) writer.status("merged %i logger processes" % removed) diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 1bab713..5569aeb 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -14,6 +14,16 @@ # along with pybootchartgui. If not, see . +class EventSample: + def __init__(self, time, time_usec, tid, pid, match, raw_file, raw_line_number): + self.time = time + self.time_usec = time_usec + self.tid = tid + self.pid = pid + self.match = match + self.raw_file = raw_file + self.raw_line_number = raw_line_number + class DiskStatSample: def __init__(self, time): self.time = time From 2985669eee26db5b0c262439e92d03b88cb19ecc Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:58 -0800 Subject: [PATCH 038/182] parsing -- events log -- tolerate extra space --- pybootchartgui/parsing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 0ab648c..ea2ab05 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -616,7 +616,7 @@ def _parse_events_log(writer, file): Extracting {timestamp_microseconds_from_boot, tid, pid, group_string, raw_line} from the target-specific system logs is the responsibility of a target-specific script. ''' - split_re = re.compile ("^(\S+) (\S+) (\S+) (\S+) (\S+) (\S+)$") + split_re = re.compile ("^(\S+) +(\S+) +(\S+) +(\S+) +(\S+) +(\S+)$") timed_blocks = _parse_timed_blocks(file) samples = [] for time, lines in timed_blocks: From 603f4f6f837a360430eeb900b48ee4eb3524aa0f Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:58 -0800 Subject: [PATCH 039/182] events -- adorn_process_map --- pybootchartgui/parsing.py | 17 ++++++++++++++--- pybootchartgui/samples.py | 3 ++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index ea2ab05..38f188c 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -36,6 +36,7 @@ def __init__(self, writer, paths, options): self.ps_stats = None self.taskstats = None self.cpu_stats = None + self.events = None self.cmdline = None self.kernel = None self.kernel_tree = None @@ -50,7 +51,7 @@ def __init__(self, writer, paths, options): # Turn that parsed information into something more useful # link processes into a tree of pointers, calculate statistics - self.compile(writer) + self.adorn_process_map(writer) # Crop the chart to the end of the first idle period after the given # process @@ -86,8 +87,7 @@ def valid(self): return self.headers != None and self.disk_stats != None and \ self.ps_stats != None and self.cpu_stats != None - - def compile(self, writer): + def adorn_process_map(self, writer): def find_parent_id_for(pid): if pid is 0: @@ -118,6 +118,17 @@ def find_parent_id_for(pid): # else: # print "proc %d '%s' not in cmdline" % (rpid, proc.exe) + # merge in events + if self.events is not None: + for ev in self.events: + key = int(ev.pid) * 1000 + if key in self.ps_stats.process_map: + proc = self.ps_stats.process_map[key] + if len(proc.samples) < 1 or ev.time < proc.samples[-1].time: + proc.events.append(ev) + else: + writer.warn("event for proc '%s' lost " % ev.pid) + # re-parent any stray orphans if we can if self.parent_map is not None: for process in self.ps_stats.process_map.values(): diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 5569aeb..65e28e3 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -61,7 +61,7 @@ class ProcessSample: def __init__(self, time, state, cpu_sample): self.time = time self.state = state - self.cpu_sample = cpu_sample + self.cpu_sample = cpu_sample # tuple def __str__(self): return str(self.time) + "\t" + str(self.state) + "\t" + str(self.cpu_sample) @@ -87,6 +87,7 @@ def __init__(self, writer, pid, cmd, ppid, start_time): self.start_time = start_time self.duration = 0 self.samples = [] + self.events = [] # time-ordered list of EventSample self.parent = None self.child_list = [] From 1faf7f65d8ef56f0e1265f4fe3945cecdb63f1ab Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:58 -0800 Subject: [PATCH 040/182] events -- fix pruning --- pybootchartgui/parsing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 38f188c..af215dd 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -124,10 +124,10 @@ def find_parent_id_for(pid): key = int(ev.pid) * 1000 if key in self.ps_stats.process_map: proc = self.ps_stats.process_map[key] - if len(proc.samples) < 1 or ev.time < proc.samples[-1].time: + if ev.time < self.ps_stats.end_time: proc.events.append(ev) else: - writer.warn("event for proc '%s' lost " % ev.pid) + writer.warn("event for [%d:%d] lost at time %d" % (ev.pid, ev.tid, ev.time)) # re-parent any stray orphans if we can if self.parent_map is not None: From 01f39eca7a3fe938d87b8e9024e151e2948ace47 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:59 -0800 Subject: [PATCH 041/182] clipping -- eliminate legacy user coord clipping --- pybootchartgui/draw.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 543f00b..94d0707 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -313,11 +313,7 @@ def extents(options, xscale, trace): return (w, h) def clip_visible(clip, rect): - xmax = max (clip[0], rect[0]) - ymax = max (clip[1], rect[1]) - xmin = min (clip[0] + clip[2], rect[0] + rect[2]) - ymin = min (clip[1] + clip[3], rect[1] + rect[3]) - return (xmin > xmax and ymin > ymax) + return True def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w): proc_tree = options.proc_tree(trace) @@ -583,8 +579,6 @@ def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) : next_y = y + proc_h for child in proc.child_list: - if next_y > clip[1] + clip[3]: - break child_x, child_y = draw_processes_recursively(ctx, child, proc_tree, next_y, proc_h, rect, clip) draw_process_connecting_lines(ctx, x, y, child_x, child_y, proc_h) next_y = next_y + proc_h * proc_tree.num_nodes([child]) @@ -593,10 +587,6 @@ def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) : def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip): - - if y > clip[1] + clip[3] or y + proc_h + 2 < clip[1]: - return - draw_fill_rect(ctx, PROC_COLOR_S, (x, y, w, proc_h)) last_tx = -1 @@ -604,11 +594,6 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, cl tx = rect[0] + round(((sample.time - proc_tree.start_time) * rect[2] / proc_tree.duration)) # samples are sorted chronologically - if tx < clip[0]: - continue - if tx > clip[0] + clip[2]: - break - tw = round(proc_tree.sample_period * rect[2] / float(proc_tree.duration)) if last_tx != -1 and abs(last_tx - tx) <= tw: tw -= last_tx - tx From 56d2fe80538c50a4cfea6b0384107e615c196b71 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:59 -0800 Subject: [PATCH 042/182] events -- draw ticks --- pybootchartgui/draw.py | 46 ++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 94d0707..a1a8418 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -103,6 +103,9 @@ def proc_tree (self, trace): # Process label font. PROC_TEXT_FONT_SIZE = 12 +# Event tick color. +EVENT_COLOR = (0.0, 0.0, 0.0, 1.0) + # Signature color. SIG_COLOR = (0.0, 0.0, 0.0, 0.3125) # Signature font. @@ -556,12 +559,24 @@ def draw_header (ctx, headers, duration): return header_y +def get_sample_width(proc_tree, rect, tx, last_tx): + tw = round(proc_tree.sample_period * rect[2] / float(proc_tree.duration)) + if last_tx != -1 and abs(last_tx - tx) <= tw: + tw -= last_tx - tx + tx = last_tx + tw = max (tw, 1) # nice to see at least something XX + return tw, tx + tw + def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) : x = rect[0] + ((proc.start_time - proc_tree.start_time) * rect[2] / proc_tree.duration) w = ((proc.duration) * rect[2] / proc_tree.duration) draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip) draw_rect(ctx, PROC_BORDER_COLOR, (x, y, w, proc_h)) + draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip) + + draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect) + ipid = int(proc.pid) if proc_tree.taskstats and OPTIONS.show_all: cmdString = '' @@ -593,23 +608,32 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, cl for sample in proc.samples : tx = rect[0] + round(((sample.time - proc_tree.start_time) * rect[2] / proc_tree.duration)) - # samples are sorted chronologically - tw = round(proc_tree.sample_period * rect[2] / float(proc_tree.duration)) - if last_tx != -1 and abs(last_tx - tx) <= tw: - tw -= last_tx - tx - tx = last_tx - tw = max (tw, 1) # nice to see at least something - - last_tx = tx + tw - state = get_proc_state( sample.state ) + tw, last_tx = get_sample_width(proc_tree, rect, tx, last_tx) alpha = min (sample.cpu_sample.user + sample.cpu_sample.sys, 1.0) cpu_color = tuple(list(PROC_COLOR_R[0:3]) + [alpha]) # print "render time %d [ tx %d tw %d ], sample state %s color %s alpha %g" % (sample.time, tx, tw, state, color, alpha) draw_fill_rect(ctx, cpu_color, (tx, y, tw, proc_h * 7 / 8)) +def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): + y += proc_h # move to bottom of process bar + ctx.set_source_rgba(*EVENT_COLOR) + for ev in proc.events: + tx = rect[0] + round(((ev.time - proc_tree.start_time) * rect[2] / proc_tree.duration)) + ctx.move_to(tx-1, y) + ctx.line_to(tx, y-5) + ctx.line_to(tx+1, y) + ctx.line_to(tx, y) + ctx.fill() - color = STATE_COLORS[state] - draw_fill_rect(ctx, color, (tx, y + proc_h * 7 / 8, tw, proc_h / 8)) +def draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip): + last_tx = -1 + for sample in proc.samples : + tx = rect[0] + round(((sample.time - proc_tree.start_time) * rect[2] / proc_tree.duration)) + state = get_proc_state( sample.state ) + if state == STATE_WAITING: + color = STATE_COLORS[state] + tw, last_tx = get_sample_width(proc_tree, rect, tx, last_tx) + draw_fill_rect(ctx, color, (tx, y + proc_h * 7 / 8, tw, proc_h / 8)) def draw_process_connecting_lines(ctx, px, py, x, y, proc_h): ctx.set_source_rgba(*DEP_COLOR) From 2b4d3e0bd79832fdf421e01ae5605974b2e14c20 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:59 -0800 Subject: [PATCH 043/182] events -- filter by regex --- pybootchartgui/draw.py | 3 +++ pybootchartgui/main.py.in | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index a1a8418..c485576 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -615,9 +615,12 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, cl # print "render time %d [ tx %d tw %d ], sample state %s color %s alpha %g" % (sample.time, tx, tw, state, color, alpha) draw_fill_rect(ctx, cpu_color, (tx, y, tw, proc_h * 7 / 8)) def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): + ev_regex = re.compile(OPTIONS.event_regex) y += proc_h # move to bottom of process bar ctx.set_source_rgba(*EVENT_COLOR) for ev in proc.events: + if not ev_regex.match(ev.match): + continue tx = rect[0] + round(((ev.time - proc_tree.start_time) * rect[2] / proc_tree.duration)) ctx.move_to(tx-1, y) ctx.line_to(tx, y-5) diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index a5e0416..d9c1343 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -75,6 +75,10 @@ def _mk_options_parser(): parser.add_option("-j", "--justify", dest="justify", default="left", choices=["left", "center"], help="relocate the text within process bars (left, center)") + # event plotting + parser.add_option("-e", "--events", dest="event_regex", metavar="REGEX", default=".*", + help="plot only events matching REGEX") + pg_Bootchart2 = optparse.OptionGroup(parser,"Bootchart2-specific", "Options effective only for logs coming from the Bootchart2 binary-format collector") pg_Bootchart2.add_option("--show-all", action="store_true", dest="show_all", default=False, From 979934e37dbe7fa38ecd552d33090b1a05f8f9b6 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:59 -0800 Subject: [PATCH 044/182] events -- add option -- synthesize sample start events --- pybootchartgui/draw.py | 2 +- pybootchartgui/main.py.in | 3 +++ pybootchartgui/parsing.py | 13 +++++++++++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index c485576..4e0fd95 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -619,7 +619,7 @@ def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): y += proc_h # move to bottom of process bar ctx.set_source_rgba(*EVENT_COLOR) for ev in proc.events: - if not ev_regex.match(ev.match): + if not ev_regex.match(ev.match) and ev.match != "sample_start": continue tx = rect[0] + round(((ev.time - proc_tree.start_time) * rect[2] / proc_tree.duration)) ctx.move_to(tx-1, y) diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index d9c1343..0b37ca1 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -78,6 +78,9 @@ def _mk_options_parser(): # event plotting parser.add_option("-e", "--events", dest="event_regex", metavar="REGEX", default=".*", help="plot only events matching REGEX") + parser.add_option("--synthesize-sample-start-events", action="store_true", dest="synthesize_sample_start_events", + help="synthesize an event marking each boundary between sample periods -- " + + "helpful in analyzing collector timing issues") pg_Bootchart2 = optparse.OptionGroup(parser,"Bootchart2-specific", "Options effective only for logs coming from the Bootchart2 binary-format collector") diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index af215dd..782e594 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -51,7 +51,7 @@ def __init__(self, writer, paths, options): # Turn that parsed information into something more useful # link processes into a tree of pointers, calculate statistics - self.adorn_process_map(writer) + self.adorn_process_map(writer, options) # Crop the chart to the end of the first idle period after the given # process @@ -87,7 +87,7 @@ def valid(self): return self.headers != None and self.disk_stats != None and \ self.ps_stats != None and self.cpu_stats != None - def adorn_process_map(self, writer): + def adorn_process_map(self, writer, options): def find_parent_id_for(pid): if pid is 0: @@ -118,6 +118,15 @@ def find_parent_id_for(pid): # else: # print "proc %d '%s' not in cmdline" % (rpid, proc.exe) + if options.synthesize_sample_start_events: + init_pid = 1 + key = init_pid * 1000 + proc = self.ps_stats.process_map[key] + for cpu in self.cpu_stats: + # assign to the init process's bar, for lack of any better + ev = EventSample(cpu.time, cpu.time*10*1000, init_pid, init_pid, "sample_start", "no raw file", -1) + proc.events.append(ev) + # merge in events if self.events is not None: for ev in self.events: From 1f7ad8a44656a96082694f8a10781868102ba9d3 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:50:59 -0800 Subject: [PATCH 045/182] events -- checkbox to disable event display --- pybootchartgui/draw.py | 5 ++++- pybootchartgui/gui.py | 11 ++++++++++- pybootchartgui/main.py.in | 2 ++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 4e0fd95..1cf3388 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -575,7 +575,10 @@ def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) : draw_rect(ctx, PROC_BORDER_COLOR, (x, y, w, proc_h)) draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip) - draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect) + # Event ticks step on the rectangle painted by draw_process_state_colors() (e.g. for non-interruptible wait); + # user can work around this by toggling off the event ticks. + if not OPTIONS.hide_events: + draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect) ipid = int(proc.pid) if proc_tree.taskstats and OPTIONS.show_all: diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index c0cd108..a7d0192 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -123,6 +123,10 @@ def show_toggled(self, button): self.options.app_options.show_all = button.get_property ('active') self.queue_draw() + def hide_events(self, button): + self.options.app_options.hide_events = not button.get_property ('active') + self.queue_draw() + POS_INCREMENT = 100 def on_key_press_event(self, widget, event): @@ -310,7 +314,12 @@ def __init__(self, window, trace, options, xscale): # Misc. options button = gtk.CheckButton("Show more") button.connect ('toggled', self.widget.show_toggled) - hbox.pack_start (button, False, True) + hbox.pack_start (button, False) + + button = gtk.CheckButton("Events") + button.connect ('toggled', self.widget.hide_events) + button.set_active (True) + hbox.pack_start (button, False) self.pack_start(hbox, False) self.pack_start(scrolled) diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 0b37ca1..614512f 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -78,6 +78,8 @@ def _mk_options_parser(): # event plotting parser.add_option("-e", "--events", dest="event_regex", metavar="REGEX", default=".*", help="plot only events matching REGEX") + parser.add_option("--hide-events", action="store_true", dest="hide_events", default=False, + help="hide event ticks (small black triangles)") parser.add_option("--synthesize-sample-start-events", action="store_true", dest="synthesize_sample_start_events", help="synthesize an event marking each boundary between sample periods -- " + "helpful in analyzing collector timing issues") From 3e5facd2d0f69fce58f4872d4da7f68b30975be0 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:00 -0800 Subject: [PATCH 046/182] FIX -- better button label --- pybootchartgui/gui.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index a7d0192..66d0530 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -119,7 +119,7 @@ def on_zoom_100(self, action): self.zoom_image(1.0) self.set_xscale(1.0) - def show_toggled(self, button): + def show_thread_details(self, button): self.options.app_options.show_all = button.get_property ('active') self.queue_draw() @@ -312,8 +312,8 @@ def __init__(self, window, trace, options, xscale): if not options.kernel_only: # Misc. options - button = gtk.CheckButton("Show more") - button.connect ('toggled', self.widget.show_toggled) + button = gtk.CheckButton("thread details") + button.connect ('toggled', self.widget.show_thread_details) hbox.pack_start (button, False) button = gtk.CheckButton("Events") From 7f8a1691e319540aee0fc3b54a753bdb30589baa Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:00 -0800 Subject: [PATCH 047/182] events -- fix help string --- pybootchartgui/main.py.in | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 614512f..6db36df 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -77,7 +77,11 @@ def _mk_options_parser(): # event plotting parser.add_option("-e", "--events", dest="event_regex", metavar="REGEX", default=".*", - help="plot only events matching REGEX") + help="Plot only events matching REGEX." + + " The regular expression is anchored to beginning and end of line," + + "and syntax is similar to grep 'extended' REs." + + " So to match FOO or BAR anywhere, use '.*(FOO|BAR).*'." + + " (file:///usr/share/doc/python2.6/html/library/re.htm)") parser.add_option("--hide-events", action="store_true", dest="hide_events", default=False, help="hide event ticks (small black triangles)") parser.add_option("--synthesize-sample-start-events", action="store_true", dest="synthesize_sample_start_events", From 65b66dbf9f2975a4e2752272162b95dcb45536d0 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:00 -0800 Subject: [PATCH 048/182] events -- draw event times --- pybootchartgui/draw.py | 29 +++++++++++++++++++---------- pybootchartgui/gui.py | 9 +++++++++ pybootchartgui/main.py.in | 2 ++ pybootchartgui/parsing.py | 1 + 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 1cf3388..2c59ece 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -178,10 +178,15 @@ def draw_label_in_box(ctx, color, label, x, y, w, maxx): label_x = x else: label_x = x + w / 2 - label_w / 2 # CENTER - if label_w + 10 > w: - label_x = x + w + 5 - if label_x + label_w > maxx: - label_x = x - label_w - 5 + + if label_w + 10 > w: # if wider than the process box + label_x = x + w + 5 # push outside to right + if label_x + label_w > maxx: # if that's too far right + label_x = x - label_w - 5 # push outside to the left + draw_text(ctx, label, color, label_x, y) + +def draw_label_in_box_at_time(ctx, color, label, x, y, label_x): + label_w = ctx.text_extents(label)[2] draw_text(ctx, label, color, label_x, y) def draw_sec_labels(ctx, rect, sec_w, nsecs): @@ -447,6 +452,8 @@ def render(ctx, options, xscale, trace): (w, h) = extents (options, xscale, trace) global OPTIONS OPTIONS = options.app_options + global HZ + HZ = trace.HZ proc_tree = options.proc_tree (trace) @@ -619,17 +626,19 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, cl draw_fill_rect(ctx, cpu_color, (tx, y, tw, proc_h * 7 / 8)) def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): ev_regex = re.compile(OPTIONS.event_regex) - y += proc_h # move to bottom of process bar ctx.set_source_rgba(*EVENT_COLOR) for ev in proc.events: if not ev_regex.match(ev.match) and ev.match != "sample_start": continue tx = rect[0] + round(((ev.time - proc_tree.start_time) * rect[2] / proc_tree.duration)) - ctx.move_to(tx-1, y) - ctx.line_to(tx, y-5) - ctx.line_to(tx+1, y) - ctx.line_to(tx, y) + ctx.move_to(tx-1, y+proc_h) + ctx.line_to(tx, y+proc_h-5) + ctx.line_to(tx+1, y+proc_h) + ctx.line_to(tx, y+proc_h) ctx.fill() + if OPTIONS.print_event_times: + draw_label_in_box_at_time(ctx, PROC_TEXT_COLOR, '%.2f' % (float(ev.time) / HZ), \ + x, y + proc_h - 4, tx) def draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip): last_tx = -1 @@ -686,7 +695,7 @@ def get_color(self): self.color = (c[0], c[1], c[2], 1.0) return self.color - +# taskstats-specific def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, sec_w, stat_type): global palette_idx palette_idx = 0 diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index 66d0530..aed5b47 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -127,6 +127,10 @@ def hide_events(self, button): self.options.app_options.hide_events = not button.get_property ('active') self.queue_draw() + def print_event_times(self, button): + self.options.app_options.print_event_times = button.get_property ('active') + self.queue_draw() + POS_INCREMENT = 100 def on_key_press_event(self, widget, event): @@ -321,6 +325,11 @@ def __init__(self, window, trace, options, xscale): button.set_active (True) hbox.pack_start (button, False) + button = gtk.CheckButton("event Time Labels") + button.connect ('toggled', self.widget.print_event_times) + button.set_active (options.app_options.print_event_times) + hbox.pack_start (button, False) + self.pack_start(hbox, False) self.pack_start(scrolled) self.show_all() diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 6db36df..439c596 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -84,6 +84,8 @@ def _mk_options_parser(): " (file:///usr/share/doc/python2.6/html/library/re.htm)") parser.add_option("--hide-events", action="store_true", dest="hide_events", default=False, help="hide event ticks (small black triangles)") + parser.add_option("--print-event-times", action="store_true", dest="print_event_times", default=False, + help="print time of each event, inside the box of the reporting process") parser.add_option("--synthesize-sample-start-events", action="store_true", dest="synthesize_sample_start_events", help="synthesize an event marking each boundary between sample periods -- " + "helpful in analyzing collector timing issues") diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 782e594..90c5099 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -31,6 +31,7 @@ class Trace: def __init__(self, writer, paths, options): + self.HZ = 100 # XX a smarter collector would collect this self.headers = None self.disk_stats = None self.ps_stats = None From 7a59f27f272e6d8249a64b7c159b7fb51b291fca Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:00 -0800 Subject: [PATCH 049/182] FIX dont highlight button upon click --- pybootchartgui/gui.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index aed5b47..068b5d2 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -314,18 +314,23 @@ def __init__(self, window, trace, options, xscale): toolbar = uimanager.get_widget('/ToolBar') hbox.pack_start(toolbar, True, True) + def gtk_CheckButton(name): + button = gtk.CheckButton(name) + button.set_focus_on_click(False) + return button + if not options.kernel_only: # Misc. options - button = gtk.CheckButton("thread details") + button = gtk_CheckButton("thread details") button.connect ('toggled', self.widget.show_thread_details) hbox.pack_start (button, False) - button = gtk.CheckButton("Events") + button = gtk_CheckButton("Events") button.connect ('toggled', self.widget.hide_events) button.set_active (True) hbox.pack_start (button, False) - button = gtk.CheckButton("event Time Labels") + button = gtk_CheckButton("event Time Labels") button.connect ('toggled', self.widget.print_event_times) button.set_active (options.app_options.print_event_times) hbox.pack_start (button, False) From e2beca1831b137bda7890a848722b04fba4a9018 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:01 -0800 Subject: [PATCH 050/182] refactor -- sec_w args -- to -- SEC_W global --- pybootchartgui/draw.py | 50 +++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 2c59ece..5a18075 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -189,25 +189,25 @@ def draw_label_in_box_at_time(ctx, color, label, x, y, label_x): label_w = ctx.text_extents(label)[2] draw_text(ctx, label, color, label_x, y) -def draw_sec_labels(ctx, rect, sec_w, nsecs): +def draw_sec_labels(ctx, rect, nsecs): ctx.set_font_size(AXIS_FONT_SIZE) prev_x = 0 - for i in range(0, rect[2] + 1, sec_w): - if ((i / sec_w) % nsecs == 0) : - label = "%ds" % (i / sec_w) + for i in range(0, rect[2] + 1, SEC_W): + if ((i / SEC_W) % nsecs == 0) : + label = "%ds" % (i / SEC_W) label_w = ctx.text_extents(label)[2] x = rect[0] + i - label_w/2 if x >= prev_x: draw_text(ctx, label, TEXT_COLOR, x, rect[1] - 2) prev_x = x + label_w -def draw_box_ticks(ctx, rect, sec_w): +def draw_box_ticks(ctx, rect): draw_rect(ctx, BORDER_COLOR, tuple(rect)) ctx.set_line_cap(cairo.LINE_CAP_SQUARE) - for i in range(sec_w, rect[2] + 1, sec_w): - if ((i / sec_w) % 5 == 0) : + for i in range(SEC_W, rect[2] + 1, SEC_W): + if ((i / SEC_W) % 5 == 0) : ctx.set_source_rgba(*TICK_COLOR_BOLD) else : ctx.set_source_rgba(*TICK_COLOR) @@ -310,9 +310,14 @@ def transform_point_coords(point, x_base, y_base, \ CUML_HEIGHT = 2000 # Increased value to accomodate CPU and I/O Graphs OPTIONS = None +SEC_W = None + def extents(options, xscale, trace): + global SEC_W + SEC_W = int (xscale * sec_w_base) + proc_tree = options.proc_tree(trace) - w = int (proc_tree.duration * sec_w_base * xscale / 100) + 2*off_x + w = int (proc_tree.duration * SEC_W / 100) + 2*off_x h = proc_h * proc_tree.num_proc + 2 * off_y if options.charts: h += 110 + (2 + len(trace.disk_stats)) * (30 + bar_h) + 1 * (30 + meminfo_bar_h) @@ -323,7 +328,7 @@ def extents(options, xscale, trace): def clip_visible(clip, rect): return True -def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w): +def render_charts(ctx, options, clip, trace, curr_y, w, h): proc_tree = options.proc_tree(trace) # render bar legend @@ -339,7 +344,7 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w): # render I/O wait chart_rect = (off_x, curr_y+30, w, bar_h) if clip_visible (clip, chart_rect): - draw_box_ticks (ctx, chart_rect, sec_w) + draw_box_ticks (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) draw_chart (ctx, IO_COLOR, True, chart_rect, \ [(sample.time, sample.user + sample.sys + sample.io) for sample in trace.cpu_stats], \ @@ -383,7 +388,7 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w): # utilization -- inherently normalized [0,1] chart_rect = (off_x, curr_y+30+5, w, bar_h) if clip_visible (clip, chart_rect): - draw_box_ticks (ctx, chart_rect, sec_w) + draw_box_ticks (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) draw_chart (ctx, IO_COLOR, True, chart_rect, \ [(sample.time, sample.util) for sample in partition.samples], \ @@ -426,7 +431,7 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w): draw_legend_box(ctx, "Buffers", MEM_BUFFERS_COLOR, off_x + 360, curr_y+20, leg_s) draw_legend_line(ctx, "Swap (scale: %u MiB)" % max([(sample.records['SwapTotal'] - sample.records['SwapFree'])/1024 for sample in mem_stats]), \ MEM_SWAP_COLOR, off_x + 480, curr_y+20, leg_s) - draw_box_ticks(ctx, chart_rect, sec_w) + draw_box_ticks(ctx, chart_rect) draw_annotations(ctx, proc_tree, trace.times, chart_rect) draw_chart(ctx, MEM_BUFFERS_COLOR, True, chart_rect, \ [(sample.time, sample.records['MemTotal'] - sample.records['MemFree']) for sample in trace.mem_stats], \ @@ -460,7 +465,6 @@ def render(ctx, options, xscale, trace): # x, y, w, h clip = ctx.clip_extents() - sec_w = int (xscale * sec_w_base) ctx.set_line_width(1.0) ctx.select_font_face(FONT_NAME) draw_fill_rect(ctx, WHITE, (0, 0, max(w, MIN_IMG_W), h)) @@ -477,7 +481,7 @@ def render(ctx, options, xscale, trace): curr_y = off_y; if options.charts: - curr_y = render_charts (ctx, options, clip, trace, curr_y, w, h, sec_w) + curr_y = render_charts (ctx, options, clip, trace, curr_y, w, h) # draw process boxes proc_height = h @@ -485,7 +489,7 @@ def render(ctx, options, xscale, trace): proc_height -= CUML_HEIGHT draw_process_bar_chart(ctx, clip, options, proc_tree, trace.times, - curr_y, w, proc_height, sec_w) + curr_y, w, proc_height) curr_y = proc_height ctx.set_font_size(SIG_FONT_SIZE) @@ -495,15 +499,15 @@ def render(ctx, options, xscale, trace): if proc_tree.taskstats and options.cumulative: cuml_rect = (off_x, curr_y + off_y, w, CUML_HEIGHT/2 - off_y * 2) if clip_visible (clip, cuml_rect): - draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, sec_w, STAT_TYPE_CPU) + draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_CPU) # draw a cumulative I/O-time-per-process graph if proc_tree.taskstats and options.cumulative: cuml_rect = (off_x, curr_y + off_y * 100, w, CUML_HEIGHT/2 - off_y * 2) if clip_visible (clip, cuml_rect): - draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, sec_w, STAT_TYPE_IO) + draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_IO) -def draw_process_bar_chart(ctx, clip, options, proc_tree, times, curr_y, w, h, sec_w): +def draw_process_bar_chart(ctx, clip, options, proc_tree, times, curr_y, w, h): header_size = 0 if not options.kernel_only: draw_legend_box (ctx, "Running (%cpu)", @@ -520,12 +524,12 @@ def draw_process_bar_chart(ctx, clip, options, proc_tree, times, curr_y, w, h, s w, h - 2 * off_y - (curr_y + header_size + 15) + proc_h] ctx.set_font_size (PROC_TEXT_FONT_SIZE) - draw_box_ticks (ctx, chart_rect, sec_w) - if sec_w > 100: + draw_box_ticks (ctx, chart_rect) + if SEC_W > 100: nsec = 1 else: nsec = 5 - draw_sec_labels (ctx, chart_rect, sec_w, nsec) + draw_sec_labels (ctx, chart_rect, nsec) draw_annotations (ctx, proc_tree, times, chart_rect) y = curr_y + 60 @@ -696,7 +700,7 @@ def get_color(self): return self.color # taskstats-specific -def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, sec_w, stat_type): +def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, stat_type): global palette_idx palette_idx = 0 @@ -821,7 +825,7 @@ def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, sec_w, stat_type): below = row # render grid-lines over the top - draw_box_ticks(ctx, chart_bounds, sec_w) + draw_box_ticks(ctx, chart_bounds) # render labels for l in labels: From 32cea72b11bc6f68c4dec842cc155b55839f8ba4 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:01 -0800 Subject: [PATCH 051/182] add -- prehistory -- option -- XXX breaks taskstats --- pybootchartgui/draw.py | 91 ++++++++++++++++++++++++++------------- pybootchartgui/main.py.in | 2 + pybootchartgui/parsing.py | 15 ++++--- pybootchartgui/samples.py | 3 +- 4 files changed, 75 insertions(+), 36 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 5a18075..6437362 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -189,6 +189,9 @@ def draw_label_in_box_at_time(ctx, color, label, x, y, label_x): label_w = ctx.text_extents(label)[2] draw_text(ctx, label, color, label_x, y) +def time_in_hz_to_ideal_coord(t_hz): + return (t_hz-time_origin_drawn) * SEC_W / HZ + off_x + def draw_sec_labels(ctx, rect, nsecs): ctx.set_font_size(AXIS_FONT_SIZE) prev_x = 0 @@ -233,20 +236,20 @@ def draw_annotations(ctx, proc_tree, times, rect): ctx.set_line_cap(cairo.LINE_CAP_BUTT) ctx.set_dash([]) +# All charts assumed to be full-width def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree, data_range, square = True): if square and not fill: ctx.set_line_width(2.0) else: ctx.set_line_width(1.0) - x_shift = proc_tree.start_time - def transform_point_coords(point, x_base, y_base, \ + def transform_point_coords(point, y_base, \ xscale, yscale, x_trans, y_trans): - x = (point[0] - x_base) * xscale + x_trans + x = time_in_hz_to_ideal_coord(point[0]) y = (point[1] - y_base) * -yscale + y_trans + chart_bounds[3] return x, y - max_x = max (x for (x, y) in data) + max_x = proc_tree.end_time # units of HZ max_y = max (y for (x, y) in data) # avoid divide by zero if max_y == 0: @@ -262,9 +265,9 @@ def transform_point_coords(point, x_base, y_base, \ yscale = float(chart_bounds[3]) / max_y ybase = 0 - first = transform_point_coords (data[0], x_shift, ybase, xscale, yscale, \ + last = transform_point_coords (data[-1], ybase, xscale, yscale, \ chart_bounds[0], chart_bounds[1]) - last = transform_point_coords (data[-1], x_shift, ybase, xscale, yscale, \ + first = transform_point_coords (data[0], ybase, xscale, yscale, \ chart_bounds[0], chart_bounds[1]) ctx.set_source_rgba(*color) @@ -272,7 +275,7 @@ def transform_point_coords(point, x_base, y_base, \ prev_y = first[1] prev_point = data[0] for point in data: - x, y = transform_point_coords (point, x_shift, ybase, xscale, yscale, \ + x, y = transform_point_coords (point, ybase, xscale, yscale, \ chart_bounds[0], chart_bounds[1]) if square: if fill: @@ -299,6 +302,8 @@ def transform_point_coords(point, x_base, y_base, \ ctx.stroke() ctx.set_line_width(1.0) +# Constants +# XX put all of constants in a named tuple, for immutability bar_h = 55 meminfo_bar_h = 2 * bar_h # offsets @@ -311,19 +316,36 @@ def transform_point_coords(point, x_base, y_base, \ OPTIONS = None SEC_W = None +HZ = None +time_origin_drawn = None # time of leftmost plotted data + +w_extents = None # includes off_x * 2 +h_extents = None # includes off_y * (2 + N) +# Called from gui.py and batch.py, before first call to render(), +# and every time xscale changes. def extents(options, xscale, trace): + global OPTIONS, HZ, time_origin_drawn + OPTIONS = options.app_options + HZ = trace.HZ + if OPTIONS.prehistory: + time_origin_drawn = 0 # XX Would have to be process_tree.starttime for backwards compatibility + else: + time_origin_drawn = trace.ps_stats.start_time global SEC_W SEC_W = int (xscale * sec_w_base) proc_tree = options.proc_tree(trace) - w = int (proc_tree.duration * SEC_W / 100) + 2*off_x + w = int ((proc_tree.duration-time_origin_drawn) * SEC_W / HZ) + 2*off_x h = proc_h * proc_tree.num_proc + 2 * off_y if options.charts: h += 110 + (2 + len(trace.disk_stats)) * (30 + bar_h) + 1 * (30 + meminfo_bar_h) if proc_tree.taskstats and options.cumulative: h += CUML_HEIGHT + 4 * off_y - return (w, h) + global w_extents, h_extents + w_extents = w + h_extents = h + return (w, h) # includes off_x, off_y def clip_visible(clip, rect): return True @@ -455,20 +477,22 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): # def render(ctx, options, xscale, trace): (w, h) = extents (options, xscale, trace) - global OPTIONS - OPTIONS = options.app_options - global HZ - HZ = trace.HZ proc_tree = options.proc_tree (trace) # x, y, w, h - clip = ctx.clip_extents() + clip = ctx.clip_extents() # XX Bounds are initialized, yet clipping is not enforced by pyCairo! ??? ctx.set_line_width(1.0) ctx.select_font_face(FONT_NAME) draw_fill_rect(ctx, WHITE, (0, 0, max(w, MIN_IMG_W), h)) + w -= 2*off_x + + ctx.new_path() + ctx.rectangle(off_x, 0, w, h) + ctx.clip() + # draw the title and headers if proc_tree.idle: duration = proc_tree.idle @@ -579,8 +603,10 @@ def get_sample_width(proc_tree, rect, tx, last_tx): return tw, tx + tw def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) : - x = rect[0] + ((proc.start_time - proc_tree.start_time) * rect[2] / proc_tree.duration) - w = ((proc.duration) * rect[2] / proc_tree.duration) + #x = rect[0] + ((proc.start_time - proc_tree.start_time) * rect[2] / proc_tree.duration) + x = time_in_hz_to_ideal_coord(proc.start_time) + #w = ((proc.duration) * rect[2] / proc_tree.duration) + w = time_in_hz_to_ideal_coord(proc.start_time + proc.duration) - x draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip) draw_rect(ctx, PROC_BORDER_COLOR, (x, y, w, proc_h)) @@ -614,40 +640,45 @@ def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) : return x, y - def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip): draw_fill_rect(ctx, PROC_COLOR_S, (x, y, w, proc_h)) + if len(proc.samples) <= 0: + return + # cases: + # 1. proc started before sampling did + # XX should look up time of previous sample, not assume 'proc_tree.sample_period' + # 2. proc start after sampling + last_time = max(proc.start_time, proc.samples[0].time - proc_tree.sample_period) + last_tx = time_in_hz_to_ideal_coord(last_time) + for sample in proc.samples[1:] : + tx = time_in_hz_to_ideal_coord(sample.time) + alpha = min(sample.cpu_sample.user + sample.cpu_sample.sys, 1.0) # XX rationale? + cpu_color = tuple(list(PROC_COLOR_R[0:3]) + [alpha]) + # XXX correct color for non-uniform sample intervals + draw_fill_rect(ctx, cpu_color, (last_tx, y, tx - last_tx, proc_h * 7 / 8)) + last_tx = tx - last_tx = -1 - for sample in proc.samples : - tx = rect[0] + round(((sample.time - proc_tree.start_time) * rect[2] / proc_tree.duration)) - - tw, last_tx = get_sample_width(proc_tree, rect, tx, last_tx) - - alpha = min (sample.cpu_sample.user + sample.cpu_sample.sys, 1.0) - cpu_color = tuple(list(PROC_COLOR_R[0:3]) + [alpha]) -# print "render time %d [ tx %d tw %d ], sample state %s color %s alpha %g" % (sample.time, tx, tw, state, color, alpha) - draw_fill_rect(ctx, cpu_color, (tx, y, tw, proc_h * 7 / 8)) def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): ev_regex = re.compile(OPTIONS.event_regex) ctx.set_source_rgba(*EVENT_COLOR) for ev in proc.events: if not ev_regex.match(ev.match) and ev.match != "sample_start": continue - tx = rect[0] + round(((ev.time - proc_tree.start_time) * rect[2] / proc_tree.duration)) + tx = time_in_hz_to_ideal_coord(ev.time) ctx.move_to(tx-1, y+proc_h) ctx.line_to(tx, y+proc_h-5) ctx.line_to(tx+1, y+proc_h) ctx.line_to(tx, y+proc_h) ctx.fill() if OPTIONS.print_event_times: - draw_label_in_box_at_time(ctx, PROC_TEXT_COLOR, '%.2f' % (float(ev.time) / HZ), \ + draw_label_in_box_at_time(ctx, PROC_TEXT_COLOR, + '%.2f' % (float(ev.time - time_origin_drawn) / HZ), x, y + proc_h - 4, tx) def draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip): last_tx = -1 for sample in proc.samples : - tx = rect[0] + round(((sample.time - proc_tree.start_time) * rect[2] / proc_tree.duration)) + tx = time_in_hz_to_ideal_coord(sample.time) state = get_proc_state( sample.state ) if state == STATE_WAITING: color = STATE_COLORS[state] diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 439c596..00b2b6a 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -99,6 +99,8 @@ def _mk_options_parser(): pg_Scripting = optparse.OptionGroup(parser,"Scripting support", "Options most useful in scripted processing of tgz batches") + pg_Scripting.add_option("--prehistory", action="store_true", dest="prehistory", default=False, + help="extend process bars to the recorded start time of each, even if before any samples were collected") pg_Scripting.add_option("-t", "--boot-time", action="store_true", dest="boottime", default=False, help="only display the boot time of the boot in text format (stdout)") pg_Scripting.add_option("-f", "--format", dest="format", default="png", choices=["png", "svg", "pdf"], diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 90c5099..e44eaaa 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -268,7 +268,7 @@ def parse(block): blocks = file.read().split('\n\n') return [parse(block) for block in blocks if block.strip() and not block.endswith(' not running\n')] -def _parse_proc_ps_log(writer, file): +def _parse_proc_ps_log(options, writer, file): """ * See proc(5) for details. * @@ -288,7 +288,7 @@ def _parse_proc_ps_log(writer, file): offset = [index for index, token in enumerate(tokens[1:]) if token[-1] == ')'][0] pid, cmd, state, ppid = int(tokens[0]), ' '.join(tokens[1:2+offset]), tokens[2+offset], int(tokens[3+offset]) - userCpu, sysCpu, stime = int(tokens[13+offset]), int(tokens[14+offset]), int(tokens[21+offset]) + userCpu, sysCpu, starttime = int(tokens[13+offset]), int(tokens[14+offset]), int(tokens[21+offset]) # magic fixed point-ness ... pid *= 1000 @@ -297,7 +297,12 @@ def _parse_proc_ps_log(writer, file): process = processMap[pid] process.cmd = cmd.strip('()') # why rename after latest name?? else: - process = Process(writer, pid, cmd.strip('()'), ppid, min(time, stime)) + if time < starttime: + # large values signify a collector problem, e.g. resource starvation + writer.status("time (%d) < starttime (%d), diff %d -- PID %d" % + (time, starttime, time-starttime, pid/1000)) + + process = Process(writer, pid, cmd.strip('()'), ppid, starttime) processMap[pid] = process if process.last_user_cpu_time is not None and process.last_sys_cpu_time is not None: @@ -332,7 +337,7 @@ def _parse_taskstats_log(writer, file): ltime = None timed_blocks = _parse_timed_blocks(file) for time, lines in timed_blocks: - # we have no 'stime' from taskstats, so prep 'init' + # we have no 'starttime' from taskstats, so prep 'init' if ltime is None: process = Process(writer, 1, '[init]', 0, 0) processMap[1000] = process @@ -746,7 +751,7 @@ def _do_parse(writer, state, name, file, options): elif name == "paternity.log": state.parent_map = _parse_paternity_log(writer, file) elif name == "proc_ps.log": # obsoleted by TASKSTATS - state.ps_stats = _parse_proc_ps_log(writer, file) + state.ps_stats = _parse_proc_ps_log(options, writer, file) elif name == "kernel_pacct": # obsoleted by PROC_EVENTS state.parent_map = _parse_pacct(writer, file) elif name == "events-6.log": # 6 is number of fields -- a crude versioning scheme diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 65e28e3..41c785f 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -67,11 +67,12 @@ def __str__(self): return str(self.time) + "\t" + str(self.state) + "\t" + str(self.cpu_sample) class ProcessStats: + """stats over the collection of all processes, all samples""" def __init__(self, writer, process_map, sample_count, sample_period, start_time, end_time): self.process_map = process_map self.sample_count = sample_count self.sample_period = sample_period - self.start_time = start_time + self.start_time = start_time # time at which the first sample was collected self.end_time = end_time writer.info ("%d samples, avg. sample length %f" % (self.sample_count, self.sample_period)) writer.info ("process list size: %d" % len (self.process_map.values())) From d506fe1aa7e39d31aa578b2d59a2fa82e87e4f0f Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:01 -0800 Subject: [PATCH 052/182] clipping -- relax clipping so process bars can project outward --- pybootchartgui/draw.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 6437362..408c1c5 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -489,10 +489,6 @@ def render(ctx, options, xscale, trace): w -= 2*off_x - ctx.new_path() - ctx.rectangle(off_x, 0, w, h) - ctx.clip() - # draw the title and headers if proc_tree.idle: duration = proc_tree.idle @@ -548,6 +544,11 @@ def draw_process_bar_chart(ctx, clip, options, proc_tree, times, curr_y, w, h): w, h - 2 * off_y - (curr_y + header_size + 15) + proc_h] ctx.set_font_size (PROC_TEXT_FONT_SIZE) + ctx.new_path() + ctx.rectangle(chart_rect[0]-off_x, 0, \ + chart_rect[2]+2*off_x, h_extents) + ctx.clip() + draw_box_ticks (ctx, chart_rect) if SEC_W > 100: nsec = 1 From 82cd03e7551c716d90880dc569d477fb782b2bd1 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:01 -0800 Subject: [PATCH 053/182] draw_label_in_box -- rework --- pybootchartgui/draw.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 408c1c5..9ed88ba 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -172,7 +172,8 @@ def draw_legend_line(ctx, label, fill_color, x, y, s): ctx.fill() draw_text(ctx, label, TEXT_COLOR, x + s + 5, y) -def draw_label_in_box(ctx, color, label, x, y, w, maxx): +# XX Needs to know about the transform, to make sure process name is visible +def draw_label_in_box(ctx, color, label, x, y, w, minx, maxx): label_w = ctx.text_extents(label)[2] if OPTIONS.justify == JUSTIFY_LEFT: label_x = x @@ -183,6 +184,8 @@ def draw_label_in_box(ctx, color, label, x, y, w, maxx): label_x = x + w + 5 # push outside to right if label_x + label_w > maxx: # if that's too far right label_x = x - label_w - 5 # push outside to the left + if label_x < minx: + label_x = minx draw_text(ctx, label, color, label_x, y) def draw_label_in_box_at_time(ctx, color, label, x, y, label_x): @@ -631,7 +634,8 @@ def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) : else: cmdString = cmdString - draw_label_in_box(ctx, PROC_TEXT_COLOR, cmdString, x, y + proc_h - 4, w, rect[0] + rect[2]) + draw_label_in_box(ctx, PROC_TEXT_COLOR, cmdString, x, y + proc_h - 4, w, + off_x, rect[0] + rect[2]) next_y = y + proc_h for child in proc.child_list: From 88479bd17ba19903e6e163fc4080f19da7e8b565 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:01 -0800 Subject: [PATCH 054/182] disable -- draw_sec_labels --- pybootchartgui/draw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 9ed88ba..f9f3b2a 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -557,7 +557,7 @@ def draw_process_bar_chart(ctx, clip, options, proc_tree, times, curr_y, w, h): nsec = 1 else: nsec = 5 - draw_sec_labels (ctx, chart_rect, nsec) + #draw_sec_labels (ctx, chart_rect, nsec) draw_annotations (ctx, proc_tree, times, chart_rect) y = curr_y + 60 From 5909db4394cd191865291d9d02804f7e802d34b0 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:02 -0800 Subject: [PATCH 055/182] heuristic tweak process duration stat upward by half of samplePeriod --- pybootchartgui/samples.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 41c785f..83cde9c 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -119,7 +119,9 @@ def calc_stats(self, samplePeriod): firstSample = self.samples[0] lastSample = self.samples[-1] self.start_time = min(firstSample.time, self.start_time) - self.duration = lastSample.time - self.start_time + samplePeriod + # self.duration is a heuristic: process may be expected to continue running at least + # one-half of a sample period beyond the instant at which lastSample was taken. + self.duration = lastSample.time - self.start_time + samplePeriod / 2 activeCount = sum( [1 for sample in self.samples if sample.cpu_sample and sample.cpu_sample.sys + sample.cpu_sample.user + sample.cpu_sample.io > 0.0] ) activeCount = activeCount + sum( [1 for sample in self.samples if sample.state == 'D'] ) From 64987919ba36c62f9223033242a09749c2b58f5c Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:02 -0800 Subject: [PATCH 056/182] ProcessTree -- refactor -- comments in process_tree.py --- pybootchartgui/process_tree.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index 0044715..0b53355 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -255,7 +255,7 @@ def merge_siblings(self, process_subtree): idx -= 1 num_removed += 1 p.child_list.extend(nextp.child_list) - self.merge_processes(p, nextp) + self.subsume_process(p, nextp) num_removed += self.merge_siblings(p.child_list) idx += 1 if len(process_subtree) > 0: @@ -275,16 +275,17 @@ def merge_runs(self, process_subtree): if len(p.child_list) == 1 and p.child_list[0].cmd == p.cmd: child = p.child_list[0] p.child_list = list(child.child_list) - self.merge_processes(p, child) + self.subsume_process(p, child) num_removed += 1 continue num_removed += self.merge_runs(p.child_list) idx += 1 return num_removed - def merge_processes(self, p1, p2): - """Merges two process' samples.""" - p1.samples.extend(p2.samples) + # XX return a new instance instead, so that start_time and end_time can be made immutable? + def subsume_process(self, p1, p2): + """Subsume process p2 into p1. Attributes of p2 other than samples[], start_time and end_time are lost.""" + p1.samples.extend(p2.samples) # result no longer necessarily in temporal order p1.samples.sort( key = lambda p: p.time ) p1time = p1.start_time p2time = p2.start_time From af67d90533f5cbb9deb8a4cfd421905941fee2e3 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:02 -0800 Subject: [PATCH 057/182] ProcessTree -- refactor -- proc_tree.duration becomes a method --- pybootchartgui/draw.py | 18 ++++++++---------- pybootchartgui/main.py.in | 2 +- pybootchartgui/process_tree.py | 5 +++-- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index f9f3b2a..4c40180 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -230,7 +230,7 @@ def draw_annotations(ctx, proc_tree, times, rect): for time in times: if time is not None: - x = ((time - proc_tree.start_time) * rect[2] / proc_tree.duration) + x = ((time - proc_tree.start_time) * rect[2] / proc_tree.duration()) ctx.move_to(rect[0] + x, rect[1] + 1) ctx.line_to(rect[0] + x, rect[1] + rect[3] - 1) @@ -339,7 +339,7 @@ def extents(options, xscale, trace): SEC_W = int (xscale * sec_w_base) proc_tree = options.proc_tree(trace) - w = int ((proc_tree.duration-time_origin_drawn) * SEC_W / HZ) + 2*off_x + w = int ((proc_tree.duration()-time_origin_drawn) * SEC_W / HZ) + 2*off_x h = proc_h * proc_tree.num_proc + 2 * off_y if options.charts: h += 110 + (2 + len(trace.disk_stats)) * (30 + bar_h) + 1 * (30 + meminfo_bar_h) @@ -434,7 +434,7 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): [(sample.time, sample.write) for sample in partition.samples], \ proc_tree, [0, max_sample.tput]) - pos_x = off_x + ((max_sample.time - proc_tree.start_time) * w / proc_tree.duration) + pos_x = off_x + ((max_sample.time - proc_tree.start_time) * w / proc_tree.duration()) shift_x, shift_y = -20, 20 if (pos_x < off_x + 245): @@ -496,7 +496,7 @@ def render(ctx, options, xscale, trace): if proc_tree.idle: duration = proc_tree.idle else: - duration = proc_tree.duration + duration = proc_tree.duration() if not options.kernel_only: curr_y = draw_header (ctx, trace.headers, duration) @@ -599,7 +599,7 @@ def draw_header (ctx, headers, duration): return header_y def get_sample_width(proc_tree, rect, tx, last_tx): - tw = round(proc_tree.sample_period * rect[2] / float(proc_tree.duration)) + tw = round(proc_tree.sample_period * rect[2] / float(proc_tree.duration())) if last_tx != -1 and abs(last_tx - tx) <= tw: tw -= last_tx - tx tx = last_tx @@ -607,9 +607,7 @@ def get_sample_width(proc_tree, rect, tx, last_tx): return tw, tx + tw def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) : - #x = rect[0] + ((proc.start_time - proc_tree.start_time) * rect[2] / proc_tree.duration) x = time_in_hz_to_ideal_coord(proc.start_time) - #w = ((proc.duration) * rect[2] / proc_tree.duration) w = time_in_hz_to_ideal_coord(proc.start_time + proc.duration) - x draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip) @@ -826,8 +824,8 @@ def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, stat_type): # draw the trailing rectangle from the last time to # before now, at the height of the last segment. if render_seg: - w = math.ceil ((time - last_time) * chart_bounds[2] / proc_tree.duration) + 1 - x = chart_bounds[0] + round((last_time - proc_tree.start_time) * chart_bounds[2] / proc_tree.duration) + w = math.ceil ((time - last_time) * chart_bounds[2] / proc_tree.duration()) + 1 + x = chart_bounds[0] + round((last_time - proc_tree.start_time) * chart_bounds[2] / proc_tree.duration()) ctx.rectangle (x, below[last_time] - last_cuml, w, last_cuml) ctx.fill() # ctx.stroke() @@ -837,7 +835,7 @@ def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, stat_type): row[time] = y # render the last segment - x = chart_bounds[0] + round((last_time - proc_tree.start_time) * chart_bounds[2] / proc_tree.duration) + x = chart_bounds[0] + round((last_time - proc_tree.start_time) * chart_bounds[2] / proc_tree.duration()) y = below[last_time] - cuml ctx.rectangle (x, y, chart_bounds[2] - x, cuml) ctx.fill() diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 00b2b6a..c503c4a 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -191,7 +191,7 @@ def main(argv=None): if proc_tree.idle: duration = proc_tree.idle else: - duration = proc_tree.duration + duration = proc_tree.duration() dur = duration / 100.0 print('%02d:%05.2f' % (math.floor(dur/60), dur - 60 * math.floor(dur/60))) else: diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index 0b53355..f472827 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -58,7 +58,6 @@ def __init__(self, writer, kernel, psstats, sample_period, self.start_time = self.get_start_time(self.process_tree) self.end_time = self.get_end_time(self.process_tree) - self.duration = self.end_time - self.start_time self.idle = idle if for_testing: @@ -86,10 +85,12 @@ def __init__(self, writer, kernel, psstats, sample_period, self.start_time = self.get_start_time(self.process_tree) self.end_time = self.get_end_time(self.process_tree) - self.duration = self.end_time - self.start_time self.num_proc = self.num_nodes(self.process_tree) + def duration(self): + return self.end_time - self.start_time + def build(self): """Build the process tree from the list of top samples.""" self.process_tree = [] From 94350fd21f65b43d78fbcb19404394f845e70285 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:02 -0800 Subject: [PATCH 058/182] events -- never prune a process that reports an event --- pybootchartgui/process_tree.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index f472827..35cb66a 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -170,7 +170,8 @@ def prune(self, process_subtree, parent): """ def is_idle_background_process_without_children(p): return not p.active and \ - self.num_nodes(p.child_list) == 0 + self.num_nodes(p.child_list) == 0 and \ + len(p.events) == 0 # never prune a process that reports an event num_removed = 0 idx = 0 From a04da1df312581b5a76f3242d3ad924d4fe562c2 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:02 -0800 Subject: [PATCH 059/182] charts -- draw.py -- implement scatter plots --- pybootchartgui/draw.py | 79 +++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 32 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 4c40180..d935aa4 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -161,6 +161,17 @@ def draw_rect(ctx, color, rect): ctx.rectangle(*rect) ctx.stroke() +def draw_diamond(ctx, x, y, w, h): + ctx.save() + ctx.set_line_width(0.0) + ctx.move_to(x-w/2, y) + ctx.line_to(x, y+h/2) + ctx.line_to(x+w/2, y) + ctx.line_to(x, y-h/2) + ctx.line_to(x-w/2, y) + ctx.fill() + ctx.restore() + def draw_legend_box(ctx, label, fill_color, x, y, s): draw_fill_rect(ctx, fill_color, (x, y - s, s, s)) draw_rect(ctx, PROC_BORDER_COLOR, (x, y - s, s, s)) @@ -239,13 +250,30 @@ def draw_annotations(ctx, proc_tree, times, rect): ctx.set_line_cap(cairo.LINE_CAP_BUTT) ctx.set_dash([]) -# All charts assumed to be full-width -def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree, data_range, square = True): - if square and not fill: - ctx.set_line_width(2.0) - else: - ctx.set_line_width(1.0) +def plot_line(ctx, point, x, y): + ctx.set_line_width(1.0) + ctx.line_to(x, y) # rightward, and upward or downward + +def plot_square(ctx, point, x, y): + ctx.set_line_width(1.0) + ctx.line_to(x, ctx.get_current_point()[1]) # rightward + ctx.line_to(x, y) # upward or downward +def plot_segment_positive(ctx, point, x, y): + ctx.move_to(ctx.get_current_point()[0], y) # upward or downward + if point[1] <= 0: + ctx.move_to(x, y) + return + ctx.set_line_width(1.5) + ctx.line_to(x, y) + +def plot_scatter_positive(ctx, point, x, y): + if point[1] <= 0: + return + draw_diamond(ctx, x, y, 3.6, 3.6) + +# All charts assumed to be full-width +def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree, data_range, plot_point_func): def transform_point_coords(point, y_base, \ xscale, yscale, x_trans, y_trans): x = time_in_hz_to_ideal_coord(point[0]) @@ -275,24 +303,11 @@ def transform_point_coords(point, y_base, \ ctx.set_source_rgba(*color) ctx.move_to(*first) - prev_y = first[1] - prev_point = data[0] + for point in data: x, y = transform_point_coords (point, ybase, xscale, yscale, \ chart_bounds[0], chart_bounds[1]) - if square: - if fill: - ctx.line_to(x, prev_y) # rightward - ctx.line_to(x, y) # upward or downward - else: - # draw horizontals only, and then only if non-zero -- result cannot be "filled" - if prev_point[1] > 0: - ctx.line_to(x, prev_y) # rightward - ctx.move_to(x, y) # upward or downward, maybe rightward - prev_point = point - prev_y = y - else: - ctx.line_to(x, y) + plot_point_func(ctx, point, x, y) if fill: ctx.set_line_width(0.0) @@ -373,19 +388,19 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): draw_annotations (ctx, proc_tree, trace.times, chart_rect) draw_chart (ctx, IO_COLOR, True, chart_rect, \ [(sample.time, sample.user + sample.sys + sample.io) for sample in trace.cpu_stats], \ - proc_tree, None, True) + proc_tree, None, plot_square) # render CPU load draw_chart (ctx, CPU_COLOR, True, chart_rect, \ [(sample.time, sample.user + sample.sys) for sample in trace.cpu_stats], \ - proc_tree, None) + proc_tree, None, plot_square) draw_chart (ctx, PROCS_RUNNING_COLOR, False, chart_rect, [(sample.time, sample.procs_running) for sample in trace.cpu_stats], \ - proc_tree, [0, 9], True) + proc_tree, [0, 9], plot_scatter_positive) draw_chart (ctx, PROCS_BLOCKED_COLOR, False, chart_rect, [(sample.time, sample.procs_blocked) for sample in trace.cpu_stats], \ - proc_tree, [0, 9], True) + proc_tree, [0, 9], plot_scatter_positive) curr_y = curr_y + 50 + bar_h @@ -417,7 +432,7 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): draw_annotations (ctx, proc_tree, trace.times, chart_rect) draw_chart (ctx, IO_COLOR, True, chart_rect, \ [(sample.time, sample.util) for sample in partition.samples], \ - proc_tree, [0, 1]) + proc_tree, [0, 1], plot_square) # render disk throughput # XXX assume single block device, for now @@ -427,12 +442,12 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): if clip_visible (clip, chart_rect): draw_chart (ctx, DISK_TPUT_COLOR, False, chart_rect, [(sample.time, sample.tput) for sample in partition.samples], \ - proc_tree, [0, max_sample.tput]) + proc_tree, [0, max_sample.tput], plot_segment_positive) # overlay write throughput draw_chart (ctx, DISK_WRITE_COLOR, False, chart_rect, [(sample.time, sample.write) for sample in partition.samples], \ - proc_tree, [0, max_sample.tput]) + proc_tree, [0, max_sample.tput], plot_segment_positive) pos_x = off_x + ((max_sample.time - proc_tree.start_time) * w / proc_tree.duration()) @@ -460,16 +475,16 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): draw_annotations(ctx, proc_tree, trace.times, chart_rect) draw_chart(ctx, MEM_BUFFERS_COLOR, True, chart_rect, \ [(sample.time, sample.records['MemTotal'] - sample.records['MemFree']) for sample in trace.mem_stats], \ - proc_tree, [0, mem_scale]) + proc_tree, [0, mem_scale], plot_square) draw_chart(ctx, MEM_USED_COLOR, True, chart_rect, \ [(sample.time, sample.records['MemTotal'] - sample.records['MemFree'] - sample.records['Buffers']) for sample in mem_stats], \ - proc_tree, [0, mem_scale]) + proc_tree, [0, mem_scale], plot_square) draw_chart(ctx, MEM_CACHED_COLOR, True, chart_rect, \ [(sample.time, sample.records['Cached']) for sample in mem_stats], \ - proc_tree, [0, mem_scale]) + proc_tree, [0, mem_scale], plot_square) draw_chart(ctx, MEM_SWAP_COLOR, False, chart_rect, \ [(sample.time, float(sample.records['SwapTotal'] - sample.records['SwapFree'])) for sample in mem_stats], \ - proc_tree, None) + proc_tree, None, plot_square) curr_y = curr_y + meminfo_bar_h From 820fd14910e14d59d440ebd7df0ca72ab231ba5f Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:03 -0800 Subject: [PATCH 060/182] process -- draw state as diamond --- pybootchartgui/draw.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index d935aa4..96f836e 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -84,7 +84,7 @@ def proc_tree (self, trace): # Process border color. PROC_BORDER_COLOR = (0.71, 0.71, 0.71, 1.0) # Waiting process color. -PROC_COLOR_D = (0.76, 0.45, 0.35, 1.0) +PROC_COLOR_D = PROCS_BLOCKED_COLOR # (0.76, 0.45, 0.35, 1.0) # Running process color. PROC_COLOR_R = CPU_COLOR # (0.40, 0.50, 0.80, 1.0) # should look similar to CPU_COLOR # Sleeping process color. @@ -129,7 +129,7 @@ def proc_tree (self, trace): STATE_UNDEFINED = 0 STATE_RUNNING = 1 STATE_SLEEPING = 2 -STATE_WAITING = 3 +STATE_WAITING = 3 # sole useful state info? STATE_STOPPED = 4 STATE_ZOMBIE = 5 @@ -613,14 +613,6 @@ def draw_header (ctx, headers, duration): return header_y -def get_sample_width(proc_tree, rect, tx, last_tx): - tw = round(proc_tree.sample_period * rect[2] / float(proc_tree.duration())) - if last_tx != -1 and abs(last_tx - tx) <= tw: - tw -= last_tx - tx - tx = last_tx - tw = max (tw, 1) # nice to see at least something XX - return tw, tx + tw - def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) : x = time_in_hz_to_ideal_coord(proc.start_time) w = time_in_hz_to_ideal_coord(proc.start_time + proc.duration) - x @@ -673,7 +665,7 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, cl alpha = min(sample.cpu_sample.user + sample.cpu_sample.sys, 1.0) # XX rationale? cpu_color = tuple(list(PROC_COLOR_R[0:3]) + [alpha]) # XXX correct color for non-uniform sample intervals - draw_fill_rect(ctx, cpu_color, (last_tx, y, tx - last_tx, proc_h * 7 / 8)) + draw_fill_rect(ctx, cpu_color, (last_tx, y, tx - last_tx, proc_h)) last_tx = tx def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): @@ -700,8 +692,8 @@ def draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip) state = get_proc_state( sample.state ) if state == STATE_WAITING: color = STATE_COLORS[state] - tw, last_tx = get_sample_width(proc_tree, rect, tx, last_tx) - draw_fill_rect(ctx, color, (tx, y + proc_h * 7 / 8, tw, proc_h / 8)) + ctx.set_source_rgba(*color) + draw_diamond(ctx, tx, y + proc_h/2, 2.5, proc_h) def draw_process_connecting_lines(ctx, px, py, x, y, proc_h): ctx.set_source_rgba(*DEP_COLOR) From bf3688b3c3057fbc134c34fe5b4309e3fa28db04 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:03 -0800 Subject: [PATCH 061/182] legend text tweak --- pybootchartgui/draw.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 96f836e..5a6d14d 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -172,14 +172,18 @@ def draw_diamond(ctx, x, y, w, h): ctx.fill() ctx.restore() +def draw_legend_diamond(ctx, label, fill_color, x, y, w, h): + ctx.set_source_rgba(*fill_color) + draw_diamond(ctx, x, y-h/2, w, h) + draw_text(ctx, label, TEXT_COLOR, x + w + 5, y) + def draw_legend_box(ctx, label, fill_color, x, y, s): draw_fill_rect(ctx, fill_color, (x, y - s, s, s)) - draw_rect(ctx, PROC_BORDER_COLOR, (x, y - s, s, s)) + #draw_rect(ctx, PROC_BORDER_COLOR, (x, y - s, s, s)) draw_text(ctx, label, TEXT_COLOR, x + s + 5, y) def draw_legend_line(ctx, label, fill_color, x, y, s): draw_fill_rect(ctx, fill_color, (x, y - s/2, s + 1, 3)) - ctx.arc(x + (s + 1)/2.0, y - (s - 3)/2.0, 2.5, 0, 2.0 * math.pi) ctx.fill() draw_text(ctx, label, TEXT_COLOR, x + s + 5, y) @@ -328,7 +332,7 @@ def transform_point_coords(point, y_base, \ off_x, off_y = 10, 10 sec_w_base = 50 # the width of a second proc_h = 16 # the height of a process -leg_s = 10 +leg_s = 11 MIN_IMG_W = 800 CUML_HEIGHT = 2000 # Increased value to accomodate CPU and I/O Graphs OPTIONS = None @@ -376,10 +380,10 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): draw_legend_box(ctx, "CPU (user+sys)", CPU_COLOR, off_x, curr_y+20, leg_s) draw_legend_box(ctx, "I/O (wait)", IO_COLOR, off_x + 120, curr_y+20, leg_s) - draw_legend_box(ctx, "Runnable threads", PROCS_RUNNING_COLOR, - off_x +120 +80, curr_y+20, leg_s) - draw_legend_box(ctx, "Blocked threads -- uninterruptible sleep (I/O)", PROCS_BLOCKED_COLOR, - off_x +120 +80 +140, curr_y+20, leg_s) + draw_legend_diamond(ctx, "Runnable threads", PROCS_RUNNING_COLOR, + off_x +120 +90, curr_y+20, leg_s, leg_s) + draw_legend_diamond(ctx, "Blocked threads -- Uninterruptible Syscall", PROCS_BLOCKED_COLOR, + off_x +120 +90 +140, curr_y+20, leg_s, leg_s) # render I/O wait chart_rect = (off_x, curr_y+30, w, bar_h) @@ -548,17 +552,17 @@ def render(ctx, options, xscale, trace): def draw_process_bar_chart(ctx, clip, options, proc_tree, times, curr_y, w, h): header_size = 0 if not options.kernel_only: + draw_legend_diamond (ctx, "Uninterruptible Syscall", + PROC_COLOR_D, off_x+10, curr_y + 45, leg_s*3/4, proc_h) draw_legend_box (ctx, "Running (%cpu)", - PROC_COLOR_R, off_x , curr_y + 45, leg_s) - draw_legend_box (ctx, "Unint.sleep (I/O)", - PROC_COLOR_D, off_x+120, curr_y + 45, leg_s) + PROC_COLOR_R, off_x+10+180, curr_y + 45, leg_s) draw_legend_box (ctx, "Sleeping", - PROC_COLOR_S, off_x+240, curr_y + 45, leg_s) + PROC_COLOR_S, off_x+10+180+130, curr_y + 45, leg_s) draw_legend_box (ctx, "Zombie", - PROC_COLOR_Z, off_x+360, curr_y + 45, leg_s) + PROC_COLOR_Z, off_x+10+180+130+90, curr_y + 45, leg_s) header_size = 45 - chart_rect = [off_x, curr_y + header_size + 15, + chart_rect = [off_x, curr_y + header_size + 30, w, h - 2 * off_y - (curr_y + header_size + 15) + proc_h] ctx.set_font_size (PROC_TEXT_FONT_SIZE) From 89311267079008d21a63d0c1b59287eda2df3303 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:03 -0800 Subject: [PATCH 062/182] gui -- isotemporal --- pybootchartgui/draw.py | 23 ++++++++++++++++++++++- pybootchartgui/gui.py | 27 +++++++++++++++++++++------ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 5a6d14d..21d19fc 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -210,6 +210,15 @@ def draw_label_in_box_at_time(ctx, color, label, x, y, label_x): def time_in_hz_to_ideal_coord(t_hz): return (t_hz-time_origin_drawn) * SEC_W / HZ + off_x +# Solve for t_hz: +# x = (t_hz-time_origin_drawn) * SEC_W / HZ + off_x +# +# x - off_x = (t_hz-time_origin_drawn) * SEC_W / HZ +# (x - off_x) * HZ / SEC_W = t_hz-time_origin_drawn +# +def user_to_time(x): + return (x - off_x) * HZ / SEC_W + time_origin_drawn + def draw_sec_labels(ctx, rect, nsecs): ctx.set_font_size(AXIS_FONT_SIZE) prev_x = 0 @@ -497,7 +506,7 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): # # Render the chart. # -def render(ctx, options, xscale, trace): +def render(ctx, options, xscale, trace, isotemporal_x = None): (w, h) = extents (options, xscale, trace) proc_tree = options.proc_tree (trace) @@ -549,6 +558,18 @@ def render(ctx, options, xscale, trace): if clip_visible (clip, cuml_rect): draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_IO) + if isotemporal_x: + draw_isotemporal(ctx, isotemporal_x) + +def draw_isotemporal(ctx, isotemporal_x): + ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + ctx.set_line_width(0.8) + ctx.set_dash([4, 2]) + isotemporal_x = time_in_hz_to_ideal_coord(isotemporal_x) + ctx.move_to(isotemporal_x, 0) + ctx.line_to(isotemporal_x, CUML_HEIGHT) + ctx.stroke() + def draw_process_bar_chart(ctx, clip, options, proc_tree, times, curr_y, w, h): header_size = 0 if not options.kernel_only: diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index 068b5d2..5124833 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -58,6 +58,8 @@ def __init__(self, trace, options, xscale): self.hadj_changed_signal_id = None self.vadj_changed_signal_id = None + self.isotemporal_x = None + def do_expose_event(self, event): cr = self.window.cairo_create() @@ -70,12 +72,15 @@ def do_expose_event(self, event): self.draw(cr, self.get_allocation()) return False + def cr_set_up_transform(self, cr): + cr.scale(self.zoom_ratio, self.zoom_ratio) + cr.translate(-self.x, -self.y) + def draw(self, cr, rect): cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) cr.paint() - cr.scale(self.zoom_ratio, self.zoom_ratio) - cr.translate(-self.x, -self.y) - draw.render(cr, self.options, self.xscale, self.trace) + self.cr_set_up_transform(cr) + draw.render(cr, self.options, self.xscale, self.trace, self.isotemporal_x) def position_changed(self): self.emit("position-changed", self.x, self.y) @@ -149,16 +154,26 @@ def on_key_press_event(self, widget, event): return True def on_area_button_press(self, area, event): - if event.button == 2 or event.button == 1: + if event.button == 1: area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR)) self.prevmousex = event.x self.prevmousey = event.y + if event.button == 2: + cr = self.window.cairo_create() + uy = None + self.cr_set_up_transform(cr) + self.isotemporal_x, uy = cr.device_to_user(event.x, 0) + self.isotemporal_x = draw.user_to_time(self.isotemporal_x) + self.queue_draw() + if event.button == 3: + self.isotemporal_x = None + self.queue_draw() if event.type not in (gtk.gdk.BUTTON_PRESS, gtk.gdk.BUTTON_RELEASE): return False return False def on_area_button_release(self, area, event): - if event.button == 2 or event.button == 1: + if event.button == 1: area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW)) self.prevmousex = None self.prevmousey = None @@ -183,7 +198,7 @@ def on_area_scroll_event(self, area, event): def on_area_motion_notify(self, area, event): state = event.state - if state & gtk.gdk.BUTTON2_MASK or state & gtk.gdk.BUTTON1_MASK: + if state & gtk.gdk.BUTTON1_MASK: x, y = int(event.x), int(event.y) if self.prevmousex==None or self.prevmousey==None: return True From e36c77a18e48472fa9dcad94706f582e82908ed0 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:03 -0800 Subject: [PATCH 063/182] draw -- rework -- replace off_x with matrix translate --- pybootchartgui/draw.py | 117 +++++++++++++++++++++-------------------- 1 file changed, 61 insertions(+), 56 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 21d19fc..c2eb632 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -208,7 +208,7 @@ def draw_label_in_box_at_time(ctx, color, label, x, y, label_x): draw_text(ctx, label, color, label_x, y) def time_in_hz_to_ideal_coord(t_hz): - return (t_hz-time_origin_drawn) * SEC_W / HZ + off_x + return (t_hz-time_origin_drawn) * SEC_W / HZ # Solve for t_hz: # x = (t_hz-time_origin_drawn) * SEC_W / HZ + off_x @@ -254,10 +254,10 @@ def draw_annotations(ctx, proc_tree, times, rect): for time in times: if time is not None: - x = ((time - proc_tree.start_time) * rect[2] / proc_tree.duration()) + x = time_in_hz_to_ideal_coord(time) - ctx.move_to(rect[0] + x, rect[1] + 1) - ctx.line_to(rect[0] + x, rect[1] + rect[3] - 1) + ctx.move_to(x, rect[1] + 1) + ctx.line_to(x, rect[1] + rect[3] - 1) ctx.stroke() ctx.set_line_cap(cairo.LINE_CAP_BUTT) @@ -350,32 +350,30 @@ def transform_point_coords(point, y_base, \ HZ = None time_origin_drawn = None # time of leftmost plotted data -w_extents = None # includes off_x * 2 -h_extents = None # includes off_y * (2 + N) +# window coords # Called from gui.py and batch.py, before first call to render(), # and every time xscale changes. +# Returns size of a window capable of holding the whole scene? def extents(options, xscale, trace): global OPTIONS, HZ, time_origin_drawn OPTIONS = options.app_options HZ = trace.HZ + + proc_tree = options.proc_tree(trace) if OPTIONS.prehistory: time_origin_drawn = 0 # XX Would have to be process_tree.starttime for backwards compatibility else: - time_origin_drawn = trace.ps_stats.start_time + time_origin_drawn = trace.ps_stats.start_time - proc_tree.sample_period global SEC_W SEC_W = int (xscale * sec_w_base) - proc_tree = options.proc_tree(trace) - w = int ((proc_tree.duration()-time_origin_drawn) * SEC_W / HZ) + 2*off_x + w = int (time_in_hz_to_ideal_coord(proc_tree.end_time)) + proc_tree.sample_period + 2*off_x h = proc_h * proc_tree.num_proc + 2 * off_y if options.charts: h += 110 + (2 + len(trace.disk_stats)) * (30 + bar_h) + 1 * (30 + meminfo_bar_h) if proc_tree.taskstats and options.cumulative: h += CUML_HEIGHT + 4 * off_y - global w_extents, h_extents - w_extents = w - h_extents = h return (w, h) # includes off_x, off_y def clip_visible(clip, rect): @@ -387,15 +385,15 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): # render bar legend ctx.set_font_size(LEGEND_FONT_SIZE) - draw_legend_box(ctx, "CPU (user+sys)", CPU_COLOR, off_x, curr_y+20, leg_s) - draw_legend_box(ctx, "I/O (wait)", IO_COLOR, off_x + 120, curr_y+20, leg_s) + draw_legend_box(ctx, "CPU (user+sys)", CPU_COLOR, 0, curr_y+20, leg_s) + draw_legend_box(ctx, "I/O (wait)", IO_COLOR, 120, curr_y+20, leg_s) draw_legend_diamond(ctx, "Runnable threads", PROCS_RUNNING_COLOR, - off_x +120 +90, curr_y+20, leg_s, leg_s) + 120 +90, curr_y+20, leg_s, leg_s) draw_legend_diamond(ctx, "Blocked threads -- Uninterruptible Syscall", PROCS_BLOCKED_COLOR, - off_x +120 +90 +140, curr_y+20, leg_s, leg_s) + 120 +90 +140, curr_y+20, leg_s, leg_s) # render I/O wait - chart_rect = (off_x, curr_y+30, w, bar_h) + chart_rect = (0, curr_y+30, w, bar_h) if clip_visible (clip, chart_rect): draw_box_ticks (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) @@ -419,15 +417,15 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): # render second chart draw_legend_box(ctx, "Disk utilization -- fraction of sample interval I/O queue was not empty", - IO_COLOR, off_x, curr_y+20, leg_s) + IO_COLOR, 0, curr_y+20, leg_s) if OPTIONS.show_ops_not_bytes: unit = "ops" else: unit = "bytes" draw_legend_line(ctx, "Disk writes -- " + unit + "/sample", - DISK_WRITE_COLOR, off_x+470, curr_y+20, leg_s) + DISK_WRITE_COLOR, 470, curr_y+20, leg_s) draw_legend_line(ctx, "Disk reads+writes -- " + unit + "/sample", - DISK_TPUT_COLOR, off_x+470+120*2, curr_y+20, leg_s) + DISK_TPUT_COLOR, 470+120*2, curr_y+20, leg_s) curr_y += 5 @@ -436,10 +434,10 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): # render I/O utilization for partition in trace.disk_stats: - draw_text(ctx, partition.name, TEXT_COLOR, off_x, curr_y+30) + draw_text(ctx, partition.name, TEXT_COLOR, 0, curr_y+30) # utilization -- inherently normalized [0,1] - chart_rect = (off_x, curr_y+30+5, w, bar_h) + chart_rect = (0, curr_y+30+5, w, bar_h) if clip_visible (clip, chart_rect): draw_box_ticks (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) @@ -462,10 +460,10 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): [(sample.time, sample.write) for sample in partition.samples], \ proc_tree, [0, max_sample.tput], plot_segment_positive) - pos_x = off_x + ((max_sample.time - proc_tree.start_time) * w / proc_tree.duration()) + pos_x = ((max_sample.time - proc_tree.start_time) * w / proc_tree.duration()) shift_x, shift_y = -20, 20 - if (pos_x < off_x + 245): + if (pos_x < 245): shift_x, shift_y = 5, 40 # DISK_BLOCK_SIZE = 1024 @@ -475,15 +473,15 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): curr_y = curr_y + 30 + bar_h # render mem usage - chart_rect = (off_x, curr_y+30, w, meminfo_bar_h) + chart_rect = (0, curr_y+30, w, meminfo_bar_h) mem_stats = trace.mem_stats if mem_stats and clip_visible (clip, chart_rect): mem_scale = max(sample.records['MemTotal'] - sample.records['MemFree'] for sample in mem_stats) - draw_legend_box(ctx, "Mem cached (scale: %u MiB)" % (float(mem_scale) / 1024), MEM_CACHED_COLOR, off_x, curr_y+20, leg_s) - draw_legend_box(ctx, "Used", MEM_USED_COLOR, off_x + 240, curr_y+20, leg_s) - draw_legend_box(ctx, "Buffers", MEM_BUFFERS_COLOR, off_x + 360, curr_y+20, leg_s) + draw_legend_box(ctx, "Mem cached (scale: %u MiB)" % (float(mem_scale) / 1024), MEM_CACHED_COLOR, curr_y+20, leg_s) + draw_legend_box(ctx, "Used", MEM_USED_COLOR, 240, curr_y+20, leg_s) + draw_legend_box(ctx, "Buffers", MEM_BUFFERS_COLOR, 360, curr_y+20, leg_s) draw_legend_line(ctx, "Swap (scale: %u MiB)" % max([(sample.records['SwapTotal'] - sample.records['SwapFree'])/1024 for sample in mem_stats]), \ - MEM_SWAP_COLOR, off_x + 480, curr_y+20, leg_s) + MEM_SWAP_COLOR, 480, curr_y+20, leg_s) draw_box_ticks(ctx, chart_rect) draw_annotations(ctx, proc_tree, trace.times, chart_rect) draw_chart(ctx, MEM_BUFFERS_COLOR, True, chart_rect, \ @@ -506,18 +504,27 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): # # Render the chart. # +# "ctx" is the Cairo drawing context. ctx transform already has panning translation +# and "zoom" scaling applied, but not the asymmetrical xscale arg. def render(ctx, options, xscale, trace, isotemporal_x = None): - (w, h) = extents (options, xscale, trace) - - proc_tree = options.proc_tree (trace) - - # x, y, w, h - clip = ctx.clip_extents() # XX Bounds are initialized, yet clipping is not enforced by pyCairo! ??? + (w, h) = extents (options, xscale, trace) # XX redundant? ctx.set_line_width(1.0) ctx.select_font_face(FONT_NAME) draw_fill_rect(ctx, WHITE, (0, 0, max(w, MIN_IMG_W), h)) + ctx.save() + ctx.translate(off_x, 0) # current window-coord clip shrinks with loss of the off_x-wide strip on left + + proc_tree = options.proc_tree (trace) + + ctx.new_path() + ctx.rectangle(0, 0, w, h) + ctx.clip() + + # x, y, w, h + clip = (-1, -1, -1, -1) # ctx.clip_extents() + w -= 2*off_x # draw the title and headers @@ -548,19 +555,21 @@ def render(ctx, options, xscale, trace, isotemporal_x = None): # draw a cumulative CPU-time-per-process graph if proc_tree.taskstats and options.cumulative: - cuml_rect = (off_x, curr_y + off_y, w, CUML_HEIGHT/2 - off_y * 2) + cuml_rect = (0, curr_y + off_y, w, CUML_HEIGHT/2 - off_y * 2) if clip_visible (clip, cuml_rect): draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_CPU) # draw a cumulative I/O-time-per-process graph if proc_tree.taskstats and options.cumulative: - cuml_rect = (off_x, curr_y + off_y * 100, w, CUML_HEIGHT/2 - off_y * 2) + cuml_rect = (0, curr_y + off_y * 100, w, CUML_HEIGHT/2 - off_y * 2) if clip_visible (clip, cuml_rect): draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_IO) if isotemporal_x: draw_isotemporal(ctx, isotemporal_x) + ctx.restore() + def draw_isotemporal(ctx, isotemporal_x): ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) ctx.set_line_width(0.8) @@ -574,24 +583,20 @@ def draw_process_bar_chart(ctx, clip, options, proc_tree, times, curr_y, w, h): header_size = 0 if not options.kernel_only: draw_legend_diamond (ctx, "Uninterruptible Syscall", - PROC_COLOR_D, off_x+10, curr_y + 45, leg_s*3/4, proc_h) + PROC_COLOR_D, 10, curr_y + 45, leg_s*3/4, proc_h) draw_legend_box (ctx, "Running (%cpu)", - PROC_COLOR_R, off_x+10+180, curr_y + 45, leg_s) + PROC_COLOR_R, 10+180, curr_y + 45, leg_s) draw_legend_box (ctx, "Sleeping", - PROC_COLOR_S, off_x+10+180+130, curr_y + 45, leg_s) + PROC_COLOR_S, 10+180+130, curr_y + 45, leg_s) draw_legend_box (ctx, "Zombie", - PROC_COLOR_Z, off_x+10+180+130+90, curr_y + 45, leg_s) + PROC_COLOR_Z, 10+180+130+90, curr_y + 45, leg_s) header_size = 45 - chart_rect = [off_x, curr_y + header_size + 30, - w, h - 2 * off_y - (curr_y + header_size + 15) + proc_h] + #chart_rect = [0, curr_y + header_size + 30, + # w, h - 2 * off_y - (curr_y + header_size + 15) + proc_h] + chart_rect = [-1, -1, -1, -1] ctx.set_font_size (PROC_TEXT_FONT_SIZE) - ctx.new_path() - ctx.rectangle(chart_rect[0]-off_x, 0, \ - chart_rect[2]+2*off_x, h_extents) - ctx.clip() - draw_box_ticks (ctx, chart_rect) if SEC_W > 100: nsec = 1 @@ -616,7 +621,7 @@ def draw_header (ctx, headers, duration): header_y = ctx.font_extents()[2] + 10 ctx.set_font_size(TITLE_FONT_SIZE) - draw_text(ctx, headers['title'], TEXT_COLOR, off_x, header_y) + draw_text(ctx, headers['title'], TEXT_COLOR, 0, header_y) ctx.set_font_size(TEXT_FONT_SIZE) for (headerkey, headertitle, mangle) in toshow: @@ -626,7 +631,7 @@ def draw_header (ctx, headers, duration): else: value = "" txt = headertitle + ': ' + mangle(value) - draw_text(ctx, txt, TEXT_COLOR, off_x, header_y) + draw_text(ctx, txt, TEXT_COLOR, 0, header_y) dur = duration / 100.0 txt = 'time : %02d:%05.2f' % (math.floor(dur/60), dur - 60 * math.floor(dur/60)) @@ -634,13 +639,13 @@ def draw_header (ctx, headers, duration): txt = txt + ' max pid: %s' % (headers.get('system.maxpid')) header_y += ctx.font_extents()[2] - draw_text (ctx, txt, TEXT_COLOR, off_x, header_y) + draw_text (ctx, txt, TEXT_COLOR, 0, header_y) return header_y def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) : x = time_in_hz_to_ideal_coord(proc.start_time) - w = time_in_hz_to_ideal_coord(proc.start_time + proc.duration) - x + w = time_in_hz_to_ideal_coord(proc.start_time + proc.duration) - x # XX parser fudges duration upward draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip) draw_rect(ctx, PROC_BORDER_COLOR, (x, y, w, proc_h)) @@ -665,7 +670,7 @@ def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) : cmdString = cmdString draw_label_in_box(ctx, PROC_TEXT_COLOR, cmdString, x, y + proc_h - 4, w, - off_x, rect[0] + rect[2]) + 0, rect[0] + rect[2]) next_y = y + proc_h for child in proc.child_list: @@ -881,7 +886,7 @@ def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, stat_type): label_h = extnts[3] # print "Text extents %g by %g" % (label_w, label_h) labels.append((label, - chart_bounds[0] + chart_bounds[2] - label_w - off_x * 2, + chart_bounds[0] + chart_bounds[2] - label_w, y + (cuml + label_h) / 2)) if cs in legends: print("ARGH - duplicate process in list !") @@ -915,7 +920,7 @@ def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, stat_type): label = "Cumulative I/O usage, by process; total I/O: " \ " %.5g(s) time: %.3g(s)" % (cpu_secs, dur_secs) - draw_text(ctx, label, TEXT_COLOR, chart_bounds[0] + off_x, + draw_text(ctx, label, TEXT_COLOR, chart_bounds[0], chart_bounds[1] + font_height) i = 0 @@ -924,7 +929,7 @@ def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, stat_type): for t in legends: cs = t[0] time = t[1] - x = chart_bounds[0] + off_x + int (i/LEGENDS_PER_COL) * label_width + x = chart_bounds[0] + int (i/LEGENDS_PER_COL) * label_width y = chart_bounds[1] + font_height * ((i % LEGENDS_PER_COL) + 2) str = "%s - %.0f(ms) (%2.2f%%)" % (cs.cmd, time/1000000, (time/total_time) * 100.0) draw_legend_box(ctx, str, cs.color, x, y, leg_s) From c75cf01cdaf980f316503aff5052df3627880075 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:03 -0800 Subject: [PATCH 064/182] show diamond for each runnable _process_leader_ thread --- pybootchartgui/draw.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index c2eb632..3167faa 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -127,13 +127,13 @@ def proc_tree (self, trace): # Process states STATE_UNDEFINED = 0 -STATE_RUNNING = 1 +STATE_RUNNING = 1 # useful state info STATE_SLEEPING = 2 -STATE_WAITING = 3 # sole useful state info? +STATE_WAITING = 3 # useful state info STATE_STOPPED = 4 STATE_ZOMBIE = 5 -STATE_COLORS = [(0, 0, 0, 0), PROC_COLOR_S, PROC_COLOR_S, PROC_COLOR_D, \ +STATE_COLORS = [(0, 0, 0, 0), PROCS_RUNNING_COLOR, PROC_COLOR_S, PROC_COLOR_D, \ PROC_COLOR_T, PROC_COLOR_Z, PROC_COLOR_X, PROC_COLOR_W] JUSTIFY_LEFT = "left" @@ -582,14 +582,16 @@ def draw_isotemporal(ctx, isotemporal_x): def draw_process_bar_chart(ctx, clip, options, proc_tree, times, curr_y, w, h): header_size = 0 if not options.kernel_only: + draw_legend_diamond (ctx, "Runnable", + PROCS_RUNNING_COLOR, 10, curr_y + 45, leg_s*3/4, proc_h) draw_legend_diamond (ctx, "Uninterruptible Syscall", - PROC_COLOR_D, 10, curr_y + 45, leg_s*3/4, proc_h) + PROC_COLOR_D, 10+100, curr_y + 45, leg_s*3/4, proc_h) draw_legend_box (ctx, "Running (%cpu)", - PROC_COLOR_R, 10+180, curr_y + 45, leg_s) + PROC_COLOR_R, 10+100+180, curr_y + 45, leg_s) draw_legend_box (ctx, "Sleeping", - PROC_COLOR_S, 10+180+130, curr_y + 45, leg_s) + PROC_COLOR_S, 10+100+180+130, curr_y + 45, leg_s) draw_legend_box (ctx, "Zombie", - PROC_COLOR_Z, 10+180+130+90, curr_y + 45, leg_s) + PROC_COLOR_Z, 10+100+180+130+90, curr_y + 45, leg_s) header_size = 45 #chart_rect = [0, curr_y + header_size + 30, @@ -720,7 +722,7 @@ def draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip) for sample in proc.samples : tx = time_in_hz_to_ideal_coord(sample.time) state = get_proc_state( sample.state ) - if state == STATE_WAITING: + if state == STATE_WAITING or state == STATE_RUNNING: color = STATE_COLORS[state] ctx.set_source_rgba(*color) draw_diamond(ctx, tx, y + proc_h/2, 2.5, proc_h) From 60d4316c29b9f18bd9f624e28f49f6fccb7de614 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:04 -0800 Subject: [PATCH 065/182] process -- keep labels on screen too if process bar is --- pybootchartgui/draw.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 3167faa..6916f49 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -187,7 +187,6 @@ def draw_legend_line(ctx, label, fill_color, x, y, s): ctx.fill() draw_text(ctx, label, TEXT_COLOR, x + s + 5, y) -# XX Needs to know about the transform, to make sure process name is visible def draw_label_in_box(ctx, color, label, x, y, w, minx, maxx): label_w = ctx.text_extents(label)[2] if OPTIONS.justify == JUSTIFY_LEFT: @@ -672,7 +671,7 @@ def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) : cmdString = cmdString draw_label_in_box(ctx, PROC_TEXT_COLOR, cmdString, x, y + proc_h - 4, w, - 0, rect[0] + rect[2]) + ctx.clip_extents()[0], ctx.clip_extents()[2]) next_y = y + proc_h for child in proc.child_list: From e00fa3c1b3929fe196ac18544dd4d043eb7f2076 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:04 -0800 Subject: [PATCH 066/182] draw process without end cap --- pybootchartgui/draw.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 6916f49..77587b4 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -649,7 +649,16 @@ def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) : w = time_in_hz_to_ideal_coord(proc.start_time + proc.duration) - x # XX parser fudges duration upward draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip) - draw_rect(ctx, PROC_BORDER_COLOR, (x, y, w, proc_h)) + + # Do not draw right-hand vertical border -- process exit never exactly known + ctx.set_source_rgba(*PROC_BORDER_COLOR) + ctx.set_line_width(1.0) + ctx.move_to(x+w, y) + ctx.rel_line_to(-w, 0) + ctx.rel_line_to(0, proc_h) + ctx.rel_line_to(w, 0) + ctx.stroke() + draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip) # Event ticks step on the rectangle painted by draw_process_state_colors() (e.g. for non-interruptible wait); From da001125694cd6a697eea6e35205dc68e57f42db Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:04 -0800 Subject: [PATCH 067/182] disable max_sample.tput rendering --- pybootchartgui/draw.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 77587b4..3ec4a66 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -459,12 +459,12 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): [(sample.time, sample.write) for sample in partition.samples], \ proc_tree, [0, max_sample.tput], plot_segment_positive) - pos_x = ((max_sample.time - proc_tree.start_time) * w / proc_tree.duration()) - - shift_x, shift_y = -20, 20 - if (pos_x < 245): - shift_x, shift_y = 5, 40 - + # pos_x = ((max_sample.time - proc_tree.start_time) * w / proc_tree.duration()) + # + # shift_x, shift_y = -20, 20 + # if (pos_x < 245): + # shift_x, shift_y = 5, 40 + # # DISK_BLOCK_SIZE = 1024 # label = "%.1fMB/s" % round ((max_sample.tput) / DISK_BLOCK_SIZE) # draw_text (ctx, label, DISK_TPUT_COLOR, pos_x + shift_x, curr_y + shift_y) From 60fd68eb1a787bb8fee5923d3d91a194d05424c0 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:04 -0800 Subject: [PATCH 068/182] disable header elapsed time printing --- pybootchartgui/draw.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 3ec4a66..aba0369 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -634,13 +634,13 @@ def draw_header (ctx, headers, duration): txt = headertitle + ': ' + mangle(value) draw_text(ctx, txt, TEXT_COLOR, 0, header_y) - dur = duration / 100.0 - txt = 'time : %02d:%05.2f' % (math.floor(dur/60), dur - 60 * math.floor(dur/60)) - if headers.get('system.maxpid') is not None: - txt = txt + ' max pid: %s' % (headers.get('system.maxpid')) - - header_y += ctx.font_extents()[2] - draw_text (ctx, txt, TEXT_COLOR, 0, header_y) +# dur = duration / 100.0 +# txt = 'time : %02d:%05.2f' % (math.floor(dur/60), dur - 60 * math.floor(dur/60)) +# if headers.get('system.maxpid') is not None: +# txt = txt + ' max pid: %s' % (headers.get('system.maxpid')) +# +# header_y += ctx.font_extents()[2] +# draw_text (ctx, txt, TEXT_COLOR, 0, header_y) return header_y From 397b82cfd1e553322efb31a023e6012088267618 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:05 -0800 Subject: [PATCH 069/182] hide mmeeks signature --- pybootchartgui/draw.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index aba0369..66a025f 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -549,8 +549,6 @@ def render(ctx, options, xscale, trace, isotemporal_x = None): curr_y, w, proc_height) curr_y = proc_height - ctx.set_font_size(SIG_FONT_SIZE) - draw_text(ctx, SIGNATURE, SIG_COLOR, off_x + 5, proc_height - 8) # draw a cumulative CPU-time-per-process graph if proc_tree.taskstats and options.cumulative: From 567ec34866dddccdb9079efe4e2c2daaf80466c8 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:05 -0800 Subject: [PATCH 070/182] events -- fix event label collisions --- pybootchartgui/draw.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 66a025f..ca3d421 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -202,9 +202,9 @@ def draw_label_in_box(ctx, color, label, x, y, w, minx, maxx): label_x = minx draw_text(ctx, label, color, label_x, y) -def draw_label_in_box_at_time(ctx, color, label, x, y, label_x): - label_w = ctx.text_extents(label)[2] +def draw_label_in_box_at_time(ctx, color, label, y, label_x): draw_text(ctx, label, color, label_x, y) + return ctx.text_extents(label)[2] def time_in_hz_to_ideal_coord(t_hz): return (t_hz-time_origin_drawn) * SEC_W / HZ @@ -709,6 +709,7 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, cl def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): ev_regex = re.compile(OPTIONS.event_regex) ctx.set_source_rgba(*EVENT_COLOR) + last_x_touched = 0 for ev in proc.events: if not ev_regex.match(ev.match) and ev.match != "sample_start": continue @@ -718,10 +719,11 @@ def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): ctx.line_to(tx+1, y+proc_h) ctx.line_to(tx, y+proc_h) ctx.fill() - if OPTIONS.print_event_times: - draw_label_in_box_at_time(ctx, PROC_TEXT_COLOR, - '%.2f' % (float(ev.time - time_origin_drawn) / HZ), - x, y + proc_h - 4, tx) + if OPTIONS.print_event_times and tx > last_x_touched + 5: + last_x_touched = tx + draw_label_in_box_at_time( + ctx, PROC_TEXT_COLOR, + '%.2f' % (float(ev.time - time_origin_drawn) / HZ), + y + proc_h - 4, tx) def draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip): last_tx = -1 From 9b38927ff09fa8ea14c85f78b7cf5b939a2e6eaf Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:05 -0800 Subject: [PATCH 071/182] ctx_save -- wrapper -- time_in_hz_to_x --- pybootchartgui/draw.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index ca3d421..1e137fc 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -218,6 +218,11 @@ def time_in_hz_to_ideal_coord(t_hz): def user_to_time(x): return (x - off_x) * HZ / SEC_W + time_origin_drawn +def ctx_save__time_in_hz_to_x(ctx): + ctx.save() + ctx.scale(float(SEC_W) / HZ, 1.0) + ctx.translate(-time_origin_drawn, 0.0) + def draw_sec_labels(ctx, rect, nsecs): ctx.set_font_size(AXIS_FONT_SIZE) prev_x = 0 @@ -697,14 +702,14 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, cl # XX should look up time of previous sample, not assume 'proc_tree.sample_period' # 2. proc start after sampling last_time = max(proc.start_time, proc.samples[0].time - proc_tree.sample_period) - last_tx = time_in_hz_to_ideal_coord(last_time) + ctx_save__time_in_hz_to_x(ctx) for sample in proc.samples[1:] : - tx = time_in_hz_to_ideal_coord(sample.time) alpha = min(sample.cpu_sample.user + sample.cpu_sample.sys, 1.0) # XX rationale? cpu_color = tuple(list(PROC_COLOR_R[0:3]) + [alpha]) # XXX correct color for non-uniform sample intervals - draw_fill_rect(ctx, cpu_color, (last_tx, y, tx - last_tx, proc_h)) - last_tx = tx + draw_fill_rect(ctx, cpu_color, (last_time, y, sample.time - last_time, proc_h)) + last_time = sample.time + ctx.restore() def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): ev_regex = re.compile(OPTIONS.event_regex) From d973b77d4a041f805fe090aafaadf714cfdbdbce Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:05 -0800 Subject: [PATCH 072/182] FOLD draw.py -- time variables renaming --- pybootchartgui/draw.py | 55 +++++++++++++++++++-------------------- pybootchartgui/gui.py | 11 ++++---- pybootchartgui/parsing.py | 1 - 3 files changed, 32 insertions(+), 35 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 1e137fc..0f5418f 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -206,21 +206,21 @@ def draw_label_in_box_at_time(ctx, color, label, y, label_x): draw_text(ctx, label, color, label_x, y) return ctx.text_extents(label)[2] -def time_in_hz_to_ideal_coord(t_hz): - return (t_hz-time_origin_drawn) * SEC_W / HZ +def csec_to_xscaled(t_csec): + return (t_csec-time_origin_drawn) * SEC_W / CSEC -# Solve for t_hz: -# x = (t_hz-time_origin_drawn) * SEC_W / HZ + off_x +# Solve for t_csec: +# x = (t_csec-time_origin_drawn) * SEC_W / CSEC + off_x # -# x - off_x = (t_hz-time_origin_drawn) * SEC_W / HZ -# (x - off_x) * HZ / SEC_W = t_hz-time_origin_drawn +# x - off_x = (t_csec-time_origin_drawn) * SEC_W / CSEC +# (x - off_x) * CSEC / SEC_W = t_csec-time_origin_drawn # -def user_to_time(x): - return (x - off_x) * HZ / SEC_W + time_origin_drawn +def xscaled_to_csec(x): + return (x - off_x) * CSEC / SEC_W + time_origin_drawn -def ctx_save__time_in_hz_to_x(ctx): +def ctx_save__csec_to_xscaled(ctx): ctx.save() - ctx.scale(float(SEC_W) / HZ, 1.0) + ctx.scale(float(SEC_W) / CSEC, 1.0) ctx.translate(-time_origin_drawn, 0.0) def draw_sec_labels(ctx, rect, nsecs): @@ -258,7 +258,7 @@ def draw_annotations(ctx, proc_tree, times, rect): for time in times: if time is not None: - x = time_in_hz_to_ideal_coord(time) + x = csec_to_xscaled(time) ctx.move_to(x, rect[1] + 1) ctx.line_to(x, rect[1] + rect[3] - 1) @@ -293,11 +293,11 @@ def plot_scatter_positive(ctx, point, x, y): def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree, data_range, plot_point_func): def transform_point_coords(point, y_base, \ xscale, yscale, x_trans, y_trans): - x = time_in_hz_to_ideal_coord(point[0]) + x = csec_to_xscaled(point[0]) y = (point[1] - y_base) * -yscale + y_trans + chart_bounds[3] return x, y - max_x = proc_tree.end_time # units of HZ + max_x = proc_tree.end_time # units of CSEC max_y = max (y for (x, y) in data) # avoid divide by zero if max_y == 0: @@ -339,6 +339,7 @@ def transform_point_coords(point, y_base, \ # Constants # XX put all of constants in a named tuple, for immutability +CSEC = 100 bar_h = 55 meminfo_bar_h = 2 * bar_h # offsets @@ -351,7 +352,6 @@ def transform_point_coords(point, y_base, \ OPTIONS = None SEC_W = None -HZ = None time_origin_drawn = None # time of leftmost plotted data # window coords @@ -360,9 +360,8 @@ def transform_point_coords(point, y_base, \ # and every time xscale changes. # Returns size of a window capable of holding the whole scene? def extents(options, xscale, trace): - global OPTIONS, HZ, time_origin_drawn + global OPTIONS, time_origin_drawn OPTIONS = options.app_options - HZ = trace.HZ proc_tree = options.proc_tree(trace) if OPTIONS.prehistory: @@ -372,7 +371,7 @@ def extents(options, xscale, trace): global SEC_W SEC_W = int (xscale * sec_w_base) - w = int (time_in_hz_to_ideal_coord(proc_tree.end_time)) + proc_tree.sample_period + 2*off_x + w = int (csec_to_xscaled(proc_tree.end_time)) + proc_tree.sample_period + 2*off_x h = proc_h * proc_tree.num_proc + 2 * off_y if options.charts: h += 110 + (2 + len(trace.disk_stats)) * (30 + bar_h) + 1 * (30 + meminfo_bar_h) @@ -510,7 +509,7 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): # # "ctx" is the Cairo drawing context. ctx transform already has panning translation # and "zoom" scaling applied, but not the asymmetrical xscale arg. -def render(ctx, options, xscale, trace, isotemporal_x = None): +def render(ctx, options, xscale, trace, isotemporal_csec = None): (w, h) = extents (options, xscale, trace) # XX redundant? ctx.set_line_width(1.0) @@ -567,16 +566,16 @@ def render(ctx, options, xscale, trace, isotemporal_x = None): if clip_visible (clip, cuml_rect): draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_IO) - if isotemporal_x: - draw_isotemporal(ctx, isotemporal_x) + if isotemporal_csec: + draw_isotemporal(ctx, isotemporal_csec) ctx.restore() -def draw_isotemporal(ctx, isotemporal_x): +def draw_isotemporal(ctx, isotemporal_csec): ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) ctx.set_line_width(0.8) ctx.set_dash([4, 2]) - isotemporal_x = time_in_hz_to_ideal_coord(isotemporal_x) + isotemporal_x = csec_to_xscaled(isotemporal_csec) ctx.move_to(isotemporal_x, 0) ctx.line_to(isotemporal_x, CUML_HEIGHT) ctx.stroke() @@ -648,8 +647,8 @@ def draw_header (ctx, headers, duration): return header_y def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) : - x = time_in_hz_to_ideal_coord(proc.start_time) - w = time_in_hz_to_ideal_coord(proc.start_time + proc.duration) - x # XX parser fudges duration upward + x = csec_to_xscaled(proc.start_time) + w = csec_to_xscaled(proc.start_time + proc.duration) - x # XX parser fudges duration upward draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip) @@ -702,7 +701,7 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, cl # XX should look up time of previous sample, not assume 'proc_tree.sample_period' # 2. proc start after sampling last_time = max(proc.start_time, proc.samples[0].time - proc_tree.sample_period) - ctx_save__time_in_hz_to_x(ctx) + ctx_save__csec_to_xscaled(ctx) for sample in proc.samples[1:] : alpha = min(sample.cpu_sample.user + sample.cpu_sample.sys, 1.0) # XX rationale? cpu_color = tuple(list(PROC_COLOR_R[0:3]) + [alpha]) @@ -718,7 +717,7 @@ def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): for ev in proc.events: if not ev_regex.match(ev.match) and ev.match != "sample_start": continue - tx = time_in_hz_to_ideal_coord(ev.time) + tx = csec_to_xscaled(ev.time) ctx.move_to(tx-1, y+proc_h) ctx.line_to(tx, y+proc_h-5) ctx.line_to(tx+1, y+proc_h) @@ -727,13 +726,13 @@ def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): if OPTIONS.print_event_times and tx > last_x_touched + 5: last_x_touched = tx + draw_label_in_box_at_time( ctx, PROC_TEXT_COLOR, - '%.2f' % (float(ev.time - time_origin_drawn) / HZ), + '%.2f' % (float(ev.time - time_origin_drawn) / CSEC), y + proc_h - 4, tx) def draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip): last_tx = -1 for sample in proc.samples : - tx = time_in_hz_to_ideal_coord(sample.time) + tx = csec_to_xscaled(sample.time) state = get_proc_state( sample.state ) if state == STATE_WAITING or state == STATE_RUNNING: color = STATE_COLORS[state] diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index 5124833..1c23697 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -58,7 +58,7 @@ def __init__(self, trace, options, xscale): self.hadj_changed_signal_id = None self.vadj_changed_signal_id = None - self.isotemporal_x = None + self.isotemporal_csec = None def do_expose_event(self, event): cr = self.window.cairo_create() @@ -80,7 +80,7 @@ def draw(self, cr, rect): cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) cr.paint() self.cr_set_up_transform(cr) - draw.render(cr, self.options, self.xscale, self.trace, self.isotemporal_x) + draw.render(cr, self.options, self.xscale, self.trace, self.isotemporal_csec) def position_changed(self): self.emit("position-changed", self.x, self.y) @@ -160,13 +160,12 @@ def on_area_button_press(self, area, event): self.prevmousey = event.y if event.button == 2: cr = self.window.cairo_create() - uy = None self.cr_set_up_transform(cr) - self.isotemporal_x, uy = cr.device_to_user(event.x, 0) - self.isotemporal_x = draw.user_to_time(self.isotemporal_x) + ux, uy = cr.device_to_user(event.x, 0) + self.isotemporal_csec = draw.xscaled_to_csec(ux) self.queue_draw() if event.button == 3: - self.isotemporal_x = None + self.isotemporal_csec = None self.queue_draw() if event.type not in (gtk.gdk.BUTTON_PRESS, gtk.gdk.BUTTON_RELEASE): return False diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index e44eaaa..5d83294 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -31,7 +31,6 @@ class Trace: def __init__(self, writer, paths, options): - self.HZ = 100 # XX a smarter collector would collect this self.headers = None self.disk_stats = None self.ps_stats = None From 8e2c36db56bbe944380077a49e289bd3e47ec95b Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:05 -0800 Subject: [PATCH 073/182] zoom remains centered --- pybootchartgui/gui.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index 1c23697..fe1ab1f 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -60,7 +60,7 @@ def __init__(self, trace, options, xscale): self.isotemporal_csec = None - def do_expose_event(self, event): + def do_expose_event(self, event): # XX called on mouse entering or leaving window -- can these be disabled? cr = self.window.cairo_create() # set a clip region for the expose event @@ -69,7 +69,7 @@ def do_expose_event(self, event): event.area.width, event.area.height ) cr.clip() - self.draw(cr, self.get_allocation()) + self.draw(cr, self.get_allocation()) # XX get_allocation() can yield a sub-rectangle of cr.clip_extents() return False def cr_set_up_transform(self, cr): @@ -77,22 +77,39 @@ def cr_set_up_transform(self, cr): cr.translate(-self.x, -self.y) def draw(self, cr, rect): - cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) - cr.paint() + cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) # XX redundant + cr.paint() # XX effect? self.cr_set_up_transform(cr) draw.render(cr, self.options, self.xscale, self.trace, self.isotemporal_csec) def position_changed(self): self.emit("position-changed", self.x, self.y) - ZOOM_INCREMENT = 1.25 + # back-transform center of window to user coords -- c.f. cr_set_up_transform() + def current_center (self): + return (self.x + (self.hadj.page_size / self.zoom_ratio / 2), self.y + (self.vadj.page_size / self.zoom_ratio / 2)) + + # assuming a new zoom_ratio, set top-left corner displayed in user space so that + # (x, y) will be at window center + def set_center (self, x, y): + # back-transform window (w, h) + user_w = self.hadj.page_size / self.zoom_ratio + user_h = self.vadj.page_size / self.zoom_ratio + self.x = x - user_w/2 + self.y = y - user_h/2 + + ZOOM_INCREMENT = 1.25 + # Zoom maintaining the content at window's current center untranslated. + # "Center" is irrespective of any occlusion. def zoom_image (self, zoom_ratio): + old_x, old_y = self.current_center () self.zoom_ratio = zoom_ratio + self.set_center(old_x, old_y) self._set_scroll_adjustments (self.hadj, self.vadj) self.queue_draw() - def zoom_to_rect (self, rect): + def zoom_to_rect (self, rect): # rename "zoom_to_window_width"? zoom_ratio = float(rect.width)/float(self.chart_width) self.zoom_image(zoom_ratio) self.x = 0 @@ -121,7 +138,7 @@ def on_zoom_fit(self, action): self.zoom_to_rect(self.get_allocation()) def on_zoom_100(self, action): - self.zoom_image(1.0) + self.zoom_image(1.0) # XX replace with: self.zoom_ratio = 1.0 \ self.x = 0 \ self.y = 0 self.set_xscale(1.0) def show_thread_details(self, button): @@ -253,7 +270,7 @@ def _set_scroll_adjustments(self, hadj, vadj): if hadj != None: self.hadj = hadj self._set_adj_upper (self.hadj, self.zoom_ratio * self.chart_width) - self.hadj_changed_signal_id = self.hadj.connect('value-changed', self.on_adjustments_changed) + self.hadj_changed_signal_id = self.hadj.connect('value-changed', self.on_adjustments_changed) # XX leaky? if vadj != None: self.vadj = vadj From 7f27584d781cd1a8faefd68ee10e7782adb4beda Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:06 -0800 Subject: [PATCH 074/182] FIX -- xscale centered --- TODO | 3 --- pybootchartgui/gui.py | 49 ++++++++++++++++++++++++------------------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/TODO b/TODO index 7ae09f8..2b2da24 100644 --- a/TODO +++ b/TODO @@ -13,9 +13,6 @@ + traced %d processes of $$ (!) :-) + consumed %d seconds of CPU [ render these ... ] - + horizontal zoom - should keep view horiz centered: - but does not - cf. set_xscale - + merge processes for CPU counts: + if the ns count is -identical- (and non-zero) it is the same process + need to back-merge somehow ? diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index fe1ab1f..e39412d 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -77,27 +77,36 @@ def cr_set_up_transform(self, cr): cr.translate(-self.x, -self.y) def draw(self, cr, rect): - cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) # XX redundant - cr.paint() # XX effect? + cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) + cr.paint() # fill whole DrawingArea with white self.cr_set_up_transform(cr) draw.render(cr, self.options, self.xscale, self.trace, self.isotemporal_csec) def position_changed(self): self.emit("position-changed", self.x, self.y) - # back-transform center of window to user coords -- c.f. cr_set_up_transform() - def current_center (self): - return (self.x + (self.hadj.page_size / self.zoom_ratio / 2), self.y + (self.vadj.page_size / self.zoom_ratio / 2)) - - # assuming a new zoom_ratio, set top-left corner displayed in user space so that - # (x, y) will be at window center - def set_center (self, x, y): - # back-transform window (w, h) - user_w = self.hadj.page_size / self.zoom_ratio - user_h = self.vadj.page_size / self.zoom_ratio + def device_to_csec_user_y(self, dx, dy): + cr = self.window.cairo_create() + self.cr_set_up_transform(cr) + ux, uy = cr.device_to_user(dx, dy) + self.chart_width, self.chart_height = draw.extents(self.options, self.xscale, self.trace) + return draw.xscaled_to_csec(ux), uy # XX depends on state set by draw.extents() - self.x = x - user_w/2 - self.y = y - user_h/2 + # back-transform center of widget to (time, chart_height) coords + def current_center (self): + (wx, wy, ww, wh) = self.get_allocation() + return self.device_to_csec_user_y (ww/2, wh/2) + + # Assuming a new zoom_ratio or xscale have been set, correspondingly + # set top-left corner displayed (self.x, self.y) so that + # (csec_x, user_y) will be at window center + def set_center (self, csec_x, user_y): + cur_csec, cur_user_y = self.current_center () + cur_user_x = draw.csec_to_xscaled(cur_csec) + user_x = draw.csec_to_xscaled(csec_x) + self.x += (user_x - cur_user_x) + self.y += (user_y - cur_user_y) + self.position_changed() ZOOM_INCREMENT = 1.25 # Zoom maintaining the content at window's current center untranslated. @@ -116,11 +125,12 @@ def zoom_to_rect (self, rect): # rename "zoom_to_window_width"? self.position_changed() def set_xscale(self, xscale): - old_mid_x = self.x + self.hadj.page_size / 2 + old_x, old_y = self.current_center () self.xscale = xscale self.chart_width, self.chart_height = draw.extents(self.options, self.xscale, self.trace) - new_x = old_mid_x - self.zoom_image (self.zoom_ratio) + self.set_center(old_x, old_y) + self._set_scroll_adjustments (self.hadj, self.vadj) + self.queue_draw() def on_expand(self, action): self.set_xscale (self.xscale * 1.1) @@ -176,10 +186,7 @@ def on_area_button_press(self, area, event): self.prevmousex = event.x self.prevmousey = event.y if event.button == 2: - cr = self.window.cairo_create() - self.cr_set_up_transform(cr) - ux, uy = cr.device_to_user(event.x, 0) - self.isotemporal_csec = draw.xscaled_to_csec(ux) + self.isotemporal_csec, uy = self.device_to_csec_user_y(event.x, 0) self.queue_draw() if event.button == 3: self.isotemporal_csec = None From 6b4fa79fdac498dc37f9ed6231e752460a9eb431 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:06 -0800 Subject: [PATCH 075/182] refactor -- gui simplify --- pybootchartgui/gui.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index e39412d..dffa036 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -62,21 +62,14 @@ def __init__(self, trace, options, xscale): def do_expose_event(self, event): # XX called on mouse entering or leaving window -- can these be disabled? cr = self.window.cairo_create() - - # set a clip region for the expose event - cr.rectangle( - event.area.x, event.area.y, - event.area.width, event.area.height - ) - cr.clip() - self.draw(cr, self.get_allocation()) # XX get_allocation() can yield a sub-rectangle of cr.clip_extents() + self.draw(cr) return False def cr_set_up_transform(self, cr): cr.scale(self.zoom_ratio, self.zoom_ratio) cr.translate(-self.x, -self.y) - def draw(self, cr, rect): + def draw(self, cr): cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) cr.paint() # fill whole DrawingArea with white self.cr_set_up_transform(cr) From abd3297ca992cdfc18cfcf946bab1a03c5f9381f Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:06 -0800 Subject: [PATCH 076/182] events -- times relative to isotemporal --- pybootchartgui/draw.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 0f5418f..1db33f7 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -504,6 +504,8 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): return curr_y +ISOTEMPORAL_CSEC = None + # # Render the chart. # @@ -516,6 +518,9 @@ def render(ctx, options, xscale, trace, isotemporal_csec = None): ctx.select_font_face(FONT_NAME) draw_fill_rect(ctx, WHITE, (0, 0, max(w, MIN_IMG_W), h)) + global ISOTEMPORAL_CSEC + ISOTEMPORAL_CSEC = isotemporal_csec + ctx.save() ctx.translate(off_x, 0) # current window-coord clip shrinks with loss of the off_x-wide strip on left @@ -712,8 +717,14 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, cl def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): ev_regex = re.compile(OPTIONS.event_regex) + if ISOTEMPORAL_CSEC: + time_origin_relative = ISOTEMPORAL_CSEC + else: + time_origin_relative = time_origin_drawn + proc_tree.sample_period # XX align to time of first sample ctx.set_source_rgba(*EVENT_COLOR) last_x_touched = 0 + last_label_str = None + precision = min(6, SEC_W/100) for ev in proc.events: if not ev_regex.match(ev.match) and ev.match != "sample_start": continue @@ -724,10 +735,13 @@ def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): ctx.line_to(tx, y+proc_h) ctx.fill() if OPTIONS.print_event_times and tx > last_x_touched + 5: - last_x_touched = tx + draw_label_in_box_at_time( + label_str = '%.*f' % (precision, (float(ev.time_usec)/1000/10 - time_origin_relative) / CSEC) + if label_str != last_label_str: + last_x_touched = tx + draw_label_in_box_at_time( ctx, PROC_TEXT_COLOR, - '%.2f' % (float(ev.time - time_origin_drawn) / CSEC), + label_str, y + proc_h - 4, tx) + last_label_str = label_str def draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip): last_tx = -1 From 12d83a37d6f9b261c9b23f6113e16453baf5d83a Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:06 -0800 Subject: [PATCH 077/182] SEC_W float not int --- pybootchartgui/draw.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 1db33f7..fb2b36a 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -237,6 +237,7 @@ def draw_sec_labels(ctx, rect, nsecs): def draw_box_ticks(ctx, rect): draw_rect(ctx, BORDER_COLOR, tuple(rect)) + return ctx.set_line_cap(cairo.LINE_CAP_SQUARE) @@ -369,7 +370,7 @@ def extents(options, xscale, trace): else: time_origin_drawn = trace.ps_stats.start_time - proc_tree.sample_period global SEC_W - SEC_W = int (xscale * sec_w_base) + SEC_W = xscale * sec_w_base w = int (csec_to_xscaled(proc_tree.end_time)) + proc_tree.sample_period + 2*off_x h = proc_h * proc_tree.num_proc + 2 * off_y @@ -724,7 +725,7 @@ def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): ctx.set_source_rgba(*EVENT_COLOR) last_x_touched = 0 last_label_str = None - precision = min(6, SEC_W/100) + precision = int( min(6, SEC_W/100)) for ev in proc.events: if not ev_regex.match(ev.match) and ev.match != "sample_start": continue From 83a7dbce343bd144a95d068a01a8ed93d09c324b Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:06 -0800 Subject: [PATCH 078/182] FOLD fix extents --- pybootchartgui/draw.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index fb2b36a..55560a7 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -368,11 +368,11 @@ def extents(options, xscale, trace): if OPTIONS.prehistory: time_origin_drawn = 0 # XX Would have to be process_tree.starttime for backwards compatibility else: - time_origin_drawn = trace.ps_stats.start_time - proc_tree.sample_period + time_origin_drawn = trace.cpu_stats[0].time - proc_tree.sample_period global SEC_W SEC_W = xscale * sec_w_base - w = int (csec_to_xscaled(proc_tree.end_time)) + proc_tree.sample_period + 2*off_x + w = int (csec_to_xscaled(trace.cpu_stats[-1].time + proc_tree.sample_period) + 2*off_x) h = proc_h * proc_tree.num_proc + 2 * off_y if options.charts: h += 110 + (2 + len(trace.disk_stats)) * (30 + bar_h) + 1 * (30 + meminfo_bar_h) From 70a98c39e056cd06c181c9c93b95da30715be0cf Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:07 -0800 Subject: [PATCH 079/182] FOLD -- off by one period --- pybootchartgui/draw.py | 58 +++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 55560a7..96f0511 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -272,14 +272,16 @@ def plot_line(ctx, point, x, y): ctx.set_line_width(1.0) ctx.line_to(x, y) # rightward, and upward or downward +# backward-looking def plot_square(ctx, point, x, y): ctx.set_line_width(1.0) - ctx.line_to(x, ctx.get_current_point()[1]) # rightward - ctx.line_to(x, y) # upward or downward + ctx.line_to(ctx.get_current_point()[0], y) # upward or downward + ctx.line_to(x, y) # rightward +# backward-looking def plot_segment_positive(ctx, point, x, y): ctx.move_to(ctx.get_current_point()[0], y) # upward or downward - if point[1] <= 0: + if point[1] <= 0: # zero-Y samples draw nothing ctx.move_to(x, y) return ctx.set_line_width(1.5) @@ -292,18 +294,14 @@ def plot_scatter_positive(ctx, point, x, y): # All charts assumed to be full-width def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree, data_range, plot_point_func): - def transform_point_coords(point, y_base, \ - xscale, yscale, x_trans, y_trans): + def transform_point_coords(point, y_base, yscale, y_trans): x = csec_to_xscaled(point[0]) y = (point[1] - y_base) * -yscale + y_trans + chart_bounds[3] return x, y - max_x = proc_tree.end_time # units of CSEC max_y = max (y for (x, y) in data) - # avoid divide by zero - if max_y == 0: + if max_y <= 0: # avoid divide by zero max_y = 1.0 - xscale = float (chart_bounds[2]) / max_x # If data_range is given, scale the chart so that the value range in # data_range matches the chart bounds exactly. # Otherwise, scale so that the actual data matches the chart bounds. @@ -314,23 +312,23 @@ def transform_point_coords(point, y_base, \ yscale = float(chart_bounds[3]) / max_y ybase = 0 - last = transform_point_coords (data[-1], ybase, xscale, yscale, \ - chart_bounds[0], chart_bounds[1]) - first = transform_point_coords (data[0], ybase, xscale, yscale, \ - chart_bounds[0], chart_bounds[1]) - ctx.set_source_rgba(*color) - ctx.move_to(*first) + + # move to the x of the missing first sample point + first = transform_point_coords ([time_origin_drawn + in_chart_X_margin(proc_tree), -9999], + ybase, yscale, chart_bounds[1]) + ctx.move_to(first[0], first[1]) for point in data: - x, y = transform_point_coords (point, ybase, xscale, yscale, \ - chart_bounds[0], chart_bounds[1]) + x, y = transform_point_coords (point, ybase, yscale, chart_bounds[1]) plot_point_func(ctx, point, x, y) + final = transform_point_coords (data[-1], ybase, yscale, chart_bounds[1]) + if fill: ctx.set_line_width(0.0) ctx.stroke_preserve() - ctx.line_to(last[0], chart_bounds[1]+chart_bounds[3]) + ctx.line_to(final[0], chart_bounds[1]+chart_bounds[3]) ctx.line_to(first[0], chart_bounds[1]+chart_bounds[3]) ctx.line_to(first[0], first[1]) ctx.fill() @@ -357,6 +355,9 @@ def transform_point_coords(point, y_base, \ # window coords +def in_chart_X_margin(proc_tree): + return proc_tree.sample_period + # Called from gui.py and batch.py, before first call to render(), # and every time xscale changes. # Returns size of a window capable of holding the whole scene? @@ -368,11 +369,11 @@ def extents(options, xscale, trace): if OPTIONS.prehistory: time_origin_drawn = 0 # XX Would have to be process_tree.starttime for backwards compatibility else: - time_origin_drawn = trace.cpu_stats[0].time - proc_tree.sample_period + time_origin_drawn = trace.cpu_stats[0].time - in_chart_X_margin(proc_tree) global SEC_W SEC_W = xscale * sec_w_base - w = int (csec_to_xscaled(trace.cpu_stats[-1].time + proc_tree.sample_period) + 2*off_x) + w = int (csec_to_xscaled(trace.cpu_stats[-1].time + in_chart_X_margin(proc_tree)) + 2*off_x) h = proc_h * proc_tree.num_proc + 2 * off_y if options.charts: h += 110 + (2 + len(trace.disk_stats)) * (30 + bar_h) + 1 * (30 + meminfo_bar_h) @@ -396,23 +397,25 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): draw_legend_diamond(ctx, "Blocked threads -- Uninterruptible Syscall", PROCS_BLOCKED_COLOR, 120 +90 +140, curr_y+20, leg_s, leg_s) - # render I/O wait chart_rect = (0, curr_y+30, w, bar_h) if clip_visible (clip, chart_rect): draw_box_ticks (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) + # render I/O wait -- a backwards delta draw_chart (ctx, IO_COLOR, True, chart_rect, \ [(sample.time, sample.user + sample.sys + sample.io) for sample in trace.cpu_stats], \ proc_tree, None, plot_square) - # render CPU load + # render CPU load -- a backwards delta draw_chart (ctx, CPU_COLOR, True, chart_rect, \ [(sample.time, sample.user + sample.sys) for sample in trace.cpu_stats], \ proc_tree, None, plot_square) + # instantaneous sample draw_chart (ctx, PROCS_RUNNING_COLOR, False, chart_rect, [(sample.time, sample.procs_running) for sample in trace.cpu_stats], \ proc_tree, [0, 9], plot_scatter_positive) + # instantaneous sample draw_chart (ctx, PROCS_BLOCKED_COLOR, False, chart_rect, [(sample.time, sample.procs_blocked) for sample in trace.cpu_stats], \ proc_tree, [0, 9], plot_scatter_positive) @@ -445,8 +448,9 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): if clip_visible (clip, chart_rect): draw_box_ticks (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) - draw_chart (ctx, IO_COLOR, True, chart_rect, \ - [(sample.time, sample.util) for sample in partition.samples], \ + # a backwards delta + draw_chart (ctx, IO_COLOR, True, chart_rect, + [(sample.time, sample.util) for sample in partition.samples], proc_tree, [0, 1], plot_square) # render disk throughput @@ -455,13 +459,15 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): # XXX correction for non-constant sample.time? max_sample = max (partition.samples, key = lambda s: s.tput) if clip_visible (clip, chart_rect): + # a backwards delta draw_chart (ctx, DISK_TPUT_COLOR, False, chart_rect, - [(sample.time, sample.tput) for sample in partition.samples], \ + [(sample.time, sample.tput) for sample in partition.samples], proc_tree, [0, max_sample.tput], plot_segment_positive) # overlay write throughput + # a backwards delta draw_chart (ctx, DISK_WRITE_COLOR, False, chart_rect, - [(sample.time, sample.write) for sample in partition.samples], \ + [(sample.time, sample.write) for sample in partition.samples], proc_tree, [0, max_sample.tput], plot_segment_positive) # pos_x = ((max_sample.time - proc_tree.start_time) * w / proc_tree.duration()) From bac510e5c7cc47a3da3583868c49a69ae43e8434 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:07 -0800 Subject: [PATCH 080/182] delete Y clipping --- pybootchartgui/draw.py | 112 +++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 61 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 96f0511..734a4bd 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -381,10 +381,7 @@ def extents(options, xscale, trace): h += CUML_HEIGHT + 4 * off_y return (w, h) # includes off_x, off_y -def clip_visible(clip, rect): - return True - -def render_charts(ctx, options, clip, trace, curr_y, w, h): +def render_charts(ctx, options, trace, curr_y, w, h): proc_tree = options.proc_tree(trace) # render bar legend @@ -398,27 +395,26 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): 120 +90 +140, curr_y+20, leg_s, leg_s) chart_rect = (0, curr_y+30, w, bar_h) - if clip_visible (clip, chart_rect): - draw_box_ticks (ctx, chart_rect) - draw_annotations (ctx, proc_tree, trace.times, chart_rect) - # render I/O wait -- a backwards delta - draw_chart (ctx, IO_COLOR, True, chart_rect, \ - [(sample.time, sample.user + sample.sys + sample.io) for sample in trace.cpu_stats], \ - proc_tree, None, plot_square) - # render CPU load -- a backwards delta - draw_chart (ctx, CPU_COLOR, True, chart_rect, \ - [(sample.time, sample.user + sample.sys) for sample in trace.cpu_stats], \ - proc_tree, None, plot_square) - - # instantaneous sample - draw_chart (ctx, PROCS_RUNNING_COLOR, False, chart_rect, - [(sample.time, sample.procs_running) for sample in trace.cpu_stats], \ - proc_tree, [0, 9], plot_scatter_positive) - - # instantaneous sample - draw_chart (ctx, PROCS_BLOCKED_COLOR, False, chart_rect, - [(sample.time, sample.procs_blocked) for sample in trace.cpu_stats], \ - proc_tree, [0, 9], plot_scatter_positive) + draw_box_ticks (ctx, chart_rect) + draw_annotations (ctx, proc_tree, trace.times, chart_rect) + # render I/O wait -- a backwards delta + draw_chart (ctx, IO_COLOR, True, chart_rect, \ + [(sample.time, sample.user + sample.sys + sample.io) for sample in trace.cpu_stats], \ + proc_tree, None, plot_square) + # render CPU load -- a backwards delta + draw_chart (ctx, CPU_COLOR, True, chart_rect, \ + [(sample.time, sample.user + sample.sys) for sample in trace.cpu_stats], \ + proc_tree, None, plot_square) + + # instantaneous sample + draw_chart (ctx, PROCS_RUNNING_COLOR, False, chart_rect, + [(sample.time, sample.procs_running) for sample in trace.cpu_stats], \ + proc_tree, [0, 9], plot_scatter_positive) + + # instantaneous sample + draw_chart (ctx, PROCS_BLOCKED_COLOR, False, chart_rect, + [(sample.time, sample.procs_blocked) for sample in trace.cpu_stats], \ + proc_tree, [0, 9], plot_scatter_positive) curr_y = curr_y + 50 + bar_h @@ -445,30 +441,29 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): # utilization -- inherently normalized [0,1] chart_rect = (0, curr_y+30+5, w, bar_h) - if clip_visible (clip, chart_rect): - draw_box_ticks (ctx, chart_rect) - draw_annotations (ctx, proc_tree, trace.times, chart_rect) - # a backwards delta - draw_chart (ctx, IO_COLOR, True, chart_rect, - [(sample.time, sample.util) for sample in partition.samples], - proc_tree, [0, 1], plot_square) + draw_box_ticks (ctx, chart_rect) + draw_annotations (ctx, proc_tree, trace.times, chart_rect) + # a backwards delta + draw_chart (ctx, IO_COLOR, True, chart_rect, + [(sample.time, sample.util) for sample in partition.samples], + proc_tree, [0, 1], plot_square) # render disk throughput # XXX assume single block device, for now if not max_sample: # XXX correction for non-constant sample.time? max_sample = max (partition.samples, key = lambda s: s.tput) - if clip_visible (clip, chart_rect): - # a backwards delta - draw_chart (ctx, DISK_TPUT_COLOR, False, chart_rect, - [(sample.time, sample.tput) for sample in partition.samples], - proc_tree, [0, max_sample.tput], plot_segment_positive) - - # overlay write throughput - # a backwards delta - draw_chart (ctx, DISK_WRITE_COLOR, False, chart_rect, - [(sample.time, sample.write) for sample in partition.samples], - proc_tree, [0, max_sample.tput], plot_segment_positive) + + # a backwards delta + draw_chart (ctx, DISK_TPUT_COLOR, False, chart_rect, + [(sample.time, sample.tput) for sample in partition.samples], + proc_tree, [0, max_sample.tput], plot_segment_positive) + + # overlay write throughput + # a backwards delta + draw_chart (ctx, DISK_WRITE_COLOR, False, chart_rect, + [(sample.time, sample.write) for sample in partition.samples], + proc_tree, [0, max_sample.tput], plot_segment_positive) # pos_x = ((max_sample.time - proc_tree.start_time) * w / proc_tree.duration()) # @@ -485,7 +480,7 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h): # render mem usage chart_rect = (0, curr_y+30, w, meminfo_bar_h) mem_stats = trace.mem_stats - if mem_stats and clip_visible (clip, chart_rect): + if mem_stats: mem_scale = max(sample.records['MemTotal'] - sample.records['MemFree'] for sample in mem_stats) draw_legend_box(ctx, "Mem cached (scale: %u MiB)" % (float(mem_scale) / 1024), MEM_CACHED_COLOR, curr_y+20, leg_s) draw_legend_box(ctx, "Used", MEM_USED_COLOR, 240, curr_y+20, leg_s) @@ -537,9 +532,6 @@ def render(ctx, options, xscale, trace, isotemporal_csec = None): ctx.rectangle(0, 0, w, h) ctx.clip() - # x, y, w, h - clip = (-1, -1, -1, -1) # ctx.clip_extents() - w -= 2*off_x # draw the title and headers @@ -554,14 +546,14 @@ def render(ctx, options, xscale, trace, isotemporal_csec = None): curr_y = off_y; if options.charts: - curr_y = render_charts (ctx, options, clip, trace, curr_y, w, h) + curr_y = render_charts (ctx, options, trace, curr_y, w, h) # draw process boxes proc_height = h if proc_tree.taskstats and options.cumulative: proc_height -= CUML_HEIGHT - draw_process_bar_chart(ctx, clip, options, proc_tree, trace.times, + draw_process_bar_chart(ctx, options, proc_tree, trace.times, curr_y, w, proc_height) curr_y = proc_height @@ -569,14 +561,12 @@ def render(ctx, options, xscale, trace, isotemporal_csec = None): # draw a cumulative CPU-time-per-process graph if proc_tree.taskstats and options.cumulative: cuml_rect = (0, curr_y + off_y, w, CUML_HEIGHT/2 - off_y * 2) - if clip_visible (clip, cuml_rect): - draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_CPU) + draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_CPU) # draw a cumulative I/O-time-per-process graph if proc_tree.taskstats and options.cumulative: cuml_rect = (0, curr_y + off_y * 100, w, CUML_HEIGHT/2 - off_y * 2) - if clip_visible (clip, cuml_rect): - draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_IO) + draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_IO) if isotemporal_csec: draw_isotemporal(ctx, isotemporal_csec) @@ -592,7 +582,7 @@ def draw_isotemporal(ctx, isotemporal_csec): ctx.line_to(isotemporal_x, CUML_HEIGHT) ctx.stroke() -def draw_process_bar_chart(ctx, clip, options, proc_tree, times, curr_y, w, h): +def draw_process_bar_chart(ctx, options, proc_tree, times, curr_y, w, h): header_size = 0 if not options.kernel_only: draw_legend_diamond (ctx, "Runnable", @@ -622,7 +612,7 @@ def draw_process_bar_chart(ctx, clip, options, proc_tree, times, curr_y, w, h): y = curr_y + 60 for root in proc_tree.process_tree: - draw_processes_recursively(ctx, root, proc_tree, y, proc_h, chart_rect, clip) + draw_processes_recursively(ctx, root, proc_tree, y, proc_h, chart_rect) y = y + proc_h * proc_tree.num_nodes([root]) @@ -658,11 +648,11 @@ def draw_header (ctx, headers, duration): return header_y -def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) : +def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect) : x = csec_to_xscaled(proc.start_time) w = csec_to_xscaled(proc.start_time + proc.duration) - x # XX parser fudges duration upward - draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip) + draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect) # Do not draw right-hand vertical border -- process exit never exactly known ctx.set_source_rgba(*PROC_BORDER_COLOR) @@ -673,7 +663,7 @@ def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) : ctx.rel_line_to(w, 0) ctx.stroke() - draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip) + draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect) # Event ticks step on the rectangle painted by draw_process_state_colors() (e.g. for non-interruptible wait); # user can work around this by toggling off the event ticks. @@ -698,13 +688,13 @@ def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect, clip) : next_y = y + proc_h for child in proc.child_list: - child_x, child_y = draw_processes_recursively(ctx, child, proc_tree, next_y, proc_h, rect, clip) + child_x, child_y = draw_processes_recursively(ctx, child, proc_tree, next_y, proc_h, rect) draw_process_connecting_lines(ctx, x, y, child_x, child_y, proc_h) next_y = next_y + proc_h * proc_tree.num_nodes([child]) return x, y -def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip): +def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect): draw_fill_rect(ctx, PROC_COLOR_S, (x, y, w, proc_h)) if len(proc.samples) <= 0: return @@ -750,7 +740,7 @@ def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): y + proc_h - 4, tx) last_label_str = label_str -def draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect, clip): +def draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect): last_tx = -1 for sample in proc.samples : tx = csec_to_xscaled(sample.time) From 65499ca3a436f4cd723b5543b5d40dd007e468fe Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:07 -0800 Subject: [PATCH 081/182] delete superfluous arg --- pybootchartgui/draw.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 734a4bd..b3ecf73 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -294,9 +294,9 @@ def plot_scatter_positive(ctx, point, x, y): # All charts assumed to be full-width def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree, data_range, plot_point_func): - def transform_point_coords(point, y_base, yscale, y_trans): + def transform_point_coords(point, y_base, yscale): x = csec_to_xscaled(point[0]) - y = (point[1] - y_base) * -yscale + y_trans + chart_bounds[3] + y = (point[1] - y_base) * -yscale + chart_bounds[1] + chart_bounds[3] return x, y max_y = max (y for (x, y) in data) @@ -316,14 +316,14 @@ def transform_point_coords(point, y_base, yscale, y_trans): # move to the x of the missing first sample point first = transform_point_coords ([time_origin_drawn + in_chart_X_margin(proc_tree), -9999], - ybase, yscale, chart_bounds[1]) + ybase, yscale) ctx.move_to(first[0], first[1]) for point in data: - x, y = transform_point_coords (point, ybase, yscale, chart_bounds[1]) + x, y = transform_point_coords (point, ybase, yscale) plot_point_func(ctx, point, x, y) - final = transform_point_coords (data[-1], ybase, yscale, chart_bounds[1]) + final = transform_point_coords (data[-1], ybase, yscale) if fill: ctx.set_line_width(0.0) From b917a96fbd72bbe6aad501c8155bb794de8746cb Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:07 -0800 Subject: [PATCH 082/182] events -- log format 7 version change --- pybootchartgui/draw.py | 2 +- pybootchartgui/parsing.py | 50 +++++++++++++++++++++++---------------- pybootchartgui/samples.py | 10 ++++---- 3 files changed, 36 insertions(+), 26 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index b3ecf73..7dcb1ea 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -723,7 +723,7 @@ def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): last_label_str = None precision = int( min(6, SEC_W/100)) for ev in proc.events: - if not ev_regex.match(ev.match) and ev.match != "sample_start": + if ev.raw_log_line and not ev_regex.match(ev.raw_log_line): continue tx = csec_to_xscaled(ev.time) ctx.move_to(tx-1, y+proc_h) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 5d83294..fca3ae6 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -124,7 +124,8 @@ def find_parent_id_for(pid): proc = self.ps_stats.process_map[key] for cpu in self.cpu_stats: # assign to the init process's bar, for lack of any better - ev = EventSample(cpu.time, cpu.time*10*1000, init_pid, init_pid, "sample_start", "no raw file", -1) + ev = EventSample(cpu.time, cpu.time*10*1000, init_pid, init_pid, + "comm", "func_file_line", None) proc.events.append(ev) # merge in events @@ -630,18 +631,23 @@ def _parse_dmesg(writer, file): return processMap.values() -# -# Input resembles dmesg. Eventual output is per-process lists of events in temporal order. -# -def _parse_events_log(writer, file): +def _parse_events_log(writer, tf, file): ''' - Parse a generic log format produced by target-specific filters, which - resembles output of `dmesg`, except that the timestamp must be followed - by tid, pid, and a string common to related log messages, otherwise unformatted. - Extracting {timestamp_microseconds_from_boot, tid, pid, group_string, raw_line} from - the target-specific system logs is the responsibility of a target-specific script. + Parse a generic log format produced by target-specific filters. + Extracting the standard fields from the target-specific raw_file + is the responsibility of target-specific pre-processors. + Eventual output is per-process lists of events in temporal order. ''' - split_re = re.compile ("^(\S+) +(\S+) +(\S+) +(\S+) +(\S+) +(\S+)$") + def _readline(raw_log_filename, raw_log_seek): + file = tf.extractfile(raw_log_filename) + if not file: + return + file.seek(raw_log_seek) + line = file.readline() + file.close() + return line + + split_re = re.compile ("^(\S+) +(\S+) +(\S+) +(\S+) +(\S+) +(\S+) +(\S+)$") timed_blocks = _parse_timed_blocks(file) samples = [] for time, lines in timed_blocks: @@ -649,13 +655,17 @@ def _parse_events_log(writer, file): if line is '': continue m = split_re.match(line) + if m == None or m.lastindex < 7: # XX Ignore bad data from Java events, for now + continue time_usec = m.group(1) pid = m.group(2) tid = m.group(3) - match = m.group(4) - raw_file = m.group(5) - raw_line_number = m.group(6) - samples.append( EventSample(time, time_usec, tid, pid, match, raw_file, raw_line_number) ) + comm = m.group(4) + func_file_line = m.group(5) + raw_log_filename = m.group(6) + raw_log_seek = int(m.group(7)) + raw_log_line = _readline(raw_log_filename, raw_log_seek) + samples.append( EventSample(time, time_usec, pid, tid, comm, func_file_line, raw_log_line)) return samples # @@ -729,7 +739,7 @@ def get_num_cpus(headers): return 1 return max (int(mat.group(1)), 1) -def _do_parse(writer, state, name, file, options): +def _do_parse(writer, state, tf, name, file, options): writer.status("parsing '%s'" % name) t1 = clock() if name == "header": @@ -753,8 +763,8 @@ def _do_parse(writer, state, name, file, options): state.ps_stats = _parse_proc_ps_log(options, writer, file) elif name == "kernel_pacct": # obsoleted by PROC_EVENTS state.parent_map = _parse_pacct(writer, file) - elif name == "events-6.log": # 6 is number of fields -- a crude versioning scheme - state.events = _parse_events_log(writer, file) # writes to just-created process tree + elif name == "events-7.log": # 7 is number of fields -- a crude versioning scheme + state.events = _parse_events_log(writer, tf, file) t2 = clock() writer.info(" %s seconds" % str(t2-t1)) return state @@ -764,7 +774,7 @@ def parse_file(writer, state, filename, options): state.filename = filename basename = os.path.basename(filename) with open(filename, "rb") as file: - return _do_parse(writer, state, basename, file, options) + return _do_parse(writer, state, None, basename, file, options) def parse_paths(writer, state, paths, options): for path in paths: @@ -787,7 +797,7 @@ def parse_paths(writer, state, paths, options): writer.status("parsing '%s'" % path) tf = tarfile.open(path, 'r:*') for name in tf.getnames(): - state = _do_parse(writer, state, name, tf.extractfile(name), options) + state = _do_parse(writer, state, tf, name, tf.extractfile(name), options) except tarfile.ReadError as error: raise ParseError("error: could not read tarfile '%s': %s." % (path, error)) finally: diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 83cde9c..d4241a7 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -15,14 +15,14 @@ class EventSample: - def __init__(self, time, time_usec, tid, pid, match, raw_file, raw_line_number): + def __init__(self, time, time_usec, pid, tid, comm, func_file_line, raw_log_line): self.time = time self.time_usec = time_usec - self.tid = tid self.pid = pid - self.match = match - self.raw_file = raw_file - self.raw_line_number = raw_line_number + self.tid = tid + self.comm = comm + self.func_file_line = func_file_line + self.raw_log_line = raw_log_line class DiskStatSample: def __init__(self, time): From 0e88bdf264cd463d7677658aa9b81d5a1da00ae5 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:07 -0800 Subject: [PATCH 083/182] events -- adjust time label -- spacing --- pybootchartgui/draw.py | 60 +++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 7dcb1ea..33c4169 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -19,6 +19,7 @@ import re import random import colorsys +import traceback class RenderOptions: @@ -360,7 +361,8 @@ def in_chart_X_margin(proc_tree): # Called from gui.py and batch.py, before first call to render(), # and every time xscale changes. -# Returns size of a window capable of holding the whole scene? +# Returned (w, h) maximum useful x, y user coordinates -- minimums are 0, 0. +# (w) will get bigger if xscale does. def extents(options, xscale, trace): global OPTIONS, time_origin_drawn OPTIONS = options.app_options @@ -514,7 +516,8 @@ def render_charts(ctx, options, trace, curr_y, w, h): # "ctx" is the Cairo drawing context. ctx transform already has panning translation # and "zoom" scaling applied, but not the asymmetrical xscale arg. def render(ctx, options, xscale, trace, isotemporal_csec = None): - (w, h) = extents (options, xscale, trace) # XX redundant? + #traceback.print_stack() + (w, h) = extents (options, xscale, trace) ctx.set_line_width(1.0) ctx.select_font_face(FONT_NAME) @@ -714,31 +717,52 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect): def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): ev_regex = re.compile(OPTIONS.event_regex) + ctx.set_source_rgba(*EVENT_COLOR) + ev_list = [(ev, csec_to_xscaled(ev.time)) + if ((not ev.raw_log_line) or ev_regex.match(ev.raw_log_line)) else None + for ev in proc.events] + # draw ticks + for (ev, tx) in ev_list: + W,H = 1,5 + ctx.move_to(tx-W, y+proc_h) # bottom-left + ctx.rel_line_to(W,-H) # top + ctx.rel_line_to(W, H) # bottom-right + ctx.close_path() + ctx.fill() + + if not OPTIONS.print_event_times: + return + + # draw time labels + # XX Add support for "absolute" boot-time origin case? if ISOTEMPORAL_CSEC: time_origin_relative = ISOTEMPORAL_CSEC else: - time_origin_relative = time_origin_drawn + proc_tree.sample_period # XX align to time of first sample - ctx.set_source_rgba(*EVENT_COLOR) + # align to time of first sample + time_origin_relative = time_origin_drawn + proc_tree.sample_period + spacing = ctx.text_extents("00")[2] last_x_touched = 0 last_label_str = None - precision = int( min(6, SEC_W/100)) - for ev in proc.events: - if ev.raw_log_line and not ev_regex.match(ev.raw_log_line): + for (ev, tx) in ev_list: + if tx < last_x_touched + spacing: continue - tx = csec_to_xscaled(ev.time) - ctx.move_to(tx-1, y+proc_h) - ctx.line_to(tx, y+proc_h-5) - ctx.line_to(tx+1, y+proc_h) - ctx.line_to(tx, y+proc_h) - ctx.fill() - if OPTIONS.print_event_times and tx > last_x_touched + 5: - label_str = '%.*f' % (precision, (float(ev.time_usec)/1000/10 - time_origin_relative) / CSEC) - if label_str != last_label_str: - last_x_touched = tx + draw_label_in_box_at_time( + + delta = float(ev.time_usec)/1000/10 - time_origin_relative + if ISOTEMPORAL_CSEC: + if abs(delta) < CSEC: + label_str = '{0:3d}'.format(int(delta*10)) + else: + label_str = '{0:.{prec}f}'.format(delta/CSEC, + prec=min(3, abs( int(3*CSEC/delta)))) + else: + label_str = '{0:.{prec}f}'.format(delta/CSEC, + prec=min(3, max(0, int(SEC_W/100)))) + if label_str != last_label_str: + last_x_touched = tx + draw_label_in_box_at_time( ctx, PROC_TEXT_COLOR, label_str, y + proc_h - 4, tx) - last_label_str = label_str + last_label_str = label_str def draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect): last_tx = -1 From 9f990190dbcc1c3ce3ccdaac14148c1bc44474f9 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:08 -0800 Subject: [PATCH 084/182] dump event on button down --- pybootchartgui/draw.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 33c4169..ded4731 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -509,6 +509,7 @@ def render_charts(ctx, options, trace, curr_y, w, h): return curr_y ISOTEMPORAL_CSEC = None +ISOTEMPORAL_render_serial = None # # Render the chart. @@ -523,8 +524,12 @@ def render(ctx, options, xscale, trace, isotemporal_csec = None): ctx.select_font_face(FONT_NAME) draw_fill_rect(ctx, WHITE, (0, 0, max(w, MIN_IMG_W), h)) - global ISOTEMPORAL_CSEC - ISOTEMPORAL_CSEC = isotemporal_csec + global render_serial + if isotemporal_csec: + global ISOTEMPORAL_CSEC + ISOTEMPORAL_CSEC = isotemporal_csec + global ISOTEMPORAL_render_serial + ISOTEMPORAL_render_serial = render_serial ctx.save() ctx.translate(off_x, 0) # current window-coord clip shrinks with loss of the off_x-wide strip on left @@ -574,8 +579,12 @@ def render(ctx, options, xscale, trace, isotemporal_csec = None): if isotemporal_csec: draw_isotemporal(ctx, isotemporal_csec) + render_serial += 1 + ctx.restore() +render_serial = 0 + def draw_isotemporal(ctx, isotemporal_csec): ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) ctx.set_line_width(0.8) @@ -749,6 +758,9 @@ def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): delta = float(ev.time_usec)/1000/10 - time_origin_relative if ISOTEMPORAL_CSEC: + if ISOTEMPORAL_render_serial == render_serial and \ + abs(delta*SEC_W) < 1*sec_w_base: + print(ev.raw_log_line) if abs(delta) < CSEC: label_str = '{0:3d}'.format(int(delta*10)) else: From bfb85430f71ecae7b629c9f0b592bc86852a683f Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:08 -0800 Subject: [PATCH 085/182] usec event positions --- pybootchartgui/draw.py | 8 ++++++-- pybootchartgui/parsing.py | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index ded4731..b5712ff 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -724,10 +724,14 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect): last_time = sample.time ctx.restore() +def usec_to_csec(usec): + '''would drop precision without the float() cast''' + return float(usec) / 1000 / 10 + def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): ev_regex = re.compile(OPTIONS.event_regex) ctx.set_source_rgba(*EVENT_COLOR) - ev_list = [(ev, csec_to_xscaled(ev.time)) + ev_list = [(ev, csec_to_xscaled(usec_to_csec(ev.time_usec))) if ((not ev.raw_log_line) or ev_regex.match(ev.raw_log_line)) else None for ev in proc.events] # draw ticks @@ -758,7 +762,7 @@ def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): delta = float(ev.time_usec)/1000/10 - time_origin_relative if ISOTEMPORAL_CSEC: - if ISOTEMPORAL_render_serial == render_serial and \ + if ev.raw_log_line and ISOTEMPORAL_render_serial == render_serial and \ abs(delta*SEC_W) < 1*sec_w_base: print(ev.raw_log_line) if abs(delta) < CSEC: diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index fca3ae6..8ad0e36 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -657,9 +657,9 @@ def _readline(raw_log_filename, raw_log_seek): m = split_re.match(line) if m == None or m.lastindex < 7: # XX Ignore bad data from Java events, for now continue - time_usec = m.group(1) - pid = m.group(2) - tid = m.group(3) + time_usec = long(m.group(1)) + pid = int(m.group(2)) + tid = int(m.group(3)) comm = m.group(4) func_file_line = m.group(5) raw_log_filename = m.group(6) From 62232faeb821b093661f65cecaf92bdd672d5a17 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:08 -0800 Subject: [PATCH 086/182] write status to stderr --- pybootchartgui/main.py.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index c503c4a..a35712a 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -143,7 +143,7 @@ class Writer: def _mk_writer(options): def write(s): - print(s) + sys.stderr.write(s + "\n") return Writer(write, options) def _get_filename(paths, options): From 3d0383ad91283f10e26908747948fea81e09deb6 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:08 -0800 Subject: [PATCH 087/182] rename -- ISOTEMPORAL to SWEEP --- pybootchartgui/draw.py | 36 ++++++++++++++++++------------------ pybootchartgui/gui.py | 8 ++++---- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index b5712ff..e59cc62 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -508,15 +508,15 @@ def render_charts(ctx, options, trace, curr_y, w, h): return curr_y -ISOTEMPORAL_CSEC = None -ISOTEMPORAL_render_serial = None +SWEEP_CSEC = None +SWEEP_render_serial = None # # Render the chart. # # "ctx" is the Cairo drawing context. ctx transform already has panning translation # and "zoom" scaling applied, but not the asymmetrical xscale arg. -def render(ctx, options, xscale, trace, isotemporal_csec = None): +def render(ctx, options, xscale, trace, sweep_csec = None): #traceback.print_stack() (w, h) = extents (options, xscale, trace) @@ -525,11 +525,11 @@ def render(ctx, options, xscale, trace, isotemporal_csec = None): draw_fill_rect(ctx, WHITE, (0, 0, max(w, MIN_IMG_W), h)) global render_serial - if isotemporal_csec: - global ISOTEMPORAL_CSEC - ISOTEMPORAL_CSEC = isotemporal_csec - global ISOTEMPORAL_render_serial - ISOTEMPORAL_render_serial = render_serial + if sweep_csec: + global SWEEP_CSEC + SWEEP_CSEC = sweep_csec + global SWEEP_render_serial + SWEEP_render_serial = render_serial ctx.save() ctx.translate(off_x, 0) # current window-coord clip shrinks with loss of the off_x-wide strip on left @@ -576,8 +576,8 @@ def render(ctx, options, xscale, trace, isotemporal_csec = None): cuml_rect = (0, curr_y + off_y * 100, w, CUML_HEIGHT/2 - off_y * 2) draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_IO) - if isotemporal_csec: - draw_isotemporal(ctx, isotemporal_csec) + if sweep_csec: + draw_sweep(ctx, sweep_csec) render_serial += 1 @@ -585,13 +585,13 @@ def render(ctx, options, xscale, trace, isotemporal_csec = None): render_serial = 0 -def draw_isotemporal(ctx, isotemporal_csec): +def draw_sweep(ctx, sweep_csec): ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) ctx.set_line_width(0.8) ctx.set_dash([4, 2]) - isotemporal_x = csec_to_xscaled(isotemporal_csec) - ctx.move_to(isotemporal_x, 0) - ctx.line_to(isotemporal_x, CUML_HEIGHT) + sweep_x = csec_to_xscaled(sweep_csec) + ctx.move_to(sweep_x, 0) + ctx.line_to(sweep_x, CUML_HEIGHT) ctx.stroke() def draw_process_bar_chart(ctx, options, proc_tree, times, curr_y, w, h): @@ -748,8 +748,8 @@ def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): # draw time labels # XX Add support for "absolute" boot-time origin case? - if ISOTEMPORAL_CSEC: - time_origin_relative = ISOTEMPORAL_CSEC + if SWEEP_CSEC: + time_origin_relative = SWEEP_CSEC else: # align to time of first sample time_origin_relative = time_origin_drawn + proc_tree.sample_period @@ -761,8 +761,8 @@ def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): continue delta = float(ev.time_usec)/1000/10 - time_origin_relative - if ISOTEMPORAL_CSEC: - if ev.raw_log_line and ISOTEMPORAL_render_serial == render_serial and \ + if SWEEP_CSEC: + if ev.raw_log_line and SWEEP_render_serial == render_serial and \ abs(delta*SEC_W) < 1*sec_w_base: print(ev.raw_log_line) if abs(delta) < CSEC: diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index dffa036..dfda95f 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -58,7 +58,7 @@ def __init__(self, trace, options, xscale): self.hadj_changed_signal_id = None self.vadj_changed_signal_id = None - self.isotemporal_csec = None + self.sweep_csec = None def do_expose_event(self, event): # XX called on mouse entering or leaving window -- can these be disabled? cr = self.window.cairo_create() @@ -73,7 +73,7 @@ def draw(self, cr): cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) cr.paint() # fill whole DrawingArea with white self.cr_set_up_transform(cr) - draw.render(cr, self.options, self.xscale, self.trace, self.isotemporal_csec) + draw.render(cr, self.options, self.xscale, self.trace, self.sweep_csec) def position_changed(self): self.emit("position-changed", self.x, self.y) @@ -179,10 +179,10 @@ def on_area_button_press(self, area, event): self.prevmousex = event.x self.prevmousey = event.y if event.button == 2: - self.isotemporal_csec, uy = self.device_to_csec_user_y(event.x, 0) + self.sweep_csec, uy = self.device_to_csec_user_y(event.x, 0) self.queue_draw() if event.button == 3: - self.isotemporal_csec = None + self.sweep_csec = None self.queue_draw() if event.type not in (gtk.gdk.BUTTON_PRESS, gtk.gdk.BUTTON_RELEASE): return False From d489d94dacddbe8306fcc517653dbf6a69f0f402 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:09 -0800 Subject: [PATCH 088/182] event dump and labeling fixes --- pybootchartgui/draw.py | 64 +++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index e59cc62..6ee28d9 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with pybootchartgui. If not, see . - +import sys import cairo import math import re @@ -72,7 +72,8 @@ def proc_tree (self, trace): # Disk throughput color. DISK_TPUT_COLOR = (0.20, 0.71, 0.20, 1.0) # Disk throughput color. -DISK_WRITE_COLOR = (0.7, 0.0, 0.7, 1.0) +MAGENTA = (0.7, 0.0, 0.7, 1.0) +DISK_WRITE_COLOR = MAGENTA # Mem cached color MEM_CACHED_COLOR = CPU_COLOR # Mem used color @@ -526,10 +527,10 @@ def render(ctx, options, xscale, trace, sweep_csec = None): global render_serial if sweep_csec: - global SWEEP_CSEC - SWEEP_CSEC = sweep_csec - global SWEEP_render_serial - SWEEP_render_serial = render_serial + global SWEEP_CSEC, SWEEP_render_serial + if SWEEP_CSEC != sweep_csec: + SWEEP_render_serial = render_serial + SWEEP_CSEC = sweep_csec ctx.save() ctx.translate(off_x, 0) # current window-coord clip shrinks with loss of the off_x-wide strip on left @@ -626,7 +627,10 @@ def draw_process_bar_chart(ctx, options, proc_tree, times, curr_y, w, h): for root in proc_tree.process_tree: draw_processes_recursively(ctx, root, proc_tree, y, proc_h, chart_rect) y = y + proc_h * proc_tree.num_nodes([root]) - + if SWEEP_CSEC and SWEEP_render_serial == render_serial: + # mark end of this batch of events, for the benefit of post-processors + print + sys.stdout.flush() def draw_header (ctx, headers, duration): toshow = [ @@ -730,48 +734,56 @@ def usec_to_csec(usec): def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): ev_regex = re.compile(OPTIONS.event_regex) - ctx.set_source_rgba(*EVENT_COLOR) ev_list = [(ev, csec_to_xscaled(usec_to_csec(ev.time_usec))) if ((not ev.raw_log_line) or ev_regex.match(ev.raw_log_line)) else None for ev in proc.events] - # draw ticks + if not ev_list: + return + if SWEEP_CSEC: + time_origin_relative = SWEEP_CSEC + else: + # XX Add support for "absolute" boot-time origin case? + # align to time of first sample + time_origin_relative = time_origin_drawn + proc_tree.sample_period + + # draw ticks, maybe dump log line for (ev, tx) in ev_list: - W,H = 1,5 + delta = ev.time_usec/1000/10 - time_origin_relative + if ev.raw_log_line and \ + abs(ctx.user_to_device_distance(delta * SEC_W / CSEC, 0)[0]) < 10: + if SWEEP_CSEC and SWEEP_render_serial == render_serial: + print ev.raw_log_line, + ctx.set_source_rgba(*MAGENTA) + W,H = 2,8 + else: + ctx.set_source_rgba(*EVENT_COLOR) + W,H = 1,5 ctx.move_to(tx-W, y+proc_h) # bottom-left ctx.rel_line_to(W,-H) # top ctx.rel_line_to(W, H) # bottom-right ctx.close_path() ctx.fill() + # draw numbers if not OPTIONS.print_event_times: return - - # draw time labels - # XX Add support for "absolute" boot-time origin case? - if SWEEP_CSEC: - time_origin_relative = SWEEP_CSEC - else: - # align to time of first sample - time_origin_relative = time_origin_drawn + proc_tree.sample_period + ctx.set_source_rgba(*EVENT_COLOR) spacing = ctx.text_extents("00")[2] last_x_touched = 0 last_label_str = None for (ev, tx) in ev_list: if tx < last_x_touched + spacing: continue - - delta = float(ev.time_usec)/1000/10 - time_origin_relative + delta = ev.time_usec/1000/10 - time_origin_relative if SWEEP_CSEC: - if ev.raw_log_line and SWEEP_render_serial == render_serial and \ - abs(delta*SEC_W) < 1*sec_w_base: - print(ev.raw_log_line) if abs(delta) < CSEC: label_str = '{0:3d}'.format(int(delta*10)) else: - label_str = '{0:.{prec}f}'.format(delta/CSEC, - prec=min(3, abs( int(3*CSEC/delta)))) + label_str = '{0:.{prec}f}'.format(float(delta)/CSEC, + prec=min(3, max(1, abs(int(3*CSEC/delta))))) else: - label_str = '{0:.{prec}f}'.format(delta/CSEC, + # format independent of delta + label_str = '{0:.{prec}f}'.format(float(delta)/CSEC, prec=min(3, max(0, int(SEC_W/100)))) if label_str != last_label_str: last_x_touched = tx + draw_label_in_box_at_time( From 5dcc5e055df0b7017b67e4fef6d2028f57dc08b1 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:09 -0800 Subject: [PATCH 089/182] refactor -- options object and SWEEP --- pybootchartgui/draw.py | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 6ee28d9..2be5c67 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -21,8 +21,8 @@ import colorsys import traceback +# XX Generalize this, to hold all global drawing state? class RenderOptions: - def __init__(self, app_options): # should we render a cumulative CPU time chart self.cumulative = True @@ -191,7 +191,7 @@ def draw_legend_line(ctx, label, fill_color, x, y, s): def draw_label_in_box(ctx, color, label, x, y, w, minx, maxx): label_w = ctx.text_extents(label)[2] - if OPTIONS.justify == JUSTIFY_LEFT: + if OPTIONS.app_options.justify == JUSTIFY_LEFT: label_x = x else: label_x = x + w / 2 - label_w / 2 # CENTER @@ -350,11 +350,17 @@ def transform_point_coords(point, y_base, yscale): leg_s = 11 MIN_IMG_W = 800 CUML_HEIGHT = 2000 # Increased value to accomodate CPU and I/O Graphs + +# Variables OPTIONS = None SEC_W = None time_origin_drawn = None # time of leftmost plotted data +SWEEP_CSEC = None +SWEEP_render_serial = None +render_serial = 0 + # window coords def in_chart_X_margin(proc_tree): @@ -366,10 +372,10 @@ def in_chart_X_margin(proc_tree): # (w) will get bigger if xscale does. def extents(options, xscale, trace): global OPTIONS, time_origin_drawn - OPTIONS = options.app_options + OPTIONS = options proc_tree = options.proc_tree(trace) - if OPTIONS.prehistory: + if OPTIONS.app_options.prehistory: time_origin_drawn = 0 # XX Would have to be process_tree.starttime for backwards compatibility else: time_origin_drawn = trace.cpu_stats[0].time - in_chart_X_margin(proc_tree) @@ -424,7 +430,7 @@ def render_charts(ctx, options, trace, curr_y, w, h): # render second chart draw_legend_box(ctx, "Disk utilization -- fraction of sample interval I/O queue was not empty", IO_COLOR, 0, curr_y+20, leg_s) - if OPTIONS.show_ops_not_bytes: + if OPTIONS.app_options.show_ops_not_bytes: unit = "ops" else: unit = "bytes" @@ -509,15 +515,15 @@ def render_charts(ctx, options, trace, curr_y, w, h): return curr_y -SWEEP_CSEC = None -SWEEP_render_serial = None - # # Render the chart. # -# "ctx" is the Cairo drawing context. ctx transform already has panning translation -# and "zoom" scaling applied, but not the asymmetrical xscale arg. def render(ctx, options, xscale, trace, sweep_csec = None): + ''' + "ctx" is the Cairo drawing context -- the transform matrix it carries already has + panning translation and "zoom" scaling applied, but not the asymmetrical "xscale" arg. + "options" is a RenderOptions object. + ''' #traceback.print_stack() (w, h) = extents (options, xscale, trace) @@ -584,8 +590,6 @@ def render(ctx, options, xscale, trace, sweep_csec = None): ctx.restore() -render_serial = 0 - def draw_sweep(ctx, sweep_csec): ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) ctx.set_line_width(0.8) @@ -683,17 +687,17 @@ def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect) : # Event ticks step on the rectangle painted by draw_process_state_colors() (e.g. for non-interruptible wait); # user can work around this by toggling off the event ticks. - if not OPTIONS.hide_events: + if not OPTIONS.app_options.hide_events: draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect) ipid = int(proc.pid) - if proc_tree.taskstats and OPTIONS.show_all: + if proc_tree.taskstats and OPTIONS.app_options.show_all: cmdString = '' else: cmdString = proc.cmd - if (OPTIONS.show_pid or OPTIONS.show_all) and ipid is not 0: + if (OPTIONS.app_options.show_pid or OPTIONS.app_options.show_all) and ipid is not 0: cmdString = cmdString + " [" + str(ipid / 1000) + "]" - if OPTIONS.show_all: + if OPTIONS.app_options.show_all: if proc.args: cmdString = cmdString + " '" + "' '".join(proc.args) + "'" else: @@ -733,7 +737,7 @@ def usec_to_csec(usec): return float(usec) / 1000 / 10 def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): - ev_regex = re.compile(OPTIONS.event_regex) + ev_regex = re.compile(OPTIONS.app_options.event_regex) ev_list = [(ev, csec_to_xscaled(usec_to_csec(ev.time_usec))) if ((not ev.raw_log_line) or ev_regex.match(ev.raw_log_line)) else None for ev in proc.events] @@ -765,7 +769,7 @@ def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): ctx.fill() # draw numbers - if not OPTIONS.print_event_times: + if not OPTIONS.app_options.print_event_times: return ctx.set_source_rgba(*EVENT_COLOR) spacing = ctx.text_extents("00")[2] From 00b8d174e3cdafa9678d9f7c4814c2e0f4862486 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:09 -0800 Subject: [PATCH 090/182] process label cosmetics --- pybootchartgui/draw.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 2be5c67..ee729d4 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -40,6 +40,7 @@ def proc_tree (self, trace): BACK_COLOR = (1.0, 1.0, 1.0, 1.0) WHITE = (1.0, 1.0, 1.0, 1.0) +NOTEPAD_YELLLOW = (0.95, 0.95, 0.8, 1.0) # Process tree border color. BORDER_COLOR = (0.63, 0.63, 0.63, 1.0) # Second tick line color. @@ -189,8 +190,17 @@ def draw_legend_line(ctx, label, fill_color, x, y, s): ctx.fill() draw_text(ctx, label, TEXT_COLOR, x + s + 5, y) -def draw_label_in_box(ctx, color, label, x, y, w, minx, maxx): - label_w = ctx.text_extents(label)[2] +def draw_label_in_box(ctx, color, label, x, y, w, proc_h, minx, maxx): + # hack in a pair of left and right margins + extents = ctx.text_extents("j" + label + "k") # XX "j", "k" were found by tedious trial-and-error + label = " " + label + + label_y_bearing = extents[1] + label_w = extents[2] + label_height = extents[3] + label_y_advance = extents[5] + + y += proc_h if OPTIONS.app_options.justify == JUSTIFY_LEFT: label_x = x else: @@ -202,7 +212,9 @@ def draw_label_in_box(ctx, color, label, x, y, w, minx, maxx): label_x = x - label_w - 5 # push outside to the left if label_x < minx: label_x = minx - draw_text(ctx, label, color, label_x, y) + # XX ugly magic constants, tuned by trial-and-error + draw_fill_rect(ctx, NOTEPAD_YELLLOW, (label_x, y-1, label_w, -(proc_h-2))) + draw_text(ctx, label, color, label_x, y-4) def draw_label_in_box_at_time(ctx, color, label, y, label_x): draw_text(ctx, label, color, label_x, y) @@ -703,7 +715,7 @@ def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect) : else: cmdString = cmdString - draw_label_in_box(ctx, PROC_TEXT_COLOR, cmdString, x, y + proc_h - 4, w, + draw_label_in_box(ctx, PROC_TEXT_COLOR, cmdString, x, y, w, proc_h, ctx.clip_extents()[0], ctx.clip_extents()[2]) next_y = y + proc_h From f7f2ac69d99d9c5b912f44d64c79e4ccb020ca37 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:09 -0800 Subject: [PATCH 091/182] refactor -- remove unused arg -- rect --- pybootchartgui/draw.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index ee729d4..ee19750 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -631,7 +631,6 @@ def draw_process_bar_chart(ctx, options, proc_tree, times, curr_y, w, h): chart_rect = [-1, -1, -1, -1] ctx.set_font_size (PROC_TEXT_FONT_SIZE) - draw_box_ticks (ctx, chart_rect) if SEC_W > 100: nsec = 1 else: @@ -641,7 +640,7 @@ def draw_process_bar_chart(ctx, options, proc_tree, times, curr_y, w, h): y = curr_y + 60 for root in proc_tree.process_tree: - draw_processes_recursively(ctx, root, proc_tree, y, proc_h, chart_rect) + draw_processes_recursively(ctx, root, proc_tree, y, proc_h) y = y + proc_h * proc_tree.num_nodes([root]) if SWEEP_CSEC and SWEEP_render_serial == render_serial: # mark end of this batch of events, for the benefit of post-processors @@ -680,11 +679,11 @@ def draw_header (ctx, headers, duration): return header_y -def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect) : +def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h): x = csec_to_xscaled(proc.start_time) w = csec_to_xscaled(proc.start_time + proc.duration) - x # XX parser fudges duration upward - draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect) + draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h) # Do not draw right-hand vertical border -- process exit never exactly known ctx.set_source_rgba(*PROC_BORDER_COLOR) @@ -695,12 +694,12 @@ def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect) : ctx.rel_line_to(w, 0) ctx.stroke() - draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect) + draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h) # Event ticks step on the rectangle painted by draw_process_state_colors() (e.g. for non-interruptible wait); # user can work around this by toggling off the event ticks. if not OPTIONS.app_options.hide_events: - draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect) + draw_process_events(ctx, proc, proc_tree, x, y, proc_h) ipid = int(proc.pid) if proc_tree.taskstats and OPTIONS.app_options.show_all: @@ -720,13 +719,13 @@ def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h, rect) : next_y = y + proc_h for child in proc.child_list: - child_x, child_y = draw_processes_recursively(ctx, child, proc_tree, next_y, proc_h, rect) + child_x, child_y = draw_processes_recursively(ctx, child, proc_tree, next_y, proc_h) draw_process_connecting_lines(ctx, x, y, child_x, child_y, proc_h) next_y = next_y + proc_h * proc_tree.num_nodes([child]) return x, y -def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect): +def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h): draw_fill_rect(ctx, PROC_COLOR_S, (x, y, w, proc_h)) if len(proc.samples) <= 0: return @@ -748,7 +747,7 @@ def usec_to_csec(usec): '''would drop precision without the float() cast''' return float(usec) / 1000 / 10 -def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): +def draw_process_events(ctx, proc, proc_tree, x, y, proc_h): ev_regex = re.compile(OPTIONS.app_options.event_regex) ev_list = [(ev, csec_to_xscaled(usec_to_csec(ev.time_usec))) if ((not ev.raw_log_line) or ev_regex.match(ev.raw_log_line)) else None @@ -808,7 +807,7 @@ def draw_process_events(ctx, proc, proc_tree, x, y, proc_h, rect): y + proc_h - 4, tx) last_label_str = label_str -def draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h, rect): +def draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h): last_tx = -1 for sample in proc.samples : tx = csec_to_xscaled(sample.time) From 215b522189797b64b1e5a0fc4ed7ac239b407f8b Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:09 -0800 Subject: [PATCH 092/182] FIX -- work around numeric overflow at high xscale factors --- pybootchartgui/draw.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index ee19750..5215892 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -223,6 +223,9 @@ def draw_label_in_box_at_time(ctx, color, label, y, label_x): def csec_to_xscaled(t_csec): return (t_csec-time_origin_drawn) * SEC_W / CSEC +def csec_to_xscaled_distance(dist_csec): + return dist_csec * SEC_W / CSEC + # Solve for t_csec: # x = (t_csec-time_origin_drawn) * SEC_W / CSEC + off_x # @@ -555,6 +558,7 @@ def render(ctx, options, xscale, trace, sweep_csec = None): proc_tree = options.proc_tree (trace) + # clip off left-hand side of process bars ctx.new_path() ctx.rectangle(0, 0, w, h) ctx.clip() @@ -680,8 +684,9 @@ def draw_header (ctx, headers, duration): return header_y def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h): - x = csec_to_xscaled(proc.start_time) - w = csec_to_xscaled(proc.start_time + proc.duration) - x # XX parser fudges duration upward + xmin, ymin = ctx.device_to_user(0, 0) # work around numeric overflow at high xscale factors + x = max(xmin, csec_to_xscaled(proc.start_time)) + w = max(xmin, csec_to_xscaled(proc.start_time + proc.duration)) - x # XX parser fudges duration upward draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h) @@ -714,8 +719,10 @@ def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h): else: cmdString = cmdString - draw_label_in_box(ctx, PROC_TEXT_COLOR, cmdString, x, y, w, proc_h, - ctx.clip_extents()[0], ctx.clip_extents()[2]) + draw_label_in_box(ctx, PROC_TEXT_COLOR, cmdString, + csec_to_xscaled(proc.start_time), y, + w, proc_h, + max(0, xmin), ctx.clip_extents()[2]) next_y = y + proc_h for child in proc.child_list: From 21190a75ec473a54672f7aac3ad2b6c581925e42 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:10 -0800 Subject: [PATCH 093/182] show sweep dump window beginning and end --- pybootchartgui/draw.py | 62 ++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 5215892..c810692 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -370,7 +370,7 @@ def transform_point_coords(point, y_base, yscale): OPTIONS = None SEC_W = None -time_origin_drawn = None # time of leftmost plotted data +time_origin_drawn = None # time of leftmost plotted data, as integer csecs SWEEP_CSEC = None SWEEP_render_serial = None @@ -600,20 +600,40 @@ def render(ctx, options, xscale, trace, sweep_csec = None): draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_IO) if sweep_csec: - draw_sweep(ctx, sweep_csec) + width_sec = sweep_window_width_sec(ctx) + draw_sweep(ctx, sweep_csec, width_sec * CSEC) + #dump_pseudo_event(ctx, "start of event window, width " + int(width*1000) + "msec") render_serial += 1 ctx.restore() -def draw_sweep(ctx, sweep_csec): - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_line_width(0.8) - ctx.set_dash([4, 2]) - sweep_x = csec_to_xscaled(sweep_csec) - ctx.move_to(sweep_x, 0) - ctx.line_to(sweep_x, CUML_HEIGHT) - ctx.stroke() +def sweep_window_width_sec(ctx): + '''about half the width of the visible part of the per-process bars''' + user_width = ctx.device_to_user_distance(500,0)[0] + return float(user_width) / SEC_W + +def draw_sweep(ctx, sweep_csec, width_csec): + def draw_shading(ctx, rect): + ctx.set_source_rgba(0.0, 0.0, 0.0, 0.1) + ctx.set_line_width(0.0) + ctx.rectangle(rect) + ctx.fill() + def draw_vertical(ctx, x): + ctx.set_dash([1, 3]) + ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + ctx.set_line_width(1.0) + ctx.move_to(x, 0) + ctx.line_to(x, CUML_HEIGHT) + ctx.stroke() + + x = csec_to_xscaled(sweep_csec) + draw_shading(ctx, (int(x),0,int(ctx.clip_extents()[0]-x),CUML_HEIGHT)) + draw_vertical(ctx, x) + + x = csec_to_xscaled(sweep_csec + width_csec) + draw_shading(ctx, (int(x),0,int(ctx.clip_extents()[2]-x),CUML_HEIGHT)) + draw_vertical(ctx, x) def draw_process_bar_chart(ctx, options, proc_tree, times, curr_y, w, h): header_size = 0 @@ -768,18 +788,18 @@ def draw_process_events(ctx, proc, proc_tree, x, y, proc_h): # align to time of first sample time_origin_relative = time_origin_drawn + proc_tree.sample_period + width_csec = sweep_window_width_sec(ctx) * CSEC # draw ticks, maybe dump log line for (ev, tx) in ev_list: - delta = ev.time_usec/1000/10 - time_origin_relative - if ev.raw_log_line and \ - abs(ctx.user_to_device_distance(delta * SEC_W / CSEC, 0)[0]) < 10: - if SWEEP_CSEC and SWEEP_render_serial == render_serial: - print ev.raw_log_line, - ctx.set_source_rgba(*MAGENTA) - W,H = 2,8 - else: - ctx.set_source_rgba(*EVENT_COLOR) - W,H = 1,5 + ctx.set_source_rgba(*EVENT_COLOR) + W,H = 1,5 + if SWEEP_CSEC and ev.raw_log_line: + delta_csec = float(ev.time_usec)/1000/10 - time_origin_relative + if delta_csec >= 0 and delta_csec < width_csec: + # ctx.set_source_rgba(*MAGENTA) + # W,H = 2,8 + if SWEEP_render_serial == render_serial: + print ev.raw_log_line, ctx.move_to(tx-W, y+proc_h) # bottom-left ctx.rel_line_to(W,-H) # top ctx.rel_line_to(W, H) # bottom-right @@ -796,7 +816,7 @@ def draw_process_events(ctx, proc, proc_tree, x, y, proc_h): for (ev, tx) in ev_list: if tx < last_x_touched + spacing: continue - delta = ev.time_usec/1000/10 - time_origin_relative + delta = float(ev.time_usec)/1000/10 - time_origin_relative if SWEEP_CSEC: if abs(delta) < CSEC: label_str = '{0:3d}'.format(int(delta*10)) From 15183899cc3db7996df66fa90ee8a2193ec67bfe Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:10 -0800 Subject: [PATCH 094/182] add absolute event time option 2.py --- pybootchartgui/draw.py | 3 ++- pybootchartgui/gui.py | 9 +++++++++ pybootchartgui/main.py.in | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index c810692..3129251 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -783,8 +783,9 @@ def draw_process_events(ctx, proc, proc_tree, x, y, proc_h): return if SWEEP_CSEC: time_origin_relative = SWEEP_CSEC + elif OPTIONS.app_options.absolute_uptime_event_times: + time_origin_relative = 0 else: - # XX Add support for "absolute" boot-time origin case? # align to time of first sample time_origin_relative = time_origin_drawn + proc_tree.sample_period diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index dfda95f..6e07371 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -156,6 +156,10 @@ def print_event_times(self, button): self.options.app_options.print_event_times = button.get_property ('active') self.queue_draw() + def absolute_uptime_event_times(self, button): + self.options.app_options.absolute_uptime_event_times = button.get_property ('active') + self.queue_draw() + POS_INCREMENT = 100 def on_key_press_event(self, widget, event): @@ -366,6 +370,11 @@ def gtk_CheckButton(name): button.set_active (options.app_options.print_event_times) hbox.pack_start (button, False) + button = gtk_CheckButton("event Times Absolute") + button.connect ('toggled', self.widget.absolute_uptime_event_times) + button.set_active (options.app_options.absolute_uptime_event_times) + hbox.pack_start (button, False) + self.pack_start(hbox, False) self.pack_start(scrolled) self.show_all() diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index a35712a..6c4e73e 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -86,6 +86,8 @@ def _mk_options_parser(): help="hide event ticks (small black triangles)") parser.add_option("--print-event-times", action="store_true", dest="print_event_times", default=False, help="print time of each event, inside the box of the reporting process") + parser.add_option("--absolute-uptime-event-times", action="store_true", dest="absolute_uptime_event_times", default=False, + help="print time of each event, inside the box of the reporting process") parser.add_option("--synthesize-sample-start-events", action="store_true", dest="synthesize_sample_start_events", help="synthesize an event marking each boundary between sample periods -- " + "helpful in analyzing collector timing issues") From 3c78da77bcc95a722995506e735cba23add51c8b Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:10 -0800 Subject: [PATCH 095/182] refactor -- extents a pure function -- constants immutable -- create DrawContext object --- pybootchartgui/batch.py | 12 +- pybootchartgui/draw.py | 814 +++++++++++++++++++++------------------- pybootchartgui/gui.py | 90 +++-- 3 files changed, 473 insertions(+), 443 deletions(-) diff --git a/pybootchartgui/batch.py b/pybootchartgui/batch.py index 05c714e..93c05d9 100644 --- a/pybootchartgui/batch.py +++ b/pybootchartgui/batch.py @@ -15,7 +15,7 @@ import cairo from . import draw -from .draw import RenderOptions +from .draw import DrawContext def render(writer, trace, app_options, filename): handlers = { @@ -35,12 +35,12 @@ def render(writer, trace, app_options, filename): return 10 make_surface, write_surface = handlers[fmt] - options = RenderOptions (app_options) - (w, h) = draw.extents (options, 1.0, trace) - w = max (w, draw.MIN_IMG_W) + drawctx = DrawContext (app_options, trace) + (w, h) = draw.extents (drawctx, 1.0, trace) + w = max (w, draw.C.MIN_IMG_W) surface = make_surface (w, h) - ctx = cairo.Context (surface) - draw.render (ctx, options, 1.0, trace) + cr = cairo.Context (surface) + draw.render (cr, drawctx, 1.0, trace) write_surface (surface) writer.status ("bootchart written to '%s'" % filename) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 3129251..48bffcd 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -20,21 +20,19 @@ import random import colorsys import traceback +import collections -# XX Generalize this, to hold all global drawing state? -class RenderOptions: - def __init__(self, app_options): - # should we render a cumulative CPU time chart - self.cumulative = True - self.charts = True - self.kernel_only = False - self.app_options = app_options +# Constants: Put the more heavily used, non-derived constants in a named tuple, for immutability. +# XX The syntax is awkward, but more elegant alternatives have run-time overhead. +# http://stackoverflow.com/questions/4996815/ways-to-make-a-class-immutable-in-python +DrawConsts = collections.namedtuple('XXtypename', + ['CSEC','bar_h','off_x','off_y','proc_h','leg_s','CUML_HEIGHT','MIN_IMG_W']) +C = DrawConsts( 100, 55, 10, 10, 16, 11, 2000, 800) +# height of a process, in user-space - def proc_tree (self, trace): - if self.kernel_only: - return trace.kernel_tree - else: - return trace.proc_tree +# Derived constants +# XX create another namedtuple for these +meminfo_bar_h = 2 * C.bar_h # Process tree background color. BACK_COLOR = (1.0, 1.0, 1.0, 1.0) @@ -149,170 +147,215 @@ def proc_tree (self, trace): def get_proc_state(flag): return "RSDTZXW".find(flag) + 1 -def draw_text(ctx, text, color, x, y): - ctx.set_source_rgba(*color) - ctx.move_to(x, y) - ctx.show_text(text) - -def draw_fill_rect(ctx, color, rect): - ctx.set_source_rgba(*color) - ctx.rectangle(*rect) - ctx.fill() - -def draw_rect(ctx, color, rect): - ctx.set_source_rgba(*color) - ctx.rectangle(*rect) - ctx.stroke() - -def draw_diamond(ctx, x, y, w, h): - ctx.save() - ctx.set_line_width(0.0) - ctx.move_to(x-w/2, y) - ctx.line_to(x, y+h/2) - ctx.line_to(x+w/2, y) - ctx.line_to(x, y-h/2) - ctx.line_to(x-w/2, y) - ctx.fill() - ctx.restore() - -def draw_legend_diamond(ctx, label, fill_color, x, y, w, h): - ctx.set_source_rgba(*fill_color) - draw_diamond(ctx, x, y-h/2, w, h) - draw_text(ctx, label, TEXT_COLOR, x + w + 5, y) - -def draw_legend_box(ctx, label, fill_color, x, y, s): - draw_fill_rect(ctx, fill_color, (x, y - s, s, s)) - #draw_rect(ctx, PROC_BORDER_COLOR, (x, y - s, s, s)) - draw_text(ctx, label, TEXT_COLOR, x + s + 5, y) - -def draw_legend_line(ctx, label, fill_color, x, y, s): - draw_fill_rect(ctx, fill_color, (x, y - s/2, s + 1, 3)) - ctx.fill() - draw_text(ctx, label, TEXT_COLOR, x + s + 5, y) - -def draw_label_in_box(ctx, color, label, x, y, w, proc_h, minx, maxx): - # hack in a pair of left and right margins - extents = ctx.text_extents("j" + label + "k") # XX "j", "k" were found by tedious trial-and-error - label = " " + label - - label_y_bearing = extents[1] - label_w = extents[2] - label_height = extents[3] - label_y_advance = extents[5] - - y += proc_h - if OPTIONS.app_options.justify == JUSTIFY_LEFT: - label_x = x +def draw_text(cr, text, color, x, y): + cr.set_source_rgba(*color) + cr.move_to(x, y) + cr.show_text(text) + +def draw_fill_rect(cr, color, rect): + cr.set_source_rgba(*color) + cr.rectangle(*rect) + cr.fill() + +def draw_rect(cr, color, rect): + cr.set_source_rgba(*color) + cr.rectangle(*rect) + cr.stroke() + +def draw_diamond(cr, x, y, w, h): + cr.save() + cr.set_line_width(0.0) + cr.move_to(x-w/2, y) + cr.line_to(x, y+h/2) + cr.line_to(x+w/2, y) + cr.line_to(x, y-h/2) + cr.line_to(x-w/2, y) + cr.fill() + cr.restore() + +def draw_legend_diamond(cr, label, fill_color, x, y, w, h): + cr.set_source_rgba(*fill_color) + draw_diamond(cr, x, y-h/2, w, h) + draw_text(cr, label, TEXT_COLOR, x + w + 5, y) + +def draw_legend_box(cr, label, fill_color, x, y, s): + draw_fill_rect(cr, fill_color, (x, y - s, s, s)) + #draw_rect(cr, PROC_BORDER_COLOR, (x, y - s, s, s)) + draw_text(cr, label, TEXT_COLOR, x + s + 5, y) + +def draw_legend_line(cr, label, fill_color, x, y, s): + draw_fill_rect(cr, fill_color, (x, y - s/2, s + 1, 3)) + cr.fill() + draw_text(cr, label, TEXT_COLOR, x + s + 5, y) + +def draw_label_in_box_at_time(cr, color, label, y, label_x): + draw_text(cr, label, color, label_x, y) + return cr.text_extents(label)[2] + + +def _time_origin_drawn(ctx, trace): + if ctx.app_options.prehistory: + # XX Would have to set to proc_tree.starttime for backwards compatibility + return 0 else: - label_x = x + w / 2 - label_w / 2 # CENTER + return trace.cpu_stats[0].time - in_chart_X_margin(ctx.proc_tree(trace)) + +def _sec_w(xscale): + return xscale * 50 # width of a second, in user-space - if label_w + 10 > w: # if wider than the process box - label_x = x + w + 5 # push outside to right - if label_x + label_w > maxx: # if that's too far right - label_x = x - label_w - 5 # push outside to the left - if label_x < minx: - label_x = minx - # XX ugly magic constants, tuned by trial-and-error - draw_fill_rect(ctx, NOTEPAD_YELLLOW, (label_x, y-1, label_w, -(proc_h-2))) - draw_text(ctx, label, color, label_x, y-4) +# XX Migrating functions into the drawing object seems to be a loss: the +# replacement of 'ctx' with 'self' is no help to the reader, and +# additional text columns are lost to the necessary indentation +class DrawContext: + '''set all drawing-related variables bindable at time of class PyBootchartWindow instantiation''' + def __init__(self, app_options, trace, cumulative = True, charts = True, kernel_only = False): + self.app_options = app_options + # should we render a cumulative CPU time chart + self.cumulative = cumulative # Bootchart2 collector only + self.charts = charts + self.kernel_only = kernel_only # set iff collector daemon saved output of `dmesg` + self.SWEEP_CSEC = None + self.SWEEP_render_serial = None + self.render_serial = 0 + + self.cr = None # Cairo rendering context + self.time_origin_drawn = None # time of leftmost plotted data, as integer csecs + self.SEC_W = None + + def per_render_init(self, cr, time_origin_drawn, SEC_W): + self.cr = cr + self.time_origin_drawn = time_origin_drawn + self.SEC_W = SEC_W -def draw_label_in_box_at_time(ctx, color, label, y, label_x): - draw_text(ctx, label, color, label_x, y) - return ctx.text_extents(label)[2] + def proc_tree (self, trace): + return trace.kernel_tree if self.kernel_only else trace.proc_tree -def csec_to_xscaled(t_csec): - return (t_csec-time_origin_drawn) * SEC_W / CSEC + def draw_label_in_box(self, color, label, x, y, w, minx, maxx): + # hack in a pair of left and right margins + extents = self.cr.text_extents("j" + label + "k") # XX "j", "k" were found by tedious trial-and-error + label = " " + label -def csec_to_xscaled_distance(dist_csec): - return dist_csec * SEC_W / CSEC + label_y_bearing = extents[1] + label_w = extents[2] + label_height = extents[3] + label_y_advance = extents[5] + + y += C.proc_h + if self.app_options.justify == JUSTIFY_LEFT: + label_x = x + else: + label_x = x + w / 2 - label_w / 2 # CENTER + + if label_w + 10 > w: # if wider than the process box + label_x = x + w + 5 # push outside to right + if label_x + label_w > maxx: # if that's too far right + label_x = x - label_w - 5 # push outside to the left + if label_x < minx: + label_x = minx + # XX ugly magic constants, tuned by trial-and-error + draw_fill_rect(self.cr, NOTEPAD_YELLLOW, (label_x, y-1, label_w, -(C.proc_h-2))) + draw_text(self.cr, label, color, label_x, y-4) + +# XX Should be "_csec_to_user" +# Assume off_x translation is already applied, as it will be in drawing functions. +def _csec_to_xscaled(t_csec, time_origin_drawn, sec_w): + return (t_csec-time_origin_drawn) * sec_w / C.CSEC + +def csec_to_xscaled(ctx, t_csec): + return _csec_to_xscaled(t_csec, ctx.time_origin_drawn, ctx.SEC_W) # Solve for t_csec: -# x = (t_csec-time_origin_drawn) * SEC_W / CSEC + off_x +# x = (t_csec-ctx.time_origin_drawn) * ctx.SEC_W / C.CSEC + C.off_x # -# x - off_x = (t_csec-time_origin_drawn) * SEC_W / CSEC -# (x - off_x) * CSEC / SEC_W = t_csec-time_origin_drawn +# x - C.off_x = (t_csec-ctx.time_origin_drawn) * ctx.SEC_W / C.CSEC +# (x - C.off_x) * C.CSEC / ctx.SEC_W = t_csec-ctx.time_origin_drawn # -def xscaled_to_csec(x): - return (x - off_x) * CSEC / SEC_W + time_origin_drawn +def xscaled_to_csec(ctx, x): + return _xscaled_to_csec(x, ctx.SEC_W, ctx.time_origin_drawn) + +# XX Prefix with single underbar '_' -- double underbar '__' has special meaning to Python +# that precludes use in exported functions. +def _xscaled_to_csec(x, sec_w, _time_origin_drawn): + return (x - C.off_x) * C.CSEC / sec_w + _time_origin_drawn def ctx_save__csec_to_xscaled(ctx): - ctx.save() - ctx.scale(float(SEC_W) / CSEC, 1.0) - ctx.translate(-time_origin_drawn, 0.0) + ctx.cr.save() + ctx.cr.scale(float(ctx.SEC_W) / C.CSEC, 1.0) + ctx.cr.translate(-ctx.time_origin_drawn, 0.0) def draw_sec_labels(ctx, rect, nsecs): - ctx.set_font_size(AXIS_FONT_SIZE) + ctx.cr.set_font_size(AXIS_FONT_SIZE) prev_x = 0 - for i in range(0, rect[2] + 1, SEC_W): - if ((i / SEC_W) % nsecs == 0) : - label = "%ds" % (i / SEC_W) - label_w = ctx.text_extents(label)[2] + for i in range(0, rect[2] + 1, ctx.SEC_W): + if ((i / ctx.SEC_W) % nsecs == 0) : + label = "%ds" % (i / ctx.SEC_W) + label_w = ctx.cr.text_extents(label)[2] x = rect[0] + i - label_w/2 if x >= prev_x: - draw_text(ctx, label, TEXT_COLOR, x, rect[1] - 2) + draw_text(ctx.cr, label, TEXT_COLOR, x, rect[1] - 2) prev_x = x + label_w def draw_box_ticks(ctx, rect): - draw_rect(ctx, BORDER_COLOR, tuple(rect)) + draw_rect(ctx.cr, BORDER_COLOR, tuple(rect)) return - ctx.set_line_cap(cairo.LINE_CAP_SQUARE) + ctx.cr.set_line_cap(cairo.LINE_CAP_SQUARE) - for i in range(SEC_W, rect[2] + 1, SEC_W): - if ((i / SEC_W) % 5 == 0) : - ctx.set_source_rgba(*TICK_COLOR_BOLD) + for i in range(ctx.SEC_W, rect[2] + 1, ctx.SEC_W): + if ((i / ctx.SEC_W) % 5 == 0) : + ctx.cr.set_source_rgba(*TICK_COLOR_BOLD) else : - ctx.set_source_rgba(*TICK_COLOR) - ctx.move_to(rect[0] + i, rect[1] + 1) - ctx.line_to(rect[0] + i, rect[1] + rect[3] - 1) - ctx.stroke() + ctx.cr.set_source_rgba(*TICK_COLOR) + ctx.cr.move_to(rect[0] + i, rect[1] + 1) + ctx.cr.line_to(rect[0] + i, rect[1] + rect[3] - 1) + ctx.cr.stroke() - ctx.set_line_cap(cairo.LINE_CAP_BUTT) + ctx.cr.set_line_cap(cairo.LINE_CAP_BUTT) def draw_annotations(ctx, proc_tree, times, rect): - ctx.set_line_cap(cairo.LINE_CAP_SQUARE) - ctx.set_source_rgba(*ANNOTATION_COLOR) - ctx.set_dash([4, 4]) + ctx.cr.set_line_cap(cairo.LINE_CAP_SQUARE) + ctx.cr.set_source_rgba(*ANNOTATION_COLOR) + ctx.cr.set_dash([4, 4]) for time in times: if time is not None: - x = csec_to_xscaled(time) + x = csec_to_xscaled(ctx, time) - ctx.move_to(x, rect[1] + 1) - ctx.line_to(x, rect[1] + rect[3] - 1) - ctx.stroke() + ctx.cr.move_to(x, rect[1] + 1) + ctx.cr.line_to(x, rect[1] + rect[3] - 1) + ctx.cr.stroke() - ctx.set_line_cap(cairo.LINE_CAP_BUTT) - ctx.set_dash([]) + ctx.cr.set_line_cap(cairo.LINE_CAP_BUTT) + ctx.cr.set_dash([]) -def plot_line(ctx, point, x, y): - ctx.set_line_width(1.0) - ctx.line_to(x, y) # rightward, and upward or downward +def plot_line(cr, point, x, y): + cr.set_line_width(1.0) + cr.line_to(x, y) # rightward, and upward or downward # backward-looking -def plot_square(ctx, point, x, y): - ctx.set_line_width(1.0) - ctx.line_to(ctx.get_current_point()[0], y) # upward or downward - ctx.line_to(x, y) # rightward +def plot_square(cr, point, x, y): + cr.set_line_width(1.0) + cr.line_to(cr.get_current_point()[0], y) # upward or downward + cr.line_to(x, y) # rightward # backward-looking -def plot_segment_positive(ctx, point, x, y): - ctx.move_to(ctx.get_current_point()[0], y) # upward or downward +def plot_segment_positive(cr, point, x, y): + cr.move_to(cr.get_current_point()[0], y) # upward or downward if point[1] <= 0: # zero-Y samples draw nothing - ctx.move_to(x, y) + cr.move_to(x, y) return - ctx.set_line_width(1.5) - ctx.line_to(x, y) + cr.set_line_width(1.5) + cr.line_to(x, y) -def plot_scatter_positive(ctx, point, x, y): +def plot_scatter_positive(cr, point, x, y): if point[1] <= 0: return - draw_diamond(ctx, x, y, 3.6, 3.6) + draw_diamond(cr, x, y, 3.6, 3.6) # All charts assumed to be full-width def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree, data_range, plot_point_func): def transform_point_coords(point, y_base, yscale): - x = csec_to_xscaled(point[0]) + x = csec_to_xscaled(ctx, point[0]) y = (point[1] - y_base) * -yscale + chart_bounds[1] + chart_bounds[3] return x, y @@ -329,96 +372,69 @@ def transform_point_coords(point, y_base, yscale): yscale = float(chart_bounds[3]) / max_y ybase = 0 - ctx.set_source_rgba(*color) + ctx.cr.set_source_rgba(*color) # move to the x of the missing first sample point - first = transform_point_coords ([time_origin_drawn + in_chart_X_margin(proc_tree), -9999], + first = transform_point_coords ([ctx.time_origin_drawn + in_chart_X_margin(proc_tree), -9999], ybase, yscale) - ctx.move_to(first[0], first[1]) + ctx.cr.move_to(first[0], first[1]) for point in data: x, y = transform_point_coords (point, ybase, yscale) - plot_point_func(ctx, point, x, y) + plot_point_func(ctx.cr, point, x, y) final = transform_point_coords (data[-1], ybase, yscale) if fill: - ctx.set_line_width(0.0) - ctx.stroke_preserve() - ctx.line_to(final[0], chart_bounds[1]+chart_bounds[3]) - ctx.line_to(first[0], chart_bounds[1]+chart_bounds[3]) - ctx.line_to(first[0], first[1]) - ctx.fill() + ctx.cr.set_line_width(0.0) + ctx.cr.stroke_preserve() + ctx.cr.line_to(final[0], chart_bounds[1]+chart_bounds[3]) + ctx.cr.line_to(first[0], chart_bounds[1]+chart_bounds[3]) + ctx.cr.line_to(first[0], first[1]) + ctx.cr.fill() else: - ctx.stroke() - ctx.set_line_width(1.0) - -# Constants -# XX put all of constants in a named tuple, for immutability -CSEC = 100 -bar_h = 55 -meminfo_bar_h = 2 * bar_h -# offsets -off_x, off_y = 10, 10 -sec_w_base = 50 # the width of a second -proc_h = 16 # the height of a process -leg_s = 11 -MIN_IMG_W = 800 -CUML_HEIGHT = 2000 # Increased value to accomodate CPU and I/O Graphs - -# Variables -OPTIONS = None - -SEC_W = None -time_origin_drawn = None # time of leftmost plotted data, as integer csecs - -SWEEP_CSEC = None -SWEEP_render_serial = None -render_serial = 0 - -# window coords + ctx.cr.stroke() + ctx.cr.set_line_width(1.0) def in_chart_X_margin(proc_tree): return proc_tree.sample_period -# Called from gui.py and batch.py, before first call to render(), -# and every time xscale changes. +# A _pure_ function of its arguments -- writes to no global state nor object. +# Called from gui.py and batch.py, before instantiation of +# DrawContext and first call to render(), then every time xscale +# changes. # Returned (w, h) maximum useful x, y user coordinates -- minimums are 0, 0. # (w) will get bigger if xscale does. -def extents(options, xscale, trace): - global OPTIONS, time_origin_drawn - OPTIONS = options +def extents(ctx, xscale, trace): + '''arg "options" is a RenderOptions object''' + proc_tree = ctx.proc_tree(trace) - proc_tree = options.proc_tree(trace) - if OPTIONS.app_options.prehistory: - time_origin_drawn = 0 # XX Would have to be process_tree.starttime for backwards compatibility - else: - time_origin_drawn = trace.cpu_stats[0].time - in_chart_X_margin(proc_tree) - global SEC_W - SEC_W = xscale * sec_w_base + w = int (_csec_to_xscaled(trace.cpu_stats[-1].time + in_chart_X_margin(proc_tree), + _time_origin_drawn(ctx, trace), + _sec_w(xscale)) \ + + 2*C.off_x) - w = int (csec_to_xscaled(trace.cpu_stats[-1].time + in_chart_X_margin(proc_tree)) + 2*off_x) - h = proc_h * proc_tree.num_proc + 2 * off_y - if options.charts: - h += 110 + (2 + len(trace.disk_stats)) * (30 + bar_h) + 1 * (30 + meminfo_bar_h) - if proc_tree.taskstats and options.cumulative: - h += CUML_HEIGHT + 4 * off_y - return (w, h) # includes off_x, off_y + h = C.proc_h * proc_tree.num_proc + 2 * C.off_y + if ctx.charts: + h += 110 + (2 + len(trace.disk_stats)) * (30 + C.bar_h) + 1 * (30 + meminfo_bar_h) + if proc_tree.taskstats and ctx.cumulative: + h += C.CUML_HEIGHT + 4 * C.off_y + return (w, h) # includes C.off_x, C.off_y -def render_charts(ctx, options, trace, curr_y, w, h): - proc_tree = options.proc_tree(trace) +def render_charts(ctx, trace, curr_y, w, h): + proc_tree = ctx.proc_tree(trace) # render bar legend - ctx.set_font_size(LEGEND_FONT_SIZE) + ctx.cr.set_font_size(LEGEND_FONT_SIZE) - draw_legend_box(ctx, "CPU (user+sys)", CPU_COLOR, 0, curr_y+20, leg_s) - draw_legend_box(ctx, "I/O (wait)", IO_COLOR, 120, curr_y+20, leg_s) - draw_legend_diamond(ctx, "Runnable threads", PROCS_RUNNING_COLOR, - 120 +90, curr_y+20, leg_s, leg_s) - draw_legend_diamond(ctx, "Blocked threads -- Uninterruptible Syscall", PROCS_BLOCKED_COLOR, - 120 +90 +140, curr_y+20, leg_s, leg_s) + draw_legend_box(ctx.cr, "CPU (user+sys)", CPU_COLOR, 0, curr_y+20, C.leg_s) + draw_legend_box(ctx.cr, "I/O (wait)", IO_COLOR, 120, curr_y+20, C.leg_s) + draw_legend_diamond(ctx.cr, "Runnable threads", PROCS_RUNNING_COLOR, + 120 +90, curr_y+20, C.leg_s, C.leg_s) + draw_legend_diamond(ctx.cr, "Blocked threads -- Uninterruptible Syscall", PROCS_BLOCKED_COLOR, + 120 +90 +140, curr_y+20, C.leg_s, C.leg_s) - chart_rect = (0, curr_y+30, w, bar_h) + chart_rect = (0, curr_y+30, w, C.bar_h) draw_box_ticks (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) # render I/O wait -- a backwards delta @@ -440,19 +456,19 @@ def render_charts(ctx, options, trace, curr_y, w, h): [(sample.time, sample.procs_blocked) for sample in trace.cpu_stats], \ proc_tree, [0, 9], plot_scatter_positive) - curr_y = curr_y + 50 + bar_h + curr_y = curr_y + 50 + C.bar_h # render second chart - draw_legend_box(ctx, "Disk utilization -- fraction of sample interval I/O queue was not empty", - IO_COLOR, 0, curr_y+20, leg_s) - if OPTIONS.app_options.show_ops_not_bytes: + draw_legend_box(ctx.cr, "Disk utilization -- fraction of sample interval I/O queue was not empty", + IO_COLOR, 0, curr_y+20, C.leg_s) + if ctx.app_options.show_ops_not_bytes: unit = "ops" else: unit = "bytes" - draw_legend_line(ctx, "Disk writes -- " + unit + "/sample", - DISK_WRITE_COLOR, 470, curr_y+20, leg_s) - draw_legend_line(ctx, "Disk reads+writes -- " + unit + "/sample", - DISK_TPUT_COLOR, 470+120*2, curr_y+20, leg_s) + draw_legend_line(ctx.cr, "Disk writes -- " + unit + "/sample", + DISK_WRITE_COLOR, 470, curr_y+20, C.leg_s) + draw_legend_line(ctx.cr, "Disk reads+writes -- " + unit + "/sample", + DISK_TPUT_COLOR, 470+120*2, curr_y+20, C.leg_s) curr_y += 5 @@ -461,10 +477,10 @@ def render_charts(ctx, options, trace, curr_y, w, h): # render I/O utilization for partition in trace.disk_stats: - draw_text(ctx, partition.name, TEXT_COLOR, 0, curr_y+30) + draw_text(ctx.cr, partition.name, TEXT_COLOR, 0, curr_y+30) # utilization -- inherently normalized [0,1] - chart_rect = (0, curr_y+30+5, w, bar_h) + chart_rect = (0, curr_y+30+5, w, C.bar_h) draw_box_ticks (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) # a backwards delta @@ -499,20 +515,20 @@ def render_charts(ctx, options, trace, curr_y, w, h): # label = "%.1fMB/s" % round ((max_sample.tput) / DISK_BLOCK_SIZE) # draw_text (ctx, label, DISK_TPUT_COLOR, pos_x + shift_x, curr_y + shift_y) - curr_y = curr_y + 30 + bar_h + curr_y = curr_y + 30 + C.bar_h # render mem usage chart_rect = (0, curr_y+30, w, meminfo_bar_h) mem_stats = trace.mem_stats if mem_stats: mem_scale = max(sample.records['MemTotal'] - sample.records['MemFree'] for sample in mem_stats) - draw_legend_box(ctx, "Mem cached (scale: %u MiB)" % (float(mem_scale) / 1024), MEM_CACHED_COLOR, curr_y+20, leg_s) - draw_legend_box(ctx, "Used", MEM_USED_COLOR, 240, curr_y+20, leg_s) - draw_legend_box(ctx, "Buffers", MEM_BUFFERS_COLOR, 360, curr_y+20, leg_s) - draw_legend_line(ctx, "Swap (scale: %u MiB)" % max([(sample.records['SwapTotal'] - sample.records['SwapFree'])/1024 for sample in mem_stats]), \ - MEM_SWAP_COLOR, 480, curr_y+20, leg_s) - draw_box_ticks(ctx, chart_rect) - draw_annotations(ctx, proc_tree, trace.times, chart_rect) + draw_legend_box(ctx.cr, "Mem cached (scale: %u MiB)" % (float(mem_scale) / 1024), MEM_CACHED_COLOR, curr_y+20, C.leg_s) + draw_legend_box(ctx.cr, "Used", MEM_USED_COLOR, 240, curr_y+20, C.leg_s) + draw_legend_box(ctx.cr, "Buffers", MEM_BUFFERS_COLOR, 360, curr_y+20, C.leg_s) + draw_legend_line(ctx.cr, "Swap (scale: %u MiB)" % max([(sample.records['SwapTotal'] - sample.records['SwapFree'])/1024 for sample in mem_stats]), \ + MEM_SWAP_COLOR, 480, curr_y+20, C.leg_s) + draw_box_ticks (ctx, chart_rect) + draw_annotations (ctx, proc_tree, trace.times, chart_rect) draw_chart(ctx, MEM_BUFFERS_COLOR, True, chart_rect, \ [(sample.time, sample.records['MemTotal'] - sample.records['MemFree']) for sample in trace.mem_stats], \ proc_tree, [0, mem_scale], plot_square) @@ -530,40 +546,44 @@ def render_charts(ctx, options, trace, curr_y, w, h): return curr_y +def late_init_transform(cr): + cr.translate(C.off_x, 0) # current window-coord clip shrinks with loss of the C.off_x-wide strip on left + # -# Render the chart. +# Render the chart. Central method of this module. # -def render(ctx, options, xscale, trace, sweep_csec = None): +def render(cr, ctx, xscale, trace, sweep_csec = None): ''' - "ctx" is the Cairo drawing context -- the transform matrix it carries already has - panning translation and "zoom" scaling applied, but not the asymmetrical "xscale" arg. - "options" is a RenderOptions object. + "cr" is the Cairo drawing context -- the transform matrix it carries already has + panning translation and "zoom" scaling applied. + The asymmetrical "xscale" arg is not applied globally to "cr", because + it would distort letterforms of text output. + "ctx" is a DrawContext object. ''' #traceback.print_stack() - (w, h) = extents (options, xscale, trace) + (w, h) = extents(ctx, xscale, trace) - ctx.set_line_width(1.0) - ctx.select_font_face(FONT_NAME) - draw_fill_rect(ctx, WHITE, (0, 0, max(w, MIN_IMG_W), h)) + ctx.per_render_init(cr, _time_origin_drawn(ctx, trace), _sec_w(xscale)) - global render_serial - if sweep_csec: - global SWEEP_CSEC, SWEEP_render_serial - if SWEEP_CSEC != sweep_csec: - SWEEP_render_serial = render_serial - SWEEP_CSEC = sweep_csec + ctx.cr.set_line_width(1.0) + ctx.cr.select_font_face(FONT_NAME) + draw_fill_rect(ctx.cr, WHITE, (0, 0, max(w, C.MIN_IMG_W), h)) + + if sweep_csec and sweep_csec != ctx.SWEEP_CSEC: + ctx.SWEEP_render_serial = ctx.render_serial + ctx.SWEEP_CSEC = sweep_csec - ctx.save() - ctx.translate(off_x, 0) # current window-coord clip shrinks with loss of the off_x-wide strip on left + ctx.cr.save() + late_init_transform(ctx.cr) - proc_tree = options.proc_tree (trace) + proc_tree = ctx.proc_tree (trace) # clip off left-hand side of process bars - ctx.new_path() - ctx.rectangle(0, 0, w, h) - ctx.clip() + ctx.cr.new_path() + ctx.cr.rectangle(0, 0, w, h) + ctx.cr.clip() - w -= 2*off_x + w -= 2*C.off_x # draw the title and headers if proc_tree.idle: @@ -571,102 +591,102 @@ def render(ctx, options, xscale, trace, sweep_csec = None): else: duration = proc_tree.duration() - if not options.kernel_only: + if not ctx.kernel_only: curr_y = draw_header (ctx, trace.headers, duration) else: - curr_y = off_y; + curr_y = C.off_y; - if options.charts: - curr_y = render_charts (ctx, options, trace, curr_y, w, h) + if ctx.charts: + curr_y = render_charts (ctx, trace, curr_y, w, h) # draw process boxes proc_height = h - if proc_tree.taskstats and options.cumulative: - proc_height -= CUML_HEIGHT + if proc_tree.taskstats and ctx.cumulative: + proc_height -= C.CUML_HEIGHT - draw_process_bar_chart(ctx, options, proc_tree, trace.times, + draw_process_bar_chart(ctx, proc_tree, trace.times, curr_y, w, proc_height) curr_y = proc_height # draw a cumulative CPU-time-per-process graph - if proc_tree.taskstats and options.cumulative: - cuml_rect = (0, curr_y + off_y, w, CUML_HEIGHT/2 - off_y * 2) + if proc_tree.taskstats and ctx.cumulative: + cuml_rect = (0, curr_y + C.off_y, w, C.CUML_HEIGHT/2 - C.off_y * 2) draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_CPU) # draw a cumulative I/O-time-per-process graph - if proc_tree.taskstats and options.cumulative: - cuml_rect = (0, curr_y + off_y * 100, w, CUML_HEIGHT/2 - off_y * 2) + if proc_tree.taskstats and ctx.cumulative: + cuml_rect = (0, curr_y + C.off_y * 100, w, C.CUML_HEIGHT/2 - C.off_y * 2) draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_IO) if sweep_csec: width_sec = sweep_window_width_sec(ctx) - draw_sweep(ctx, sweep_csec, width_sec * CSEC) + draw_sweep(ctx, sweep_csec, width_sec * C.CSEC) #dump_pseudo_event(ctx, "start of event window, width " + int(width*1000) + "msec") - render_serial += 1 + ctx.render_serial += 1 - ctx.restore() + ctx.cr.restore() def sweep_window_width_sec(ctx): '''about half the width of the visible part of the per-process bars''' - user_width = ctx.device_to_user_distance(500,0)[0] - return float(user_width) / SEC_W + user_width = ctx.cr.device_to_user_distance(500,0)[0] + return float(user_width) / ctx.SEC_W def draw_sweep(ctx, sweep_csec, width_csec): - def draw_shading(ctx, rect): - ctx.set_source_rgba(0.0, 0.0, 0.0, 0.1) - ctx.set_line_width(0.0) - ctx.rectangle(rect) - ctx.fill() - def draw_vertical(ctx, x): - ctx.set_dash([1, 3]) - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_line_width(1.0) - ctx.move_to(x, 0) - ctx.line_to(x, CUML_HEIGHT) - ctx.stroke() - - x = csec_to_xscaled(sweep_csec) - draw_shading(ctx, (int(x),0,int(ctx.clip_extents()[0]-x),CUML_HEIGHT)) - draw_vertical(ctx, x) - - x = csec_to_xscaled(sweep_csec + width_csec) - draw_shading(ctx, (int(x),0,int(ctx.clip_extents()[2]-x),CUML_HEIGHT)) - draw_vertical(ctx, x) - -def draw_process_bar_chart(ctx, options, proc_tree, times, curr_y, w, h): + def draw_shading(cr, rect): + cr.set_source_rgba(0.0, 0.0, 0.0, 0.1) + cr.set_line_width(0.0) + cr.rectangle(rect) + cr.fill() + def draw_vertical(cr, x): + cr.set_dash([1, 3]) + cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) + cr.set_line_width(1.0) + cr.move_to(x, 0) + cr.line_to(x, C.CUML_HEIGHT) + cr.stroke() + + x = csec_to_xscaled(ctx, sweep_csec) + draw_shading(ctx.cr, (int(x),0,int(ctx.cr.clip_extents()[0]-x),C.CUML_HEIGHT)) + draw_vertical(ctx.cr, x) + + x = csec_to_xscaled(ctx, sweep_csec + width_csec) + draw_shading(ctx.cr, (int(x),0,int(ctx.cr.clip_extents()[2]-x),C.CUML_HEIGHT)) + draw_vertical(ctx.cr, x) + +def draw_process_bar_chart(ctx, proc_tree, times, curr_y, w, h): header_size = 0 - if not options.kernel_only: - draw_legend_diamond (ctx, "Runnable", - PROCS_RUNNING_COLOR, 10, curr_y + 45, leg_s*3/4, proc_h) - draw_legend_diamond (ctx, "Uninterruptible Syscall", - PROC_COLOR_D, 10+100, curr_y + 45, leg_s*3/4, proc_h) - draw_legend_box (ctx, "Running (%cpu)", - PROC_COLOR_R, 10+100+180, curr_y + 45, leg_s) - draw_legend_box (ctx, "Sleeping", - PROC_COLOR_S, 10+100+180+130, curr_y + 45, leg_s) - draw_legend_box (ctx, "Zombie", - PROC_COLOR_Z, 10+100+180+130+90, curr_y + 45, leg_s) + if not ctx.kernel_only: + draw_legend_diamond (ctx.cr, "Runnable", + PROCS_RUNNING_COLOR, 10, curr_y + 45, C.leg_s*3/4, C.proc_h) + draw_legend_diamond (ctx.cr, "Uninterruptible Syscall", + PROC_COLOR_D, 10+100, curr_y + 45, C.leg_s*3/4, C.proc_h) + draw_legend_box (ctx.cr, "Running (%cpu)", + PROC_COLOR_R, 10+100+180, curr_y + 45, C.leg_s) + draw_legend_box (ctx.cr, "Sleeping", + PROC_COLOR_S, 10+100+180+130, curr_y + 45, C.leg_s) + draw_legend_box (ctx.cr, "Zombie", + PROC_COLOR_Z, 10+100+180+130+90, curr_y + 45, C.leg_s) header_size = 45 #chart_rect = [0, curr_y + header_size + 30, - # w, h - 2 * off_y - (curr_y + header_size + 15) + proc_h] + # w, h - 2 * C.off_y - (curr_y + header_size + 15) + C.proc_h] chart_rect = [-1, -1, -1, -1] - ctx.set_font_size (PROC_TEXT_FONT_SIZE) + ctx.cr.set_font_size (PROC_TEXT_FONT_SIZE) - if SEC_W > 100: + if ctx.SEC_W > 100: nsec = 1 else: nsec = 5 - #draw_sec_labels (ctx, chart_rect, nsec) + #draw_sec_labels (ctx.cr, chart_rect, nsec) draw_annotations (ctx, proc_tree, times, chart_rect) y = curr_y + 60 for root in proc_tree.process_tree: - draw_processes_recursively(ctx, root, proc_tree, y, proc_h) - y = y + proc_h * proc_tree.num_nodes([root]) - if SWEEP_CSEC and SWEEP_render_serial == render_serial: + draw_processes_recursively(ctx, root, proc_tree, y) + y = y + C.proc_h * proc_tree.num_nodes([root]) + if ctx.SWEEP_CSEC and ctx.SWEEP_render_serial == ctx.render_serial: # mark end of this batch of events, for the benefit of post-processors print sys.stdout.flush() @@ -679,81 +699,83 @@ def draw_header (ctx, headers, duration): ('system.kernel.options', 'kernel options', lambda s: s), ] - header_y = ctx.font_extents()[2] + 10 - ctx.set_font_size(TITLE_FONT_SIZE) - draw_text(ctx, headers['title'], TEXT_COLOR, 0, header_y) - ctx.set_font_size(TEXT_FONT_SIZE) + cr = ctx.cr + header_y = cr.font_extents()[2] + 10 + cr.set_font_size(TITLE_FONT_SIZE) + draw_text(cr, headers['title'], TEXT_COLOR, 0, header_y) + cr.set_font_size(TEXT_FONT_SIZE) for (headerkey, headertitle, mangle) in toshow: - header_y += ctx.font_extents()[2] + header_y += cr.font_extents()[2] if headerkey in headers: value = headers.get(headerkey) else: value = "" txt = headertitle + ': ' + mangle(value) - draw_text(ctx, txt, TEXT_COLOR, 0, header_y) + draw_text(cr, txt, TEXT_COLOR, 0, header_y) # dur = duration / 100.0 # txt = 'time : %02d:%05.2f' % (math.floor(dur/60), dur - 60 * math.floor(dur/60)) # if headers.get('system.maxpid') is not None: # txt = txt + ' max pid: %s' % (headers.get('system.maxpid')) # -# header_y += ctx.font_extents()[2] -# draw_text (ctx, txt, TEXT_COLOR, 0, header_y) +# header_y += cr.font_extents()[2] +# draw_text (cr, txt, TEXT_COLOR, 0, header_y) return header_y -def draw_processes_recursively(ctx, proc, proc_tree, y, proc_h): - xmin, ymin = ctx.device_to_user(0, 0) # work around numeric overflow at high xscale factors - x = max(xmin, csec_to_xscaled(proc.start_time)) - w = max(xmin, csec_to_xscaled(proc.start_time + proc.duration)) - x # XX parser fudges duration upward +def draw_processes_recursively(ctx, proc, proc_tree, y): + xmin, ymin = ctx.cr.device_to_user(0, 0) # work around numeric overflow at high xscale factors + x = max(xmin, csec_to_xscaled(ctx, proc.start_time)) + w = max(xmin, csec_to_xscaled(ctx, proc.start_time + proc.duration)) - x # XX parser fudges duration upward - draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h) + draw_process_activity_colors(ctx, proc, proc_tree, x, y, w) # Do not draw right-hand vertical border -- process exit never exactly known - ctx.set_source_rgba(*PROC_BORDER_COLOR) - ctx.set_line_width(1.0) - ctx.move_to(x+w, y) - ctx.rel_line_to(-w, 0) - ctx.rel_line_to(0, proc_h) - ctx.rel_line_to(w, 0) - ctx.stroke() - - draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h) - - # Event ticks step on the rectangle painted by draw_process_state_colors() (e.g. for non-interruptible wait); - # user can work around this by toggling off the event ticks. - if not OPTIONS.app_options.hide_events: - draw_process_events(ctx, proc, proc_tree, x, y, proc_h) + ctx.cr.set_source_rgba(*PROC_BORDER_COLOR) + ctx.cr.set_line_width(1.0) + ctx.cr.move_to(x+w, y) + ctx.cr.rel_line_to(-w, 0) + ctx.cr.rel_line_to(0, C.proc_h) + ctx.cr.rel_line_to(w, 0) + ctx.cr.stroke() + + draw_process_state_colors(ctx, proc, proc_tree, x, y, w) + + # Event ticks step on the rectangle painted by draw_process_state_colors(), + # e.g. for non-interruptible wait. + # User can work around this by toggling off the event ticks. + if not ctx.app_options.hide_events: + draw_process_events(ctx, proc, proc_tree, x, y) ipid = int(proc.pid) - if proc_tree.taskstats and OPTIONS.app_options.show_all: + if proc_tree.taskstats and ctx.app_options.show_all: cmdString = '' else: cmdString = proc.cmd - if (OPTIONS.app_options.show_pid or OPTIONS.app_options.show_all) and ipid is not 0: + if (ctx.app_options.show_pid or ctx.app_options.show_all) and ipid is not 0: cmdString = cmdString + " [" + str(ipid / 1000) + "]" - if OPTIONS.app_options.show_all: + if ctx.app_options.show_all: if proc.args: cmdString = cmdString + " '" + "' '".join(proc.args) + "'" else: cmdString = cmdString - draw_label_in_box(ctx, PROC_TEXT_COLOR, cmdString, - csec_to_xscaled(proc.start_time), y, - w, proc_h, - max(0, xmin), ctx.clip_extents()[2]) + ctx.draw_label_in_box(PROC_TEXT_COLOR, cmdString, + csec_to_xscaled(ctx, proc.start_time), y, + w, + max(0, xmin), ctx.cr.clip_extents()[2]) - next_y = y + proc_h + next_y = y + C.proc_h for child in proc.child_list: - child_x, child_y = draw_processes_recursively(ctx, child, proc_tree, next_y, proc_h) - draw_process_connecting_lines(ctx, x, y, child_x, child_y, proc_h) - next_y = next_y + proc_h * proc_tree.num_nodes([child]) + child_x, child_y = draw_processes_recursively(ctx, child, proc_tree, next_y) + draw_process_connecting_lines(ctx, x, y, child_x, child_y) + next_y = next_y + C.proc_h * proc_tree.num_nodes([child]) return x, y -def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h): - draw_fill_rect(ctx, PROC_COLOR_S, (x, y, w, proc_h)) +def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w): + draw_fill_rect(ctx.cr, PROC_COLOR_S, (x, y, w, C.proc_h)) if len(proc.samples) <= 0: return # cases: @@ -766,101 +788,101 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w, proc_h): alpha = min(sample.cpu_sample.user + sample.cpu_sample.sys, 1.0) # XX rationale? cpu_color = tuple(list(PROC_COLOR_R[0:3]) + [alpha]) # XXX correct color for non-uniform sample intervals - draw_fill_rect(ctx, cpu_color, (last_time, y, sample.time - last_time, proc_h)) + draw_fill_rect(ctx.cr, cpu_color, (last_time, y, sample.time - last_time, C.proc_h)) last_time = sample.time - ctx.restore() + ctx.cr.restore() def usec_to_csec(usec): '''would drop precision without the float() cast''' return float(usec) / 1000 / 10 -def draw_process_events(ctx, proc, proc_tree, x, y, proc_h): - ev_regex = re.compile(OPTIONS.app_options.event_regex) - ev_list = [(ev, csec_to_xscaled(usec_to_csec(ev.time_usec))) +def draw_process_events(ctx, proc, proc_tree, x, y): + ev_regex = re.compile(ctx.app_options.event_regex) + ev_list = [(ev, csec_to_xscaled(ctx, usec_to_csec(ev.time_usec))) if ((not ev.raw_log_line) or ev_regex.match(ev.raw_log_line)) else None for ev in proc.events] if not ev_list: return - if SWEEP_CSEC: - time_origin_relative = SWEEP_CSEC - elif OPTIONS.app_options.absolute_uptime_event_times: + if ctx.SWEEP_CSEC: + time_origin_relative = ctx.SWEEP_CSEC + elif ctx.app_options.absolute_uptime_event_times: time_origin_relative = 0 else: # align to time of first sample - time_origin_relative = time_origin_drawn + proc_tree.sample_period + time_origin_relative = ctx.time_origin_drawn + proc_tree.sample_period - width_csec = sweep_window_width_sec(ctx) * CSEC + width_csec = sweep_window_width_sec(ctx) * C.CSEC # draw ticks, maybe dump log line for (ev, tx) in ev_list: - ctx.set_source_rgba(*EVENT_COLOR) + ctx.cr.set_source_rgba(*EVENT_COLOR) W,H = 1,5 - if SWEEP_CSEC and ev.raw_log_line: + if ctx.SWEEP_CSEC and ev.raw_log_line: delta_csec = float(ev.time_usec)/1000/10 - time_origin_relative if delta_csec >= 0 and delta_csec < width_csec: - # ctx.set_source_rgba(*MAGENTA) + # ctx.cr.set_source_rgba(*MAGENTA) # W,H = 2,8 - if SWEEP_render_serial == render_serial: + if ctx.SWEEP_render_serial == ctx.render_serial: print ev.raw_log_line, - ctx.move_to(tx-W, y+proc_h) # bottom-left - ctx.rel_line_to(W,-H) # top - ctx.rel_line_to(W, H) # bottom-right - ctx.close_path() - ctx.fill() + ctx.cr.move_to(tx-W, y+C.proc_h) # bottom-left + ctx.cr.rel_line_to(W,-H) # top + ctx.cr.rel_line_to(W, H) # bottom-right + ctx.cr.close_path() + ctx.cr.fill() # draw numbers - if not OPTIONS.app_options.print_event_times: + if not ctx.app_options.print_event_times: return - ctx.set_source_rgba(*EVENT_COLOR) - spacing = ctx.text_extents("00")[2] + ctx.cr.set_source_rgba(*EVENT_COLOR) + spacing = ctx.cr.text_extents("00")[2] last_x_touched = 0 last_label_str = None for (ev, tx) in ev_list: if tx < last_x_touched + spacing: continue delta = float(ev.time_usec)/1000/10 - time_origin_relative - if SWEEP_CSEC: - if abs(delta) < CSEC: + if ctx.SWEEP_CSEC: + if abs(delta) < C.CSEC: label_str = '{0:3d}'.format(int(delta*10)) else: - label_str = '{0:.{prec}f}'.format(float(delta)/CSEC, - prec=min(3, max(1, abs(int(3*CSEC/delta))))) + label_str = '{0:.{prec}f}'.format(float(delta)/C.CSEC, + prec=min(3, max(1, abs(int(3*C.CSEC/delta))))) else: # format independent of delta - label_str = '{0:.{prec}f}'.format(float(delta)/CSEC, - prec=min(3, max(0, int(SEC_W/100)))) + label_str = '{0:.{prec}f}'.format(float(delta)/C.CSEC, + prec=min(3, max(0, int(ctx.SEC_W/100)))) if label_str != last_label_str: last_x_touched = tx + draw_label_in_box_at_time( - ctx, PROC_TEXT_COLOR, + ctx.cr, PROC_TEXT_COLOR, label_str, - y + proc_h - 4, tx) + y + C.proc_h - 4, tx) last_label_str = label_str -def draw_process_state_colors(ctx, proc, proc_tree, x, y, w, proc_h): +def draw_process_state_colors(ctx, proc, proc_tree, x, y, w): last_tx = -1 for sample in proc.samples : - tx = csec_to_xscaled(sample.time) + tx = csec_to_xscaled(ctx, sample.time) state = get_proc_state( sample.state ) if state == STATE_WAITING or state == STATE_RUNNING: color = STATE_COLORS[state] - ctx.set_source_rgba(*color) - draw_diamond(ctx, tx, y + proc_h/2, 2.5, proc_h) + ctx.cr.set_source_rgba(*color) + draw_diamond(ctx.cr, tx, y + C.proc_h/2, 2.5, C.proc_h) -def draw_process_connecting_lines(ctx, px, py, x, y, proc_h): - ctx.set_source_rgba(*DEP_COLOR) - ctx.set_dash([2, 2]) +def draw_process_connecting_lines(ctx, px, py, x, y): + ctx.cr.set_source_rgba(*DEP_COLOR) + ctx.cr.set_dash([2, 2]) if abs(px - x) < 3: dep_off_x = 3 - dep_off_y = proc_h / 4 - ctx.move_to(x, y + proc_h / 2) - ctx.line_to(px - dep_off_x, y + proc_h / 2) - ctx.line_to(px - dep_off_x, py - dep_off_y) - ctx.line_to(px, py - dep_off_y) + dep_off_y = C.proc_h / 4 + ctx.cr.move_to(x, y + C.proc_h / 2) + ctx.cr.line_to(px - dep_off_x, y + C.proc_h / 2) + ctx.cr.line_to(px - dep_off_x, py - dep_off_y) + ctx.cr.line_to(px, py - dep_off_y) else: - ctx.move_to(x, y + proc_h / 2) - ctx.line_to(px, y + proc_h / 2) - ctx.line_to(px, py) - ctx.stroke() - ctx.set_dash([]) + ctx.cr.move_to(x, y + C.proc_h / 2) + ctx.cr.line_to(px, y + C.proc_h / 2) + ctx.cr.line_to(px, py) + ctx.cr.stroke() + ctx.cr.set_dash([]) class CumlSample: def __init__(self, proc): @@ -936,7 +958,7 @@ def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, stat_type): # same colors each time we render random.seed (0) - ctx.set_line_width(1) + ctx.cr.set_line_width(1) legends = [] labels = [] @@ -957,7 +979,7 @@ def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, stat_type): y = last_below = below[last_time] last_cuml = cuml = 0.0 - ctx.set_source_rgba(*cs.get_color()) + ctx.cr.set_source_rgba(*cs.get_color()) for time in times: render_seg = False @@ -983,9 +1005,9 @@ def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, stat_type): if render_seg: w = math.ceil ((time - last_time) * chart_bounds[2] / proc_tree.duration()) + 1 x = chart_bounds[0] + round((last_time - proc_tree.start_time) * chart_bounds[2] / proc_tree.duration()) - ctx.rectangle (x, below[last_time] - last_cuml, w, last_cuml) - ctx.fill() -# ctx.stroke() + ctx.cr.rectangle (x, below[last_time] - last_cuml, w, last_cuml) + ctx.cr.fill() +# ctx.cr.stroke() last_time = time y = below [time] - cuml @@ -994,14 +1016,14 @@ def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, stat_type): # render the last segment x = chart_bounds[0] + round((last_time - proc_tree.start_time) * chart_bounds[2] / proc_tree.duration()) y = below[last_time] - cuml - ctx.rectangle (x, y, chart_bounds[2] - x, cuml) - ctx.fill() -# ctx.stroke() + ctx.cr.rectangle (x, y, chart_bounds[2] - x, cuml) + ctx.cr.fill() +# ctx.cr.stroke() # render legend if it will fit if cuml > 8: label = cs.cmd - extnts = ctx.text_extents(label) + extnts = ctx.cr.text_extents(label) label_w = extnts[2] label_h = extnts[3] # print "Text extents %g by %g" % (label_w, label_h) @@ -1016,18 +1038,18 @@ def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, stat_type): below = row # render grid-lines over the top - draw_box_ticks(ctx, chart_bounds) + draw_box_ticks (ctx, chart_bounds) # render labels for l in labels: - draw_text(ctx, l[0], TEXT_COLOR, l[1], l[2]) + draw_text(ctx.cr, l[0], TEXT_COLOR, l[1], l[2]) # Render legends font_height = 20 label_width = 300 LEGENDS_PER_COL = 15 LEGENDS_TOTAL = 45 - ctx.set_font_size (TITLE_FONT_SIZE) + ctx.cr.set_font_size (TITLE_FONT_SIZE) dur_secs = duration / 100 cpu_secs = total_time / 1000000000 @@ -1040,19 +1062,19 @@ def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, stat_type): label = "Cumulative I/O usage, by process; total I/O: " \ " %.5g(s) time: %.3g(s)" % (cpu_secs, dur_secs) - draw_text(ctx, label, TEXT_COLOR, chart_bounds[0], + draw_text(ctx.cr, label, TEXT_COLOR, chart_bounds[0], chart_bounds[1] + font_height) i = 0 legends.sort(lambda a, b: cmp (b[1], a[1])) - ctx.set_font_size(TEXT_FONT_SIZE) + ctx.cr.set_font_size(TEXT_FONT_SIZE) for t in legends: cs = t[0] time = t[1] x = chart_bounds[0] + int (i/LEGENDS_PER_COL) * label_width y = chart_bounds[1] + font_height * ((i % LEGENDS_PER_COL) + 2) str = "%s - %.0f(ms) (%2.2f%%)" % (cs.cmd, time/1000000, (time/total_time) * 100.0) - draw_legend_box(ctx, str, cs.color, x, y, leg_s) + draw_legend_box(ctx.cr, str, cs.color, x, y, C.leg_s) i = i + 1 if i >= LEGENDS_TOTAL: break diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index 6e07371..1bcb88c 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -18,7 +18,7 @@ import gtk.gdk import gtk.keysyms from . import draw -from .draw import RenderOptions +from .draw import DrawContext class PyBootchartWidget(gtk.DrawingArea): __gsignals__ = { @@ -28,11 +28,11 @@ class PyBootchartWidget(gtk.DrawingArea): 'set-scroll-adjustments' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gtk.Adjustment, gtk.Adjustment)) } - def __init__(self, trace, options, xscale): + def __init__(self, trace, drawctx, xscale): gtk.DrawingArea.__init__(self) self.trace = trace - self.options = options + self.drawctx = drawctx self.set_flags(gtk.CAN_FOCUS) @@ -52,7 +52,7 @@ def __init__(self, trace, options, xscale): self.xscale = xscale self.x, self.y = 0.0, 0.0 - self.chart_width, self.chart_height = draw.extents(self.options, self.xscale, self.trace) + self.chart_width, self.chart_height = draw.extents(self.drawctx, self.xscale, self.trace) self.hadj = None self.vadj = None self.hadj_changed_signal_id = None @@ -73,41 +73,49 @@ def draw(self, cr): cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) cr.paint() # fill whole DrawingArea with white self.cr_set_up_transform(cr) - draw.render(cr, self.options, self.xscale, self.trace, self.sweep_csec) + draw.render(cr, self.drawctx, self.xscale, self.trace, self.sweep_csec) def position_changed(self): self.emit("position-changed", self.x, self.y) def device_to_csec_user_y(self, dx, dy): cr = self.window.cairo_create() - self.cr_set_up_transform(cr) + self.cr_set_up_transform(cr) # depends on (self.x, self.y) ux, uy = cr.device_to_user(dx, dy) - self.chart_width, self.chart_height = draw.extents(self.options, self.xscale, self.trace) - return draw.xscaled_to_csec(ux), uy # XX depends on state set by draw.extents() + #self.chart_width, self.chart_height = draw.extents(self.drawctx, self.xscale, self.trace) + return draw._xscaled_to_csec(ux, + draw._sec_w(self.xscale), + draw._time_origin_drawn(self.drawctx, self.trace)), \ + uy # back-transform center of widget to (time, chart_height) coords def current_center (self): (wx, wy, ww, wh) = self.get_allocation() return self.device_to_csec_user_y (ww/2, wh/2) - # Assuming a new zoom_ratio or xscale have been set, correspondingly - # set top-left corner displayed (self.x, self.y) so that + # Assuming all object attributes except self.x and self.y are now valid, + # and that a new zoom_ratio or xscale has been set, correspondingly + # set top-left user-space corner position (self.x, self.y) so that # (csec_x, user_y) will be at window center - def set_center (self, csec_x, user_y): - cur_csec, cur_user_y = self.current_center () - cur_user_x = draw.csec_to_xscaled(cur_csec) - user_x = draw.csec_to_xscaled(csec_x) - self.x += (user_x - cur_user_x) - self.y += (user_y - cur_user_y) + def set_center (self, ctr_csec_x, ctr_user_y): + ctr_user_x = draw.C.off_x + draw._csec_to_xscaled(ctr_csec_x, + draw._time_origin_drawn(self.drawctx, self.trace), + draw._sec_w(self.xscale)) + # XX Use cr.device_to_user_distance() here ? + # Subtract off from the center a vector to the top-left corner. + self.x = (ctr_user_x - float(self.get_allocation()[2])/self.zoom_ratio/2) + self.y = (ctr_user_y - float(self.get_allocation()[3])/self.zoom_ratio/2) self.position_changed() ZOOM_INCREMENT = 1.25 # Zoom maintaining the content at window's current center untranslated. # "Center" is irrespective of any occlusion. def zoom_image (self, zoom_ratio): - old_x, old_y = self.current_center () + old_csec, old_y = self.current_center () + self.zoom_ratio = zoom_ratio - self.set_center(old_x, old_y) + + self.set_center(old_csec, old_y) self._set_scroll_adjustments (self.hadj, self.vadj) self.queue_draw() @@ -118,10 +126,12 @@ def zoom_to_rect (self, rect): # rename "zoom_to_window_width"? self.position_changed() def set_xscale(self, xscale): - old_x, old_y = self.current_center () + old_csec, old_y = self.current_center () + self.xscale = xscale - self.chart_width, self.chart_height = draw.extents(self.options, self.xscale, self.trace) - self.set_center(old_x, old_y) + self.chart_width, self.chart_height = draw.extents(self.drawctx, self.xscale, self.trace) + + self.set_center(old_csec, old_y) self._set_scroll_adjustments (self.hadj, self.vadj) self.queue_draw() @@ -145,19 +155,19 @@ def on_zoom_100(self, action): self.set_xscale(1.0) def show_thread_details(self, button): - self.options.app_options.show_all = button.get_property ('active') + self.drawctx.app_options.show_all = button.get_property ('active') self.queue_draw() def hide_events(self, button): - self.options.app_options.hide_events = not button.get_property ('active') + self.drawctx.app_options.hide_events = not button.get_property ('active') self.queue_draw() def print_event_times(self, button): - self.options.app_options.print_event_times = button.get_property ('active') + self.drawctx.app_options.print_event_times = button.get_property ('active') self.queue_draw() def absolute_uptime_event_times(self, button): - self.options.app_options.absolute_uptime_event_times = button.get_property ('active') + self.drawctx.app_options.absolute_uptime_event_times = button.get_property ('active') self.queue_draw() POS_INCREMENT = 100 @@ -258,6 +268,7 @@ def _set_adj_upper(self, adj, upper): if value_changed: adj.value_changed() + # sets scroll bars to correct position and length -- no direct effect on image def _set_scroll_adjustments(self, hadj, vadj): if hadj == None: hadj = gtk.Adjustment(0.0, 0.0, 0.0, 0.0, 0.0, 0.0) @@ -306,10 +317,10 @@ class PyBootchartShell(gtk.VBox): ''' - def __init__(self, window, trace, options, xscale): + def __init__(self, window, trace, drawctx, xscale): gtk.VBox.__init__(self) - self.widget = PyBootchartWidget(trace, options, xscale) + self.widget = PyBootchartWidget(trace, drawctx, xscale) # Create a UIManager instance uimanager = self.uimanager = gtk.UIManager() @@ -354,8 +365,8 @@ def gtk_CheckButton(name): button.set_focus_on_click(False) return button - if not options.kernel_only: - # Misc. options + if not drawctx.kernel_only: + # Misc. drawctx button = gtk_CheckButton("thread details") button.connect ('toggled', self.widget.show_thread_details) hbox.pack_start (button, False) @@ -367,12 +378,12 @@ def gtk_CheckButton(name): button = gtk_CheckButton("event Time Labels") button.connect ('toggled', self.widget.print_event_times) - button.set_active (options.app_options.print_event_times) + button.set_active (drawctx.app_options.print_event_times) hbox.pack_start (button, False) button = gtk_CheckButton("event Times Absolute") button.connect ('toggled', self.widget.absolute_uptime_event_times) - button.set_active (options.app_options.absolute_uptime_event_times) + button.set_active (drawctx.app_options.absolute_uptime_event_times) hbox.pack_start (button, False) self.pack_start(hbox, False) @@ -385,7 +396,7 @@ def grab_focus(self, window): class PyBootchartWindow(gtk.Window): - def __init__(self, trace, app_options): + def __init__(self, app_options, trace): gtk.Window.__init__(self) window = self @@ -398,23 +409,20 @@ def __init__(self, trace, app_options): tab_page.show() window.add(tab_page) - full_opts = RenderOptions(app_options) - full_tree = PyBootchartShell(window, trace, full_opts, 1.0) + full_drawctx = DrawContext(app_options, trace) + full_tree = PyBootchartShell(window, trace, full_drawctx, 1.0) tab_page.append_page (full_tree, gtk.Label("Full tree")) if trace.kernel is not None and len (trace.kernel) > 2: - kernel_opts = RenderOptions(app_options) - kernel_opts.cumulative = False - kernel_opts.charts = False - kernel_opts.kernel_only = True - kernel_tree = PyBootchartShell(window, trace, kernel_opts, 5.0) + kernel_drawctx = DrawContext(app_options, trace, cumulative = False, charts = False, kernel_only = True) + kernel_tree = PyBootchartShell(window, trace, kernel_drawctx, 5.0) tab_page.append_page (kernel_tree, gtk.Label("Kernel boot")) full_tree.grab_focus(self) self.show() -def show(trace, options): - win = PyBootchartWindow(trace, options) +def show(trace, app_options): + win = PyBootchartWindow(app_options, trace) win.connect('destroy', gtk.main_quit) gtk.main() From 5f06a55cabba691b253847cb9cdc5076a73e3c3f Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:10 -0800 Subject: [PATCH 096/182] lighter shading --- pybootchartgui/draw.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 48bffcd..bdeadf6 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -635,7 +635,8 @@ def sweep_window_width_sec(ctx): def draw_sweep(ctx, sweep_csec, width_csec): def draw_shading(cr, rect): - cr.set_source_rgba(0.0, 0.0, 0.0, 0.1) + # alpha value of the rgba strikes a compromise between appearance on screen, and in printed screenshot + cr.set_source_rgba(0.0, 0.0, 0.0, 0.08) cr.set_line_width(0.0) cr.rectangle(rect) cr.fill() From 32fbb2f420e3461ce124b942c6b0796d78df9507 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:11 -0800 Subject: [PATCH 097/182] FIX colliding proc status counts --- pybootchartgui/draw.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index bdeadf6..f84cc9a 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -347,10 +347,16 @@ def plot_segment_positive(cr, point, x, y): cr.set_line_width(1.5) cr.line_to(x, y) -def plot_scatter_positive(cr, point, x, y): +def _plot_scatter_positive(cr, point, x, y, w, h): if point[1] <= 0: return - draw_diamond(cr, x, y, 3.6, 3.6) + draw_diamond(cr, x, y, w, h) + +def plot_scatter_positive_big(cr, point, x, y): + return _plot_scatter_positive(cr, point, x, y, 5.5, 5.5) + +def plot_scatter_positive_small(cr, point, x, y): + return _plot_scatter_positive(cr, point, x, y, 3.6, 3.6) # All charts assumed to be full-width def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree, data_range, plot_point_func): @@ -446,15 +452,15 @@ def render_charts(ctx, trace, curr_y, w, h): [(sample.time, sample.user + sample.sys) for sample in trace.cpu_stats], \ proc_tree, None, plot_square) - # instantaneous sample - draw_chart (ctx, PROCS_RUNNING_COLOR, False, chart_rect, - [(sample.time, sample.procs_running) for sample in trace.cpu_stats], \ - proc_tree, [0, 9], plot_scatter_positive) - # instantaneous sample draw_chart (ctx, PROCS_BLOCKED_COLOR, False, chart_rect, [(sample.time, sample.procs_blocked) for sample in trace.cpu_stats], \ - proc_tree, [0, 9], plot_scatter_positive) + proc_tree, [0, 9], plot_scatter_positive_big) + + # instantaneous sample + draw_chart (ctx, PROCS_RUNNING_COLOR, False, chart_rect, + [(sample.time, sample.procs_running) for sample in trace.cpu_stats], \ + proc_tree, [0, 9], plot_scatter_positive_small) curr_y = curr_y + 50 + C.bar_h From 54490b2cc4f647a208548609392ad077a4f50b12 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:11 -0800 Subject: [PATCH 098/182] FIX shading --- pybootchartgui/draw.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index f84cc9a..a25bd48 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -651,15 +651,16 @@ def draw_vertical(cr, x): cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) cr.set_line_width(1.0) cr.move_to(x, 0) - cr.line_to(x, C.CUML_HEIGHT) + cr.line_to(x, height) cr.stroke() + height = int(ctx.cr.device_to_user(0,2000)[1]) x = csec_to_xscaled(ctx, sweep_csec) - draw_shading(ctx.cr, (int(x),0,int(ctx.cr.clip_extents()[0]-x),C.CUML_HEIGHT)) + draw_shading(ctx.cr, (int(x),0,int(ctx.cr.clip_extents()[0]-x),height)) draw_vertical(ctx.cr, x) x = csec_to_xscaled(ctx, sweep_csec + width_csec) - draw_shading(ctx.cr, (int(x),0,int(ctx.cr.clip_extents()[2]-x),C.CUML_HEIGHT)) + draw_shading(ctx.cr, (int(x),0,int(ctx.cr.clip_extents()[2]-x),height)) draw_vertical(ctx.cr, x) def draw_process_bar_chart(ctx, proc_tree, times, curr_y, w, h): From a142b0371479b02fba00c7f9ff6965c3c3134d43 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:11 -0800 Subject: [PATCH 099/182] FIX lighter process connecting lines --- pybootchartgui/draw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index a25bd48..d95ff86 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -877,7 +877,7 @@ def draw_process_state_colors(ctx, proc, proc_tree, x, y, w): def draw_process_connecting_lines(ctx, px, py, x, y): ctx.cr.set_source_rgba(*DEP_COLOR) - ctx.cr.set_dash([2, 2]) + ctx.cr.set_dash([1, 2]) # XX repeated draws are not phase-synchronized, resulting in a solid line if abs(px - x) < 3: dep_off_x = 3 dep_off_y = C.proc_h / 4 From 864d8923519dbda1ec6dc96d2ea511a719f8980d Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:11 -0800 Subject: [PATCH 100/182] sweep maintain window time width under scaling --- pybootchartgui/draw.py | 7 +++---- pybootchartgui/gui.py | 3 ++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index d95ff86..ce8d514 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -626,8 +626,7 @@ def render(cr, ctx, xscale, trace, sweep_csec = None): draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_IO) if sweep_csec: - width_sec = sweep_window_width_sec(ctx) - draw_sweep(ctx, sweep_csec, width_sec * C.CSEC) + draw_sweep(ctx, sweep_csec[0], sweep_csec[1] - sweep_csec[0]) #dump_pseudo_event(ctx, "start of event window, width " + int(width*1000) + "msec") ctx.render_serial += 1 @@ -778,7 +777,7 @@ def draw_processes_recursively(ctx, proc, proc_tree, y): for child in proc.child_list: child_x, child_y = draw_processes_recursively(ctx, child, proc_tree, next_y) draw_process_connecting_lines(ctx, x, y, child_x, child_y) - next_y = next_y + C.proc_h * proc_tree.num_nodes([child]) + next_y += C.proc_h * proc_tree.num_nodes([child]) return x, y @@ -812,7 +811,7 @@ def draw_process_events(ctx, proc, proc_tree, x, y): if not ev_list: return if ctx.SWEEP_CSEC: - time_origin_relative = ctx.SWEEP_CSEC + time_origin_relative = ctx.SWEEP_CSEC[0] elif ctx.app_options.absolute_uptime_event_times: time_origin_relative = 0 else: diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index 1bcb88c..44e6cf5 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -193,7 +193,8 @@ def on_area_button_press(self, area, event): self.prevmousex = event.x self.prevmousey = event.y if event.button == 2: - self.sweep_csec, uy = self.device_to_csec_user_y(event.x, 0) + self.sweep_csec = [self.device_to_csec_user_y(event.x, 0)[0], + self.device_to_csec_user_y(event.x + 500, 0)[0]] self.queue_draw() if event.button == 3: self.sweep_csec = None From 8885d584d215ed6973abb921691eaae306a304fb Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:12 -0800 Subject: [PATCH 101/182] FIX simplify process clipping --- pybootchartgui/draw.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index ce8d514..04f87e6 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -584,13 +584,6 @@ def render(cr, ctx, xscale, trace, sweep_csec = None): proc_tree = ctx.proc_tree (trace) - # clip off left-hand side of process bars - ctx.cr.new_path() - ctx.cr.rectangle(0, 0, w, h) - ctx.cr.clip() - - w -= 2*C.off_x - # draw the title and headers if proc_tree.idle: duration = proc_tree.idle @@ -602,6 +595,7 @@ def render(cr, ctx, xscale, trace, sweep_csec = None): else: curr_y = C.off_y; + w -= 2*C.off_x if ctx.charts: curr_y = render_charts (ctx, trace, curr_y, w, h) @@ -732,7 +726,8 @@ def draw_header (ctx, headers, duration): return header_y def draw_processes_recursively(ctx, proc, proc_tree, y): - xmin, ymin = ctx.cr.device_to_user(0, 0) # work around numeric overflow at high xscale factors + xmin = ctx.cr.device_to_user(0, 0[0] # work around numeric overflow at high xscale factors + xmin = max(xmin, 0) x = max(xmin, csec_to_xscaled(ctx, proc.start_time)) w = max(xmin, csec_to_xscaled(ctx, proc.start_time + proc.duration)) - x # XX parser fudges duration upward From b1c6bff29eb24eb5910385296d464bfceaa54f34 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:12 -0800 Subject: [PATCH 102/182] FIX label hides left end of process bar --- pybootchartgui/draw.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 04f87e6..0f42d03 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -230,7 +230,8 @@ def per_render_init(self, cr, time_origin_drawn, SEC_W): def proc_tree (self, trace): return trace.kernel_tree if self.kernel_only else trace.proc_tree - def draw_label_in_box(self, color, label, x, y, w, minx, maxx): + def draw_label_in_box(self, color, label, + x, y, w, minx, maxx): # hack in a pair of left and right margins extents = self.cr.text_extents("j" + label + "k") # XX "j", "k" were found by tedious trial-and-error label = " " + label @@ -242,7 +243,7 @@ def draw_label_in_box(self, color, label, x, y, w, minx, maxx): y += C.proc_h if self.app_options.justify == JUSTIFY_LEFT: - label_x = x + label_x = x - label_w else: label_x = x + w / 2 - label_w / 2 # CENTER @@ -763,10 +764,12 @@ def draw_processes_recursively(ctx, proc, proc_tree, y): else: cmdString = cmdString - ctx.draw_label_in_box(PROC_TEXT_COLOR, cmdString, - csec_to_xscaled(ctx, proc.start_time), y, + ctx.draw_label_in_box( PROC_TEXT_COLOR, cmdString, + csec_to_xscaled(ctx, max(proc.start_time,ctx.time_origin_drawn)), + y, w, - max(0, xmin), ctx.cr.clip_extents()[2]) + ctx.cr.device_to_user(0, 0)[0], + ctx.cr.clip_extents()[2]) next_y = y + C.proc_h for child in proc.child_list: From 07a54cf6090bc976431195f70c19299c6a91264a Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:12 -0800 Subject: [PATCH 103/182] add show_legends checkbox --- pybootchartgui/batch.py | 2 + pybootchartgui/draw.py | 93 ++++++++++++++++++++------------------- pybootchartgui/gui.py | 8 ++++ pybootchartgui/main.py.in | 3 ++ 4 files changed, 60 insertions(+), 46 deletions(-) diff --git a/pybootchartgui/batch.py b/pybootchartgui/batch.py index 93c05d9..48cc503 100644 --- a/pybootchartgui/batch.py +++ b/pybootchartgui/batch.py @@ -30,6 +30,8 @@ def render(writer, trace, app_options, filename): else: fmt = app_options.format + app_options.show_legends = True + if not (fmt in handlers): writer.error ("Unknown format '%s'." % fmt) return 10 diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 0f42d03..0e97987 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -431,17 +431,18 @@ def extents(ctx, xscale, trace): def render_charts(ctx, trace, curr_y, w, h): proc_tree = ctx.proc_tree(trace) - # render bar legend - ctx.cr.set_font_size(LEGEND_FONT_SIZE) - - draw_legend_box(ctx.cr, "CPU (user+sys)", CPU_COLOR, 0, curr_y+20, C.leg_s) - draw_legend_box(ctx.cr, "I/O (wait)", IO_COLOR, 120, curr_y+20, C.leg_s) - draw_legend_diamond(ctx.cr, "Runnable threads", PROCS_RUNNING_COLOR, - 120 +90, curr_y+20, C.leg_s, C.leg_s) - draw_legend_diamond(ctx.cr, "Blocked threads -- Uninterruptible Syscall", PROCS_BLOCKED_COLOR, - 120 +90 +140, curr_y+20, C.leg_s, C.leg_s) - - chart_rect = (0, curr_y+30, w, C.bar_h) + if ctx.app_options.show_legends: + # render bar legend + ctx.cr.set_font_size(LEGEND_FONT_SIZE) + curr_y += 20 + draw_legend_box(ctx.cr, "CPU (user+sys)", CPU_COLOR, 0, curr_y, C.leg_s) + draw_legend_box(ctx.cr, "I/O (wait)", IO_COLOR, 120, curr_y, C.leg_s) + draw_legend_diamond(ctx.cr, "Runnable threads", PROCS_RUNNING_COLOR, + 120 +90, curr_y, C.leg_s, C.leg_s) + draw_legend_diamond(ctx.cr, "Blocked threads -- Uninterruptible Syscall", PROCS_BLOCKED_COLOR, + 120 +90 +140, curr_y, C.leg_s, C.leg_s) + + chart_rect = (0, curr_y+10, w, C.bar_h) draw_box_ticks (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) # render I/O wait -- a backwards delta @@ -463,31 +464,31 @@ def render_charts(ctx, trace, curr_y, w, h): [(sample.time, sample.procs_running) for sample in trace.cpu_stats], \ proc_tree, [0, 9], plot_scatter_positive_small) - curr_y = curr_y + 50 + C.bar_h - - # render second chart - draw_legend_box(ctx.cr, "Disk utilization -- fraction of sample interval I/O queue was not empty", - IO_COLOR, 0, curr_y+20, C.leg_s) - if ctx.app_options.show_ops_not_bytes: - unit = "ops" - else: - unit = "bytes" - draw_legend_line(ctx.cr, "Disk writes -- " + unit + "/sample", - DISK_WRITE_COLOR, 470, curr_y+20, C.leg_s) - draw_legend_line(ctx.cr, "Disk reads+writes -- " + unit + "/sample", - DISK_TPUT_COLOR, 470+120*2, curr_y+20, C.leg_s) + curr_y += 8 + C.bar_h - curr_y += 5 + if ctx.app_options.show_legends: + curr_y += 30 + # render second chart + draw_legend_box(ctx.cr, "Disk utilization -- fraction of sample interval I/O queue was not empty", + IO_COLOR, 0, curr_y, C.leg_s) + if ctx.app_options.show_ops_not_bytes: + unit = "ops" + else: + unit = "bytes" + draw_legend_line(ctx.cr, "Disk writes -- " + unit + "/sample", + DISK_WRITE_COLOR, 470, curr_y, C.leg_s) + draw_legend_line(ctx.cr, "Disk reads+writes -- " + unit + "/sample", + DISK_TPUT_COLOR, 470+120*2, curr_y, C.leg_s) # render disk throughput max_sample = None # render I/O utilization for partition in trace.disk_stats: - draw_text(ctx.cr, partition.name, TEXT_COLOR, 0, curr_y+30) + draw_text(ctx.cr, partition.name, TEXT_COLOR, 0, curr_y+18) # utilization -- inherently normalized [0,1] - chart_rect = (0, curr_y+30+5, w, C.bar_h) + chart_rect = (0, curr_y+18+5, w, C.bar_h) draw_box_ticks (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) # a backwards delta @@ -522,18 +523,20 @@ def render_charts(ctx, trace, curr_y, w, h): # label = "%.1fMB/s" % round ((max_sample.tput) / DISK_BLOCK_SIZE) # draw_text (ctx, label, DISK_TPUT_COLOR, pos_x + shift_x, curr_y + shift_y) - curr_y = curr_y + 30 + C.bar_h + curr_y += 18+C.bar_h # render mem usage chart_rect = (0, curr_y+30, w, meminfo_bar_h) mem_stats = trace.mem_stats if mem_stats: mem_scale = max(sample.records['MemTotal'] - sample.records['MemFree'] for sample in mem_stats) - draw_legend_box(ctx.cr, "Mem cached (scale: %u MiB)" % (float(mem_scale) / 1024), MEM_CACHED_COLOR, curr_y+20, C.leg_s) - draw_legend_box(ctx.cr, "Used", MEM_USED_COLOR, 240, curr_y+20, C.leg_s) - draw_legend_box(ctx.cr, "Buffers", MEM_BUFFERS_COLOR, 360, curr_y+20, C.leg_s) - draw_legend_line(ctx.cr, "Swap (scale: %u MiB)" % max([(sample.records['SwapTotal'] - sample.records['SwapFree'])/1024 for sample in mem_stats]), \ - MEM_SWAP_COLOR, 480, curr_y+20, C.leg_s) + if ctx.app_options.show_legends: + curr_y += 20 + draw_legend_box(ctx.cr, "Mem cached (scale: %u MiB)" % (float(mem_scale) / 1024), MEM_CACHED_COLOR, curr_y, C.leg_s) + draw_legend_box(ctx.cr, "Used", MEM_USED_COLOR, 240, curr_y, C.leg_s) + draw_legend_box(ctx.cr, "Buffers", MEM_BUFFERS_COLOR, 360, curr_y, C.leg_s) + draw_legend_line(ctx.cr, "Swap (scale: %u MiB)" % max([(sample.records['SwapTotal'] - sample.records['SwapFree'])/1024 for sample in mem_stats]), \ + MEM_SWAP_COLOR, 480, curr_y, C.leg_s) draw_box_ticks (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) draw_chart(ctx, MEM_BUFFERS_COLOR, True, chart_rect, \ @@ -658,22 +661,20 @@ def draw_vertical(cr, x): draw_vertical(ctx.cr, x) def draw_process_bar_chart(ctx, proc_tree, times, curr_y, w, h): - header_size = 0 - if not ctx.kernel_only: + if ctx.app_options.show_legends and not ctx.kernel_only: + curr_y += 30 draw_legend_diamond (ctx.cr, "Runnable", - PROCS_RUNNING_COLOR, 10, curr_y + 45, C.leg_s*3/4, C.proc_h) + PROCS_RUNNING_COLOR, 10, curr_y, C.leg_s*3/4, C.proc_h) draw_legend_diamond (ctx.cr, "Uninterruptible Syscall", - PROC_COLOR_D, 10+100, curr_y + 45, C.leg_s*3/4, C.proc_h) + PROC_COLOR_D, 10+100, curr_y, C.leg_s*3/4, C.proc_h) draw_legend_box (ctx.cr, "Running (%cpu)", - PROC_COLOR_R, 10+100+180, curr_y + 45, C.leg_s) + PROC_COLOR_R, 10+100+180, curr_y, C.leg_s) draw_legend_box (ctx.cr, "Sleeping", - PROC_COLOR_S, 10+100+180+130, curr_y + 45, C.leg_s) + PROC_COLOR_S, 10+100+180+130, curr_y, C.leg_s) draw_legend_box (ctx.cr, "Zombie", - PROC_COLOR_Z, 10+100+180+130+90, curr_y + 45, C.leg_s) - header_size = 45 + PROC_COLOR_Z, 10+100+180+130+90, curr_y, C.leg_s) + curr_y -= 9 - #chart_rect = [0, curr_y + header_size + 30, - # w, h - 2 * C.off_y - (curr_y + header_size + 15) + C.proc_h] chart_rect = [-1, -1, -1, -1] ctx.cr.set_font_size (PROC_TEXT_FONT_SIZE) @@ -684,10 +685,10 @@ def draw_process_bar_chart(ctx, proc_tree, times, curr_y, w, h): #draw_sec_labels (ctx.cr, chart_rect, nsec) draw_annotations (ctx, proc_tree, times, chart_rect) - y = curr_y + 60 + curr_y += 15 for root in proc_tree.process_tree: - draw_processes_recursively(ctx, root, proc_tree, y) - y = y + C.proc_h * proc_tree.num_nodes([root]) + draw_processes_recursively(ctx, root, proc_tree, curr_y) + curr_y += C.proc_h * proc_tree.num_nodes([root]) if ctx.SWEEP_CSEC and ctx.SWEEP_render_serial == ctx.render_serial: # mark end of this batch of events, for the benefit of post-processors print diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index 44e6cf5..4698ea9 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -154,6 +154,10 @@ def on_zoom_100(self, action): self.zoom_image(1.0) # XX replace with: self.zoom_ratio = 1.0 \ self.x = 0 \ self.y = 0 self.set_xscale(1.0) + def show_legends(self, button): + self.drawctx.app_options.show_legends = button.get_property ('active') + self.queue_draw() + def show_thread_details(self, button): self.drawctx.app_options.show_all = button.get_property ('active') self.queue_draw() @@ -366,6 +370,10 @@ def gtk_CheckButton(name): button.set_focus_on_click(False) return button + button = gtk_CheckButton("show Legends") + button.connect ('toggled', self.widget.show_legends) + hbox.pack_start (button, False) + if not drawctx.kernel_only: # Misc. drawctx button = gtk_CheckButton("thread details") diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 6c4e73e..764b565 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -62,6 +62,9 @@ def _mk_options_parser(): help="suppress informational messages") parser.add_option("--verbose", action="store_true", dest="verbose", default=False, help="print all messages") + parser.add_option("--show-legends", action="store_true", dest="show_legends", default=False, + help="show legend lines with keys to chart symbols") + # disk stats parser.add_option("-p", "--show-partitions", action="extend", dest="partitions", type="string", default=[], help="draw a disk stat chart for any block device partitions in this comma-separated list") From c56d041c53871af4b18e0fce62d5a8ac75ce8122 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:12 -0800 Subject: [PATCH 104/182] no box ticks --- pybootchartgui/draw.py | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 0e97987..5ea6905 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -296,22 +296,8 @@ def draw_sec_labels(ctx, rect, nsecs): draw_text(ctx.cr, label, TEXT_COLOR, x, rect[1] - 2) prev_x = x + label_w -def draw_box_ticks(ctx, rect): +def draw_box(ctx, rect): draw_rect(ctx.cr, BORDER_COLOR, tuple(rect)) - return - - ctx.cr.set_line_cap(cairo.LINE_CAP_SQUARE) - - for i in range(ctx.SEC_W, rect[2] + 1, ctx.SEC_W): - if ((i / ctx.SEC_W) % 5 == 0) : - ctx.cr.set_source_rgba(*TICK_COLOR_BOLD) - else : - ctx.cr.set_source_rgba(*TICK_COLOR) - ctx.cr.move_to(rect[0] + i, rect[1] + 1) - ctx.cr.line_to(rect[0] + i, rect[1] + rect[3] - 1) - ctx.cr.stroke() - - ctx.cr.set_line_cap(cairo.LINE_CAP_BUTT) def draw_annotations(ctx, proc_tree, times, rect): ctx.cr.set_line_cap(cairo.LINE_CAP_SQUARE) @@ -443,7 +429,7 @@ def render_charts(ctx, trace, curr_y, w, h): 120 +90 +140, curr_y, C.leg_s, C.leg_s) chart_rect = (0, curr_y+10, w, C.bar_h) - draw_box_ticks (ctx, chart_rect) + draw_box (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) # render I/O wait -- a backwards delta draw_chart (ctx, IO_COLOR, True, chart_rect, \ @@ -489,7 +475,7 @@ def render_charts(ctx, trace, curr_y, w, h): # utilization -- inherently normalized [0,1] chart_rect = (0, curr_y+18+5, w, C.bar_h) - draw_box_ticks (ctx, chart_rect) + draw_box (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) # a backwards delta draw_chart (ctx, IO_COLOR, True, chart_rect, @@ -537,7 +523,7 @@ def render_charts(ctx, trace, curr_y, w, h): draw_legend_box(ctx.cr, "Buffers", MEM_BUFFERS_COLOR, 360, curr_y, C.leg_s) draw_legend_line(ctx.cr, "Swap (scale: %u MiB)" % max([(sample.records['SwapTotal'] - sample.records['SwapFree'])/1024 for sample in mem_stats]), \ MEM_SWAP_COLOR, 480, curr_y, C.leg_s) - draw_box_ticks (ctx, chart_rect) + draw_box (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) draw_chart(ctx, MEM_BUFFERS_COLOR, True, chart_rect, \ [(sample.time, sample.records['MemTotal'] - sample.records['MemFree']) for sample in trace.mem_stats], \ @@ -1043,8 +1029,7 @@ def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, stat_type): below = row - # render grid-lines over the top - draw_box_ticks (ctx, chart_bounds) + draw_box (ctx, chart_bounds) # render labels for l in labels: From d9f854ac949091278837c5809ce6f0129e0cade2 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:12 -0800 Subject: [PATCH 105/182] add dynamic process hiding --- pybootchartgui/draw.py | 78 ++++++++++++++++++++++++++++------ pybootchartgui/gui.py | 22 +++++++--- pybootchartgui/process_tree.py | 11 ++++- pybootchartgui/samples.py | 2 + 4 files changed, 93 insertions(+), 20 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 5ea6905..4272c2c 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -39,6 +39,8 @@ WHITE = (1.0, 1.0, 1.0, 1.0) NOTEPAD_YELLLOW = (0.95, 0.95, 0.8, 1.0) +PURPLE = (0.6, 0.1, 0.6, 1.0) + # Process tree border color. BORDER_COLOR = (0.63, 0.63, 0.63, 1.0) # Second tick line color. @@ -215,13 +217,16 @@ def __init__(self, app_options, trace, cumulative = True, charts = True, kernel_ self.charts = charts self.kernel_only = kernel_only # set iff collector daemon saved output of `dmesg` self.SWEEP_CSEC = None - self.SWEEP_render_serial = None - self.render_serial = 0 self.cr = None # Cairo rendering context self.time_origin_drawn = None # time of leftmost plotted data, as integer csecs self.SEC_W = None + # per-rendering state, including recursive process tree traversal + self.SWEEP_render_serial = None + self.render_serial = 0 + self.proc_above_was_hidden = False + def per_render_init(self, cr, time_origin_drawn, SEC_W): self.cr = cr self.time_origin_drawn = time_origin_drawn @@ -548,7 +553,7 @@ def late_init_transform(cr): # # Render the chart. Central method of this module. # -def render(cr, ctx, xscale, trace, sweep_csec = None): +def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): ''' "cr" is the Cairo drawing context -- the transform matrix it carries already has panning translation and "zoom" scaling applied. @@ -594,6 +599,7 @@ def render(cr, ctx, xscale, trace, sweep_csec = None): if proc_tree.taskstats and ctx.cumulative: proc_height -= C.CUML_HEIGHT + ctx.hide_process_y = hide_process_y draw_process_bar_chart(ctx, proc_tree, trace.times, curr_y, w, proc_height) @@ -674,7 +680,11 @@ def draw_process_bar_chart(ctx, proc_tree, times, curr_y, w, h): curr_y += 15 for root in proc_tree.process_tree: draw_processes_recursively(ctx, root, proc_tree, curr_y) - curr_y += C.proc_h * proc_tree.num_nodes([root]) + curr_y += C.proc_h * proc_tree.num_nodes_drawn([root]) + if ctx.proc_above_was_hidden: + draw_hidden_process_separator(ctx, curr_y) + ctx.proc_above_was_hidden = False + if ctx.SWEEP_CSEC and ctx.SWEEP_render_serial == ctx.render_serial: # mark end of this batch of events, for the benefit of post-processors print @@ -713,12 +723,7 @@ def draw_header (ctx, headers, duration): return header_y -def draw_processes_recursively(ctx, proc, proc_tree, y): - xmin = ctx.cr.device_to_user(0, 0[0] # work around numeric overflow at high xscale factors - xmin = max(xmin, 0) - x = max(xmin, csec_to_xscaled(ctx, proc.start_time)) - w = max(xmin, csec_to_xscaled(ctx, proc.start_time + proc.duration)) - x # XX parser fudges duration upward - +def draw_process(ctx, proc, proc_tree, x, y, w): draw_process_activity_colors(ctx, proc, proc_tree, x, y, w) # Do not draw right-hand vertical border -- process exit never exactly known @@ -758,14 +763,63 @@ def draw_processes_recursively(ctx, proc, proc_tree, y): ctx.cr.device_to_user(0, 0)[0], ctx.cr.clip_extents()[2]) +def draw_processes_recursively(ctx, proc, proc_tree, y): + xmin = ctx.cr.device_to_user(0, 0)[0] # work around numeric overflow at high xscale factors + xmin = max(xmin, 0) + x = max(xmin, csec_to_xscaled(ctx, proc.start_time)) + w = max(xmin, csec_to_xscaled(ctx, proc.start_time + proc.duration)) - x # XX parser fudges duration upward + + if ctx.hide_process_y: + if ctx.hide_process_y < y - C.proc_h/4: + ctx.hide_process_y = None # no further hits in traversal are possible + else: + if ctx.hide_process_y < y + C.proc_h/4: + if not proc.draw: + proc.draw = True + ctx.hide_process_y += C.proc_h # unhide all in consecutive hidden processes + else: + pass # ignore hits on the border region if the process is not hidden + elif ctx.hide_process_y < y + C.proc_h*3/4: + if proc.draw: + proc.draw = False + ctx.hide_process_y = None + else: + pass # ignore hits on already-hidden processes + + if not proc.draw: + y -= C.proc_h + ctx.proc_above_was_hidden = True + else: + draw_process(ctx, proc, proc_tree, x, y, w) + if ctx.proc_above_was_hidden: + draw_hidden_process_separator(ctx, y) + ctx.proc_above_was_hidden = False + next_y = y + C.proc_h + for child in proc.child_list: child_x, child_y = draw_processes_recursively(ctx, child, proc_tree, next_y) - draw_process_connecting_lines(ctx, x, y, child_x, child_y) - next_y += C.proc_h * proc_tree.num_nodes([child]) + if proc.draw and child.draw: + # XX draws lines on top of the process name label + draw_process_connecting_lines(ctx, x, y, child_x, child_y) + next_y += C.proc_h * proc_tree.num_nodes_drawn([child]) # XX why a second recursion? return x, y +def draw_hidden_process_separator(ctx, y): + ctx.cr.save() + def draw_again(): + ctx.cr.move_to(ctx.cr.clip_extents()[0], y) + ctx.cr.line_to(ctx.cr.clip_extents()[2], y) + ctx.cr.stroke() + ctx.cr.set_line_width(1.0) + ctx.cr.set_source_rgb(1.0, 1.0, 1.0) + draw_again() + ctx.cr.set_source_rgb(0.3, 0.3, 0.3) + ctx.cr.set_dash([1, 6]) + draw_again() + ctx.cr.restore() + def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w): draw_fill_rect(ctx.cr, PROC_COLOR_S, (x, y, w, C.proc_h)) if len(proc.samples) <= 0: diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index 4698ea9..540b3c1 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -60,6 +60,8 @@ def __init__(self, trace, drawctx, xscale): self.sweep_csec = None + self.hide_process_y = None # XX valid only between self.on_area_button_press() and self.draw() + def do_expose_event(self, event): # XX called on mouse entering or leaving window -- can these be disabled? cr = self.window.cairo_create() self.draw(cr) @@ -73,7 +75,8 @@ def draw(self, cr): cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) cr.paint() # fill whole DrawingArea with white self.cr_set_up_transform(cr) - draw.render(cr, self.drawctx, self.xscale, self.trace, self.sweep_csec) + draw.render(cr, self.drawctx, self.xscale, self.trace, self.sweep_csec, self.hide_process_y) + self.hide_process_y = None def position_changed(self): self.emit("position-changed", self.x, self.y) @@ -196,13 +199,18 @@ def on_area_button_press(self, area, event): area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR)) self.prevmousex = event.x self.prevmousey = event.y - if event.button == 2: - self.sweep_csec = [self.device_to_csec_user_y(event.x, 0)[0], + if event.state & gtk.gdk.CONTROL_MASK: + if event.button == 2: + self.hide_process_y = self.device_to_csec_user_y(event.x, event.y)[1] + self.queue_draw() + else: + if event.button == 2: + self.sweep_csec = [self.device_to_csec_user_y(event.x, 0)[0], self.device_to_csec_user_y(event.x + 500, 0)[0]] - self.queue_draw() - if event.button == 3: - self.sweep_csec = None - self.queue_draw() + self.queue_draw() + if event.button == 3: + self.sweep_csec = None + self.queue_draw() if event.type not in (gtk.gdk.BUTTON_PRESS, gtk.gdk.BUTTON_RELEASE): return False return False diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index 35cb66a..e83ad75 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -110,9 +110,18 @@ def num_nodes(self, process_list): "Counts the number of nodes in the specified process tree.""" nodes = 0 for proc in process_list: - nodes = nodes + self.num_nodes(proc.child_list) + nodes += self.num_nodes(proc.child_list) return nodes + len(process_list) + def num_nodes_drawn(self, process_list): + "Counts the number of nodes in the specified process tree.""" + nodes = 0 + for proc in process_list: + nodes += self.num_nodes_drawn(proc.child_list) + if proc.draw: + nodes += 1 + return nodes + def get_start_time(self, process_subtree): """Returns the start time of the process subtree. This is the start time of the earliest process. diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index d4241a7..a40ea30 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -100,6 +100,8 @@ def __init__(self, writer, pid, cmd, ppid, start_time): self.last_blkio_delay_ns = 0 self.last_swapin_delay_ns = 0 + self.draw = True # dynamic, view-dependent per-process state boolean + # split this process' run - triggered by a name change # XX called only if taskstats.log is provided (bootchart2 daemon) def split(self, writer, pid, cmd, ppid, start_time): From 47620baaf1f839208094c684dc0f562b944928ac Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 12 Dec 2012 12:08:24 -0800 Subject: [PATCH 106/182] refactor pruning for dynamic process hiding --- pybootchartgui/process_tree.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index e83ad75..5b131a3 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -193,9 +193,7 @@ def is_idle_background_process_without_children(p): prune = True if prune: - process_subtree.pop(idx) - for c in p.child_list: - process_subtree.insert(idx, c) + p.draw = False num_removed += 1 continue else: From 75d427b4d4525e863dea151ab83823d97730d398 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:13 -0800 Subject: [PATCH 107/182] integrate legacy static pruning as initial state of dynamic --- pybootchartgui/process_tree.py | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index 5b131a3..c689bf7 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -69,7 +69,7 @@ def __init__(self, writer, kernel, psstats, sample_period, writer.status("merged %i logger processes" % removed) if option_prune != "lightest": - p_processes = self.prune(self.process_tree, None) + p_processes = self.prune(self.process_tree, None, self.is_idle_background_process_without_children) if option_prune == "light": p_exploders = 0 p_threads = 0 @@ -170,39 +170,33 @@ def update_ppids_for_daemons(self, process_list): p.child_list = [] self.build() - def prune(self, process_subtree, parent): + def is_idle_background_process_without_children(self, p): + return not p.active and \ + self.num_nodes(p.child_list) == 0 and \ + len(p.events) == 0 # never prune a process that reports an event + + def prune(self, process_subtree, parent, pruning_test): """Prunes the process tree by removing idle processes and processes that only live for the duration of a single top sample. Sibling processes with the same command line (i.e. threads) are merged together. This filters out sleepy background processes, short-lived processes and bootcharts' analysis tools. """ - def is_idle_background_process_without_children(p): - return not p.active and \ - self.num_nodes(p.child_list) == 0 and \ - len(p.events) == 0 # never prune a process that reports an event - - num_removed = 0 + n_pruned = 0 idx = 0 while idx < len(process_subtree): p = process_subtree[idx] if parent != None or len(p.child_list) == 0: - - prune = False - if is_idle_background_process_without_children(p): - prune = True - - if prune: + if pruning_test(p): p.draw = False - num_removed += 1 - continue + n_pruned += 1 + self.prune(p.child_list, p, lambda p: True) else: - num_removed += self.prune(p.child_list, p) + n_pruned += self.prune(p.child_list, p, pruning_test) else: - num_removed += self.prune(p.child_list, p) + n_pruned += self.prune(p.child_list, p, pruning_test) idx += 1 - return num_removed + return n_pruned def merge_logger(self, process_subtree, logger_proc, monitored_app, app_tree): """Merges the logger's process subtree. The logger will typically From 78b9b11e33a9b74299392e58789b3e196c41ced4 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 12 Dec 2012 12:09:22 -0800 Subject: [PATCH 108/182] disable special case pruning of -- logger -- process --- pybootchartgui/process_tree.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index c689bf7..17a198e 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -65,8 +65,9 @@ def __init__(self, writer, kernel, psstats, sample_period, # XX Specific to bootchart2 collector; not executed for known /proc-sampling collectors. # XX Test bootchart2 with this disabled. - removed = self.merge_logger(self.process_tree, self.LOGGER_PROC, monitoredApp, False) - writer.status("merged %i logger processes" % removed) + if False: # XX disable this dubious code entirely for now + removed = self.merge_logger(self.process_tree, self.LOGGER_PROC, monitoredApp, False) + writer.status("merged %i logger processes" % removed) if option_prune != "lightest": p_processes = self.prune(self.process_tree, None, self.is_idle_background_process_without_children) From 5c4480ed6b2884175afd6263e3c55c23b5dedadd Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:13 -0800 Subject: [PATCH 109/182] simplify button assignments --- pybootchartgui/gui.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index 540b3c1..82afa31 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -199,18 +199,16 @@ def on_area_button_press(self, area, event): area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR)) self.prevmousex = event.x self.prevmousey = event.y - if event.state & gtk.gdk.CONTROL_MASK: - if event.button == 2: - self.hide_process_y = self.device_to_csec_user_y(event.x, event.y)[1] - self.queue_draw() - else: - if event.button == 2: + if event.button == 2: + self.hide_process_y = self.device_to_csec_user_y(event.x, event.y)[1] + self.queue_draw() + if event.button == 3: + if not self.sweep_csec: self.sweep_csec = [self.device_to_csec_user_y(event.x, 0)[0], self.device_to_csec_user_y(event.x + 500, 0)[0]] - self.queue_draw() - if event.button == 3: + else: self.sweep_csec = None - self.queue_draw() + self.queue_draw() if event.type not in (gtk.gdk.BUTTON_PRESS, gtk.gdk.BUTTON_RELEASE): return False return False From 6bbe3ee06dbc41ec067eed6e930d2a4974d1bf5f Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Tue, 4 Dec 2012 18:26:51 -0800 Subject: [PATCH 110/182] proc_ps_threads.log format support --- pybootchartgui/parsing.py | 74 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 8ad0e36..f356fba 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -325,6 +325,78 @@ def _parse_proc_ps_log(options, writer, file): return ProcessStats (writer, processMap, len (timed_blocks), avgSampleLength, startTime, ltime) +def _parse_proc_ps_threads_log(options, writer, file): + """ + * 0* pid -- inserted here from value in /proc/*pid*/task/. Not to be found in /proc/*pid*/task/*tid*/stat. + * Not the same as pgrp, session, or tpgid. Refer to collector daemon source code for details. + * 1 tid + * 2 comm + * 3 state + * 4 ppid + * 5 flags + * 6 majflt + * 7 utime + * 8 stime + * 9 cutime + * 10 cstime + * 11 priority + * 12 nice + * 13 time_in_jiffies_the_process_started_after_system_boot + * 14 current_EIP_instruction_pointer + * 15 wchan + * 16 scheduling_policy + """ + processMap = {} + ltime = 0 + timed_blocks = _parse_timed_blocks(file) + for time, lines in timed_blocks: + for line in lines: + if line is '': continue + tokens = line.split(' ') + if len(tokens) < 17: + writer.status("misformatted line at time {0:d}:\n\t{1:s}".format(time,line)) + continue + + offset = [index for index, token in enumerate(tokens[2:]) if (len(token) > 0 and token[-1] == ')')][0] + pid, tid, cmd, state, ppid = int(tokens[0]), int(tokens[1]), ' '.join(tokens[2:3+offset]), tokens[3+offset], int(tokens[4+offset]) + userCpu, sysCpu, starttime = int(tokens[7+offset]), int(tokens[8+offset]), int(tokens[13+offset]) + + # magic fixed point-ness ... + tid *= 1000 + pid *= 1000 + ppid *= 1000 + if tid in processMap: + process = processMap[tid] + process.cmd = cmd.strip('()') # why rename after latest name?? + else: + if time < starttime: + # large values signify a collector problem, e.g. resource starvation + writer.status("time (%dcs) < starttime (%dcs), diff %d -- TID %d" % + (time, starttime, time-starttime, tid/1000)) + + process = Process(writer, tid, cmd.strip('()'), ppid, starttime) + processMap[pid] = process + + if process.last_user_cpu_time is not None and process.last_sys_cpu_time is not None: + if ltime is None: + userCpuLoad, sysCpuLoad = 0, 0 + else: + userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) + cpuSample = CPUSample('null', userCpuLoad, sysCpuLoad, 0.0, 0.0) + process.samples.append(ProcessSample(time, state, cpuSample)) + + process.last_user_cpu_time = userCpu + process.last_sys_cpu_time = sysCpu + ltime = time + + if len (timed_blocks) < 2: + return None + + startTime = timed_blocks[0][0] + avgSampleLength = (ltime - startTime)/(len (timed_blocks) - 1) + + return ProcessStats (writer, processMap, len (timed_blocks), avgSampleLength, startTime, ltime) + def _parse_taskstats_log(writer, file): """ * See bootchart-collector.c for details. @@ -761,6 +833,8 @@ def _do_parse(writer, state, tf, name, file, options): state.parent_map = _parse_paternity_log(writer, file) elif name == "proc_ps.log": # obsoleted by TASKSTATS state.ps_stats = _parse_proc_ps_log(options, writer, file) + elif name == "proc_ps_threads.log": + state.ps_stats = _parse_proc_ps_threads_log(options, writer, file) elif name == "kernel_pacct": # obsoleted by PROC_EVENTS state.parent_map = _parse_pacct(writer, file) elif name == "events-7.log": # 7 is number of fields -- a crude versioning scheme From e6a4c82061cb889b6fd2c1f7939926b799b4bcf7 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:14 -0800 Subject: [PATCH 111/182] refactor -- split CPUSample by user --- pybootchartgui/parsing.py | 12 ++++++------ pybootchartgui/samples.py | 13 ++++++++++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index f356fba..0fa2c0a 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -310,7 +310,7 @@ def _parse_proc_ps_log(options, writer, file): userCpuLoad, sysCpuLoad = 0, 0 else: userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) - cpuSample = CPUSample('null', userCpuLoad, sysCpuLoad, 0.0, 0.0) + cpuSample = ProcessCPUSample('null', userCpuLoad, sysCpuLoad, 0.0, 0.0) process.samples.append(ProcessSample(time, state, cpuSample)) process.last_user_cpu_time = userCpu @@ -382,7 +382,7 @@ def _parse_proc_ps_threads_log(options, writer, file): userCpuLoad, sysCpuLoad = 0, 0 else: userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) - cpuSample = CPUSample('null', userCpuLoad, sysCpuLoad, 0.0, 0.0) + cpuSample = ProcessCPUSample('null', userCpuLoad, sysCpuLoad, 0.0, 0.0) process.samples.append(ProcessSample(time, state, cpuSample)) process.last_user_cpu_time = userCpu @@ -459,11 +459,11 @@ def _parse_taskstats_log(writer, file): else: state = "S" - # retain the ns timing information into a CPUSample - that tries + # retain the ns timing information into a ProcessCPUSample - that tries # with the old-style to be a %age of CPU used in this time-slice. if delta_cpu_ns + delta_blkio_delay_ns + delta_swapin_delay_ns > 0: # print "proc %s cpu_ns %g delta_cpu %g" % (cmd, cpu_ns, delta_cpu_ns) - cpuSample = CPUSample('null', delta_cpu_ns, 0.0, + cpuSample = ProcessCPUSample('null', delta_cpu_ns, 0.0, delta_blkio_delay_ns, delta_swapin_delay_ns) process.samples.append(ProcessSample(time, state, cpuSample)) @@ -507,9 +507,9 @@ def _parse_proc_stat_log(file): if tokens[0] == 'procs_blocked': procs_blocked = int(tokens[1]) if ltimes: - samples.append( CPUSample(time, user/aSum, system/aSum, iowait/aSum, 0.0, procs_running, procs_blocked) ) + samples.append( SystemCPUSample(time, user/aSum, system/aSum, iowait/aSum, procs_running, procs_blocked) ) else: - samples.append( CPUSample(time, 0.0, 0.0, 0.0, 0.0, procs_running, procs_blocked) ) + samples.append( SystemCPUSample(time, 0.0, 0.0, 0.0, procs_running, procs_blocked) ) ltimes = times return samples diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index a40ea30..298cb27 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -31,16 +31,23 @@ def __init__(self, time): def add_diskdata(self, new_diskdata): self.diskdata = [ a + b for a, b in zip(self.diskdata, new_diskdata) ] -class CPUSample: - def __init__(self, time, user, sys, io, swap = 0.0, procs_running = 0, procs_blocked = 0): +class SystemCPUSample: + def __init__(self, time, user, sys, io, procs_running, procs_blocked): self.time = time self.user = user self.sys = sys self.io = io - self.swap = swap self.procs_running = procs_running self.procs_blocked = procs_blocked +class ProcessCPUSample: + def __init__(self, time, user, sys, io, swap): + self.time = time + self.user = user + self.sys = sys + self.io = io + self.swap = swap + @property def cpu(self): return self.user + self.sys From 42eaf883400557c04cfb38fd14eeb72c008d52c2 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:14 -0800 Subject: [PATCH 112/182] proc_ps_threads.log -- add per thread option --- pybootchartgui/draw.py | 5 +++-- pybootchartgui/parsing.py | 17 +++++++++-------- pybootchartgui/process_tree.py | 9 +++++++-- pybootchartgui/samples.py | 3 ++- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 4272c2c..424fb77 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -743,13 +743,14 @@ def draw_process(ctx, proc, proc_tree, x, y, w): if not ctx.app_options.hide_events: draw_process_events(ctx, proc, proc_tree, x, y) - ipid = int(proc.pid) if proc_tree.taskstats and ctx.app_options.show_all: cmdString = '' else: cmdString = proc.cmd + ipid = int(proc.pid) + itid = int(proc.tid) if (ctx.app_options.show_pid or ctx.app_options.show_all) and ipid is not 0: - cmdString = cmdString + " [" + str(ipid / 1000) + "]" + cmdString = cmdString + " [" + str(ipid / 1000) + ":" + str(itid / 1000) + "]" if ctx.app_options.show_all: if proc.args: cmdString = cmdString + " '" + "' '".join(proc.args) + "'" diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 0fa2c0a..f604b48 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -131,13 +131,14 @@ def find_parent_id_for(pid): # merge in events if self.events is not None: for ev in self.events: - key = int(ev.pid) * 1000 + if ev.time > self.ps_stats.end_time: + continue + key = int(ev.tid) * 1000 if key in self.ps_stats.process_map: - proc = self.ps_stats.process_map[key] - if ev.time < self.ps_stats.end_time: - proc.events.append(ev) + self.ps_stats.process_map[key].events.append(ev) else: - writer.warn("event for [%d:%d] lost at time %d" % (ev.pid, ev.tid, ev.time)) + writer.warn("no samples of /proc/%d/task/%d/proc found -- event lost:\n\t%s" % + (ev.pid, ev.tid, ev.raw_log_line)) # re-parent any stray orphans if we can if self.parent_map is not None: @@ -302,7 +303,7 @@ def _parse_proc_ps_log(options, writer, file): writer.status("time (%d) < starttime (%d), diff %d -- PID %d" % (time, starttime, time-starttime, pid/1000)) - process = Process(writer, pid, cmd.strip('()'), ppid, starttime) + process = Process(writer, pid, pid, cmd.strip('()'), ppid, starttime) processMap[pid] = process if process.last_user_cpu_time is not None and process.last_sys_cpu_time is not None: @@ -374,8 +375,8 @@ def _parse_proc_ps_threads_log(options, writer, file): writer.status("time (%dcs) < starttime (%dcs), diff %d -- TID %d" % (time, starttime, time-starttime, tid/1000)) - process = Process(writer, tid, cmd.strip('()'), ppid, starttime) - processMap[pid] = process + process = Process(writer, pid, tid, cmd.strip('()'), ppid, starttime) + processMap[tid] = process if process.last_user_cpu_time is not None and process.last_sys_cpu_time is not None: if ltime is None: diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index 17a198e..14a6480 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -13,6 +13,11 @@ # You should have received a copy of the GNU General Public License # along with pybootchartgui. If not, see . +MAX_PID=100000 + +def sort_func(proc): + return long(proc.pid) / 1000 * MAX_PID + proc.tid / 1000 + class ProcessTree: """ProcessTree encapsulates a process tree. The tree is built from log files retrieved during the boot process. When building the process tree, it is @@ -49,7 +54,7 @@ def __init__(self, writer, kernel, psstats, sample_period, process_list = psstats.process_map.values() else: process_list = kernel + psstats.process_map.values() - self.process_list = sorted(process_list, key = lambda p: p.pid) + self.process_list = sorted(process_list, key = sort_func) self.sample_period = sample_period self.build() @@ -104,7 +109,7 @@ def build(self): def sort(self, process_subtree): """Sort process tree.""" for p in process_subtree: - p.child_list.sort(key = lambda p: p.pid) + p.child_list.sort(key = sort_func) self.sort(p.child_list) def num_nodes(self, process_list): diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 298cb27..9481ddc 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -85,9 +85,10 @@ def __init__(self, writer, process_map, sample_count, sample_period, start_time, writer.info ("process list size: %d" % len (self.process_map.values())) class Process: - def __init__(self, writer, pid, cmd, ppid, start_time): + def __init__(self, writer, pid, tid, cmd, ppid, start_time): self.writer = writer self.pid = pid + self.tid = tid self.cmd = cmd self.exe = cmd self.args = [] From 66cb74d86894523027396b143e4d8b62d9fe07a7 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:14 -0800 Subject: [PATCH 113/182] refactor -- parsing --- pybootchartgui/parsing.py | 75 ++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 44 deletions(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index f604b48..34b4915 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -269,6 +269,32 @@ def parse(block): blocks = file.read().split('\n\n') return [parse(block) for block in blocks if block.strip() and not block.endswith(' not running\n')] +def _handle_sample(processMap, writer, ltime, + time, pid, tid, cmd, state, ppid, userCpu, sysCpu, starttime): + if tid in processMap: + process = processMap[tid] + process.cmd = cmd.strip('()') # why rename after latest name?? + else: + if time < starttime: + # large values signify a collector problem, e.g. resource starvation + writer.status("time (%dcs) < starttime (%dcs), diff %d -- TID %d" % + (time, starttime, time-starttime, tid/1000)) + + process = Process(writer, pid, tid, cmd.strip('()'), ppid, starttime) + processMap[tid] = process + + if process.last_user_cpu_time is not None and process.last_sys_cpu_time is not None: + if ltime is None: + userCpuLoad, sysCpuLoad = 0, 0 + else: + userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) + cpuSample = ProcessCPUSample('null', userCpuLoad, sysCpuLoad, 0.0, 0.0) + process.samples.append(ProcessSample(time, state, cpuSample)) + + process.last_user_cpu_time = userCpu + process.last_sys_cpu_time = sysCpu + return processMap + def _parse_proc_ps_log(options, writer, file): """ * See proc(5) for details. @@ -294,28 +320,8 @@ def _parse_proc_ps_log(options, writer, file): # magic fixed point-ness ... pid *= 1000 ppid *= 1000 - if pid in processMap: - process = processMap[pid] - process.cmd = cmd.strip('()') # why rename after latest name?? - else: - if time < starttime: - # large values signify a collector problem, e.g. resource starvation - writer.status("time (%d) < starttime (%d), diff %d -- PID %d" % - (time, starttime, time-starttime, pid/1000)) - - process = Process(writer, pid, pid, cmd.strip('()'), ppid, starttime) - processMap[pid] = process - - if process.last_user_cpu_time is not None and process.last_sys_cpu_time is not None: - if ltime is None: - userCpuLoad, sysCpuLoad = 0, 0 - else: - userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) - cpuSample = ProcessCPUSample('null', userCpuLoad, sysCpuLoad, 0.0, 0.0) - process.samples.append(ProcessSample(time, state, cpuSample)) - - process.last_user_cpu_time = userCpu - process.last_sys_cpu_time = sysCpu + processMap = _handle_sample(processMap, writer, ltime, + time, pid, pid, cmd, state, ppid, userCpu, sysCpu, starttime) ltime = time if len (timed_blocks) < 2: @@ -363,31 +369,12 @@ def _parse_proc_ps_threads_log(options, writer, file): userCpu, sysCpu, starttime = int(tokens[7+offset]), int(tokens[8+offset]), int(tokens[13+offset]) # magic fixed point-ness ... - tid *= 1000 pid *= 1000 + tid *= 1000 ppid *= 1000 - if tid in processMap: - process = processMap[tid] - process.cmd = cmd.strip('()') # why rename after latest name?? - else: - if time < starttime: - # large values signify a collector problem, e.g. resource starvation - writer.status("time (%dcs) < starttime (%dcs), diff %d -- TID %d" % - (time, starttime, time-starttime, tid/1000)) - - process = Process(writer, pid, tid, cmd.strip('()'), ppid, starttime) - processMap[tid] = process - - if process.last_user_cpu_time is not None and process.last_sys_cpu_time is not None: - if ltime is None: - userCpuLoad, sysCpuLoad = 0, 0 - else: - userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) - cpuSample = ProcessCPUSample('null', userCpuLoad, sysCpuLoad, 0.0, 0.0) - process.samples.append(ProcessSample(time, state, cpuSample)) - process.last_user_cpu_time = userCpu - process.last_sys_cpu_time = sysCpu + processMap = _handle_sample(processMap, writer, ltime, + time, pid, tid, cmd, state, ppid, userCpu, sysCpu, starttime) ltime = time if len (timed_blocks) < 2: From cb1672cd79574a9541547190982a5244db4cbffa Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:14 -0800 Subject: [PATCH 114/182] event -- lazy loading of raw_log_line --- pybootchartgui/draw.py | 5 ++--- pybootchartgui/parsing.py | 28 ++++++++-------------------- pybootchartgui/samples.py | 16 ++++++++++++++-- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 424fb77..9b48fec 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -846,7 +846,6 @@ def usec_to_csec(usec): def draw_process_events(ctx, proc, proc_tree, x, y): ev_regex = re.compile(ctx.app_options.event_regex) ev_list = [(ev, csec_to_xscaled(ctx, usec_to_csec(ev.time_usec))) - if ((not ev.raw_log_line) or ev_regex.match(ev.raw_log_line)) else None for ev in proc.events] if not ev_list: return @@ -863,13 +862,13 @@ def draw_process_events(ctx, proc, proc_tree, x, y): for (ev, tx) in ev_list: ctx.cr.set_source_rgba(*EVENT_COLOR) W,H = 1,5 - if ctx.SWEEP_CSEC and ev.raw_log_line: + if ctx.SWEEP_CSEC and ev.raw_log_seek: delta_csec = float(ev.time_usec)/1000/10 - time_origin_relative if delta_csec >= 0 and delta_csec < width_csec: # ctx.cr.set_source_rgba(*MAGENTA) # W,H = 2,8 if ctx.SWEEP_render_serial == ctx.render_serial: - print ev.raw_log_line, + print ev.raw_log_line(), ctx.cr.move_to(tx-W, y+C.proc_h) # bottom-left ctx.cr.rel_line_to(W,-H) # top ctx.cr.rel_line_to(W, H) # bottom-right diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 34b4915..267eecd 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -125,7 +125,7 @@ def find_parent_id_for(pid): for cpu in self.cpu_stats: # assign to the init process's bar, for lack of any better ev = EventSample(cpu.time, cpu.time*10*1000, init_pid, init_pid, - "comm", "func_file_line", None) + "comm", "func_file_line", None, None) proc.events.append(ev) # merge in events @@ -138,7 +138,7 @@ def find_parent_id_for(pid): self.ps_stats.process_map[key].events.append(ev) else: writer.warn("no samples of /proc/%d/task/%d/proc found -- event lost:\n\t%s" % - (ev.pid, ev.tid, ev.raw_log_line)) + (ev.pid, ev.tid, ev.raw_log_line())) # re-parent any stray orphans if we can if self.parent_map is not None: @@ -698,15 +698,6 @@ def _parse_events_log(writer, tf, file): is the responsibility of target-specific pre-processors. Eventual output is per-process lists of events in temporal order. ''' - def _readline(raw_log_filename, raw_log_seek): - file = tf.extractfile(raw_log_filename) - if not file: - return - file.seek(raw_log_seek) - line = file.readline() - file.close() - return line - split_re = re.compile ("^(\S+) +(\S+) +(\S+) +(\S+) +(\S+) +(\S+) +(\S+)$") timed_blocks = _parse_timed_blocks(file) samples = [] @@ -724,8 +715,8 @@ def _readline(raw_log_filename, raw_log_seek): func_file_line = m.group(5) raw_log_filename = m.group(6) raw_log_seek = int(m.group(7)) - raw_log_line = _readline(raw_log_filename, raw_log_seek) - samples.append( EventSample(time, time_usec, pid, tid, comm, func_file_line, raw_log_line)) + samples.append( EventSample(time, time_usec, pid, tid, comm, func_file_line, + tf.extractfile(raw_log_filename), raw_log_seek)) return samples # @@ -854,17 +845,14 @@ def parse_paths(writer, state, paths, options): if extension != ".tar": writer.warn("warning: can only handle zipped tar files, not zipped '%s'-files; ignoring" % extension) continue - tf = None + state.tf = None try: writer.status("parsing '%s'" % path) - tf = tarfile.open(path, 'r:*') - for name in tf.getnames(): - state = _do_parse(writer, state, tf, name, tf.extractfile(name), options) + state.tf = tarfile.open(path, 'r:*') + for name in state.tf.getnames(): + state = _do_parse(writer, state, state.tf, name, state.tf.extractfile(name), options) except tarfile.ReadError as error: raise ParseError("error: could not read tarfile '%s': %s." % (path, error)) - finally: - if tf != None: - tf.close() else: state = parse_file(writer, state, path, options) return state diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 9481ddc..498a68d 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -15,14 +15,26 @@ class EventSample: - def __init__(self, time, time_usec, pid, tid, comm, func_file_line, raw_log_line): + def __init__(self, time, time_usec, pid, tid, comm, func_file_line, raw_log_file, raw_log_seek): self.time = time self.time_usec = time_usec self.pid = pid self.tid = tid self.comm = comm self.func_file_line = func_file_line - self.raw_log_line = raw_log_line + self.raw_log_file = raw_log_file # a File object + self.raw_log_seek = raw_log_seek + + def raw_log_line(self): + def _readline(file, raw_log_seek): + if not file: + return + file.seek(raw_log_seek) + line = file.readline() + return line + return _readline(self.raw_log_file, self.raw_log_seek) + + class DiskStatSample: def __init__(self, time): From e0462de24f8d4a6d17c4e0551a7f3a119528b887 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:15 -0800 Subject: [PATCH 115/182] draw process_activity as bars -- not alpha modulated color --- pybootchartgui/draw.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 9b48fec..3e5c371 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -832,10 +832,9 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w): last_time = max(proc.start_time, proc.samples[0].time - proc_tree.sample_period) ctx_save__csec_to_xscaled(ctx) for sample in proc.samples[1:] : - alpha = min(sample.cpu_sample.user + sample.cpu_sample.sys, 1.0) # XX rationale? - cpu_color = tuple(list(PROC_COLOR_R[0:3]) + [alpha]) - # XXX correct color for non-uniform sample intervals - draw_fill_rect(ctx.cr, cpu_color, (last_time, y, sample.time - last_time, C.proc_h)) + normalized = sample.cpu_sample.user + sample.cpu_sample.sys + draw_fill_rect(ctx.cr, PROC_COLOR_R, (last_time, y+C.proc_h, + sample.time - last_time, -normalized * C.proc_h)) last_time = sample.time ctx.cr.restore() From 9712d1f8777d8e7a662052f488486a5a4e84974e Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:15 -0800 Subject: [PATCH 116/182] event dump list --- pybootchartgui/draw.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 3e5c371..f94dd80 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -565,6 +565,7 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): (w, h) = extents(ctx, xscale, trace) ctx.per_render_init(cr, _time_origin_drawn(ctx, trace), _sec_w(xscale)) + ctx.event_dump_list = [] ctx.cr.set_line_width(1.0) ctx.cr.select_font_face(FONT_NAME) @@ -623,6 +624,9 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): ctx.cr.restore() + for ev in ctx.event_dump_list: + print ev.raw_log_line(), + def sweep_window_width_sec(ctx): '''about half the width of the visible part of the per-process bars''' user_width = ctx.cr.device_to_user_distance(500,0)[0] @@ -867,7 +871,7 @@ def draw_process_events(ctx, proc, proc_tree, x, y): # ctx.cr.set_source_rgba(*MAGENTA) # W,H = 2,8 if ctx.SWEEP_render_serial == ctx.render_serial: - print ev.raw_log_line(), + ctx.event_dump_list.append(ev) ctx.cr.move_to(tx-W, y+C.proc_h) # bottom-left ctx.cr.rel_line_to(W,-H) # top ctx.cr.rel_line_to(W, H) # bottom-right From 3ff9c46216d05a497efc1d27cc713ddf630980b1 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:15 -0800 Subject: [PATCH 117/182] rework pruning --- pybootchartgui/main.py.in | 18 ++++++++++-------- pybootchartgui/parsing.py | 4 ++-- pybootchartgui/process_tree.py | 20 ++++++++------------ 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 764b565..6524f2b 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -44,20 +44,22 @@ class ExtendOption(Option): def _mk_options_parser(): """Make an options parser.""" - usage = "%prog [options] PATH, ..., PATH" + usage = "%prog [options] PATH" version = "%prog v@VER@" - parser = optparse.OptionParser(usage, version=version, + parser = optparse.OptionParser(usage=usage, version=version, description="PATH must point to a bootchart*.tgz", epilog=None, option_class=ExtendOption) parser.add_option("-i", "--interactive", action="store_true", dest="interactive", default=False, help="start in active mode") - parser.add_option("-n", "--prune", dest="prune", default="heavy", - choices=["heavy", "light", "lightest"], - help="prune the process tree: " + - "heavy, (default); " + - "light, prune only idle processes from the tree; " + - "lightest") + parser.add_option("--merge", dest="merge", default=False, + help="Prune the process tree of all activity from dull children: " + + "stats are added to parent, child process is lost in drawing of chart.") + parser.add_option("--hide-processes", dest="hide_processes", default="idle", + choices=["none", "idle"], + help="Hide uninteresting processes on initial rendering, " + + "though recoverable in GUI by mouse click: " + + "\"none\", show all; [\"%default\"], hide idle processes.") parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="suppress informational messages") parser.add_option("--verbose", action="store_true", dest="verbose", default=False, diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 267eecd..1e80012 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -75,13 +75,13 @@ def __init__(self, writer, paths, options): self.proc_tree = ProcessTree(writer, self.kernel, self.ps_stats, self.ps_stats.sample_period, self.headers.get("profile.process"), - options.prune, idle, self.taskstats, + options, idle, self.taskstats, self.parent_map is not None) if self.kernel is not None: self.kernel_tree = ProcessTree(writer, self.kernel, None, 0, self.headers.get("profile.process"), - False, None, None, True) + options, None, None, True) def valid(self): return self.headers != None and self.disk_stats != None and \ diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index 14a6480..e27f72d 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -20,9 +20,9 @@ def sort_func(proc): class ProcessTree: """ProcessTree encapsulates a process tree. The tree is built from log files - retrieved during the boot process. When building the process tree, it is - pruned and merged in order to be able to visualize it in a comprehensible - manner. + retrieved during the boot process. When building the process tree, it may be + 'pruned' i.e. processes merged in order to be able to visualize the whole + in a comprehensible manner. The following pruning techniques are used: @@ -43,7 +43,7 @@ class ProcessTree: EXPLODER_PROCESSES = set(['hwup']) def __init__(self, writer, kernel, psstats, sample_period, - monitoredApp, option_prune, idle, taskstats, + monitoredApp, options, idle, taskstats, accurate_parentage, for_testing = False): self.writer = writer self.process_tree = [] @@ -74,18 +74,14 @@ def __init__(self, writer, kernel, psstats, sample_period, removed = self.merge_logger(self.process_tree, self.LOGGER_PROC, monitoredApp, False) writer.status("merged %i logger processes" % removed) - if option_prune != "lightest": + if options.hide_processes != "none": p_processes = self.prune(self.process_tree, None, self.is_idle_background_process_without_children) - if option_prune == "light": - p_exploders = 0 - p_threads = 0 - p_runs = 0 - else: + if options.merge: p_exploders = self.merge_exploders(self.process_tree, self.EXPLODER_PROCESSES) p_threads = self.merge_siblings(self.process_tree) p_runs = self.merge_runs(self.process_tree) - - writer.status("pruned %i process, %i exploders, %i threads, and %i runs" % (p_processes, p_exploders, p_threads, p_runs)) + writer.status("pruned %i process, %i exploders, %i threads, and %i runs" % + (p_processes, p_exploders, p_threads, p_runs)) self.sort(self.process_tree) From 20559a507b7d03fd5e5f20cba3b0daafbdcc7307 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:16 -0800 Subject: [PATCH 118/182] draw -- pid+tid first -- name last --- pybootchartgui/draw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index f94dd80..594bf3a 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -754,7 +754,7 @@ def draw_process(ctx, proc, proc_tree, x, y, w): ipid = int(proc.pid) itid = int(proc.tid) if (ctx.app_options.show_pid or ctx.app_options.show_all) and ipid is not 0: - cmdString = cmdString + " [" + str(ipid / 1000) + ":" + str(itid / 1000) + "]" + cmdString = " [" + str(ipid / 1000) + ":" + str(itid / 1000) + "]" + cmdString if ctx.app_options.show_all: if proc.args: cmdString = cmdString + " '" + "' '".join(proc.args) + "'" From f36e36d60fb8abbfb6125bf29bcf2a3746e40486 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:16 -0800 Subject: [PATCH 119/182] draw show_all show ppid --- pybootchartgui/draw.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 594bf3a..7f7f286 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -751,15 +751,14 @@ def draw_process(ctx, proc, proc_tree, x, y, w): cmdString = '' else: cmdString = proc.cmd - ipid = int(proc.pid) - itid = int(proc.tid) - if (ctx.app_options.show_pid or ctx.app_options.show_all) and ipid is not 0: - cmdString = " [" + str(ipid / 1000) + ":" + str(itid / 1000) + "]" + cmdString - if ctx.app_options.show_all: - if proc.args: - cmdString = cmdString + " '" + "' '".join(proc.args) + "'" - else: - cmdString = cmdString + if (ctx.app_options.show_pid or ctx.app_options.show_all) and proc.pid is not 0: + prefix = " [" + if ctx.app_options.show_all: + prefix += str(proc.ppid / 1000) + ":" + prefix += str(proc.pid / 1000) + ":" + str(proc.tid / 1000) + "]" + cmdString = prefix + cmdString + if ctx.app_options.show_all and proc.args: + cmdString += " '" + "' '".join(proc.args) + "'" ctx.draw_label_in_box( PROC_TEXT_COLOR, cmdString, csec_to_xscaled(ctx, max(proc.start_time,ctx.time_origin_drawn)), From 57431a3f5de55f6efc5950236f3472d36ce67575 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:16 -0800 Subject: [PATCH 120/182] events dump context --- pybootchartgui/draw.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 7f7f286..07bb67d 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -624,8 +624,20 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): ctx.cr.restore() - for ev in ctx.event_dump_list: - print ev.raw_log_line(), + if False: # dump events only + ctx.event_dump_list.sort(key = lambda e: e.time_usec) + for ev in ctx.event_dump_list: + print ev.raw_log_line(), + else: # dump all raw log lines between events, where "between" means merely textually, + # irrespective of time. + ctx.event_dump_list.sort(key = lambda e: e.raw_log_seek) + if len(ctx.event_dump_list): + event0 = ctx.event_dump_list[0] + event0.raw_log_file.seek(event0.raw_log_seek) + eventN = ctx.event_dump_list[-1] + #for line in event0.raw_log_file.readline(): + while event0.raw_log_file.tell() <= eventN.raw_log_seek: + print event0.raw_log_file.readline(), def sweep_window_width_sec(ctx): '''about half the width of the visible part of the per-process bars''' @@ -867,9 +879,8 @@ def draw_process_events(ctx, proc, proc_tree, x, y): if ctx.SWEEP_CSEC and ev.raw_log_seek: delta_csec = float(ev.time_usec)/1000/10 - time_origin_relative if delta_csec >= 0 and delta_csec < width_csec: - # ctx.cr.set_source_rgba(*MAGENTA) - # W,H = 2,8 - if ctx.SWEEP_render_serial == ctx.render_serial: + # don't dump synthetic events + if ev.raw_log_seek and ctx.SWEEP_render_serial == ctx.render_serial: ctx.event_dump_list.append(ev) ctx.cr.move_to(tx-W, y+C.proc_h) # bottom-left ctx.cr.rel_line_to(W,-H) # top From 68f9d8555f03fd25e48f9e847430f8b459ef1def Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:16 -0800 Subject: [PATCH 121/182] events -- regex match highlights rather than selects --- pybootchartgui/draw.py | 14 ++++++++++---- pybootchartgui/main.py.in | 9 ++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 07bb67d..9ac39c2 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -107,7 +107,9 @@ PROC_TEXT_FONT_SIZE = 12 # Event tick color. -EVENT_COLOR = (0.0, 0.0, 0.0, 1.0) +DIM_EVENT_COLOR = (0.3, 0.3, 0.3) +EVENT_COLOR = (0.1, 0.1, 0.1) +HIGHLIGHT_EVENT_COLOR = (2.0, 0.0, 4.0) # Signature color. SIG_COLOR = (0.0, 0.0, 0.0, 0.3125) @@ -231,6 +233,7 @@ def per_render_init(self, cr, time_origin_drawn, SEC_W): self.cr = cr self.time_origin_drawn = time_origin_drawn self.SEC_W = SEC_W + self.highlight_event__func_file_line_RE = re.compile(self.app_options.event_regex) def proc_tree (self, trace): return trace.kernel_tree if self.kernel_only else trace.proc_tree @@ -858,7 +861,6 @@ def usec_to_csec(usec): return float(usec) / 1000 / 10 def draw_process_events(ctx, proc, proc_tree, x, y): - ev_regex = re.compile(ctx.app_options.event_regex) ev_list = [(ev, csec_to_xscaled(ctx, usec_to_csec(ev.time_usec))) for ev in proc.events] if not ev_list: @@ -874,8 +876,12 @@ def draw_process_events(ctx, proc, proc_tree, x, y): width_csec = sweep_window_width_sec(ctx) * C.CSEC # draw ticks, maybe dump log line for (ev, tx) in ev_list: - ctx.cr.set_source_rgba(*EVENT_COLOR) - W,H = 1,5 + if re.search(ctx.highlight_event__func_file_line_RE, ev.func_file_line): + ctx.cr.set_source_rgba(*HIGHLIGHT_EVENT_COLOR) + W,H = 2,8 + else: + ctx.cr.set_source_rgba(*EVENT_COLOR) + W,H = 1,5 if ctx.SWEEP_CSEC and ev.raw_log_seek: delta_csec = float(ev.time_usec)/1000/10 - time_origin_relative if delta_csec >= 0 and delta_csec < width_csec: diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 6524f2b..9895f41 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -81,11 +81,10 @@ def _mk_options_parser(): help="relocate the text within process bars (left, center)") # event plotting - parser.add_option("-e", "--events", dest="event_regex", metavar="REGEX", default=".*", - help="Plot only events matching REGEX." + - " The regular expression is anchored to beginning and end of line," + - "and syntax is similar to grep 'extended' REs." + - " So to match FOO or BAR anywhere, use '.*(FOO|BAR).*'." + + parser.add_option("-e", "--events", dest="event_regex", metavar="REGEX", default="^$", + help="Highlight events matching REGEX." + + " Syntax is similar to grep 'extended' REs." + + " To match FOO or BAR anywhere on the log line, use 'FOO|BAR'." + " (file:///usr/share/doc/python2.6/html/library/re.htm)") parser.add_option("--hide-events", action="store_true", dest="hide_events", default=False, help="hide event ticks (small black triangles)") From 43612ed47a3a7e20bc3faf64dd02b40ff792f095 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:16 -0800 Subject: [PATCH 122/182] event window -- drag to width --- pybootchartgui/draw.py | 52 +++++++++++++++------------------------ pybootchartgui/gui.py | 26 +++++++++++++++++--- pybootchartgui/main.py.in | 5 ++++ 3 files changed, 48 insertions(+), 35 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 9ac39c2..481a6cc 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -219,14 +219,13 @@ def __init__(self, app_options, trace, cumulative = True, charts = True, kernel_ self.charts = charts self.kernel_only = kernel_only # set iff collector daemon saved output of `dmesg` self.SWEEP_CSEC = None + self.event_dump_list = None self.cr = None # Cairo rendering context self.time_origin_drawn = None # time of leftmost plotted data, as integer csecs self.SEC_W = None - # per-rendering state, including recursive process tree traversal - self.SWEEP_render_serial = None - self.render_serial = 0 + # per-rendering state self.proc_above_was_hidden = False def per_render_init(self, cr, time_origin_drawn, SEC_W): @@ -568,16 +567,12 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): (w, h) = extents(ctx, xscale, trace) ctx.per_render_init(cr, _time_origin_drawn(ctx, trace), _sec_w(xscale)) - ctx.event_dump_list = [] + ctx.SWEEP_CSEC = sweep_csec ctx.cr.set_line_width(1.0) ctx.cr.select_font_face(FONT_NAME) draw_fill_rect(ctx.cr, WHITE, (0, 0, max(w, C.MIN_IMG_W), h)) - if sweep_csec and sweep_csec != ctx.SWEEP_CSEC: - ctx.SWEEP_render_serial = ctx.render_serial - ctx.SWEEP_CSEC = sweep_csec - ctx.cr.save() late_init_transform(ctx.cr) @@ -623,15 +618,14 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): draw_sweep(ctx, sweep_csec[0], sweep_csec[1] - sweep_csec[0]) #dump_pseudo_event(ctx, "start of event window, width " + int(width*1000) + "msec") - ctx.render_serial += 1 - ctx.cr.restore() - if False: # dump events only - ctx.event_dump_list.sort(key = lambda e: e.time_usec) - for ev in ctx.event_dump_list: - print ev.raw_log_line(), - else: # dump all raw log lines between events, where "between" means merely textually, + if ctx.event_dump_list == None: + return + + print # blank, separator line + if ctx.app_options.dump_raw_event_context: + # dump all raw log lines between events, where "between" means merely textually, # irrespective of time. ctx.event_dump_list.sort(key = lambda e: e.raw_log_seek) if len(ctx.event_dump_list): @@ -641,11 +635,12 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): #for line in event0.raw_log_file.readline(): while event0.raw_log_file.tell() <= eventN.raw_log_seek: print event0.raw_log_file.readline(), + else: # dump events only + ctx.event_dump_list.sort(key = lambda ev: ev.time_usec) + for ev in ctx.event_dump_list: + print ev.time_usec, ".", ev.raw_log_line(), -def sweep_window_width_sec(ctx): - '''about half the width of the visible part of the per-process bars''' - user_width = ctx.cr.device_to_user_distance(500,0)[0] - return float(user_width) / ctx.SEC_W + ctx.event_dump_list = None def draw_sweep(ctx, sweep_csec, width_csec): def draw_shading(cr, rect): @@ -704,11 +699,6 @@ def draw_process_bar_chart(ctx, proc_tree, times, curr_y, w, h): draw_hidden_process_separator(ctx, curr_y) ctx.proc_above_was_hidden = False - if ctx.SWEEP_CSEC and ctx.SWEEP_render_serial == ctx.render_serial: - # mark end of this batch of events, for the benefit of post-processors - print - sys.stdout.flush() - def draw_header (ctx, headers, duration): toshow = [ ('system.uname', 'uname', lambda s: s), @@ -873,8 +863,7 @@ def draw_process_events(ctx, proc, proc_tree, x, y): # align to time of first sample time_origin_relative = ctx.time_origin_drawn + proc_tree.sample_period - width_csec = sweep_window_width_sec(ctx) * C.CSEC - # draw ticks, maybe dump log line + # draw ticks, maybe add to dump list for (ev, tx) in ev_list: if re.search(ctx.highlight_event__func_file_line_RE, ev.func_file_line): ctx.cr.set_source_rgba(*HIGHLIGHT_EVENT_COLOR) @@ -882,12 +871,11 @@ def draw_process_events(ctx, proc, proc_tree, x, y): else: ctx.cr.set_source_rgba(*EVENT_COLOR) W,H = 1,5 - if ctx.SWEEP_CSEC and ev.raw_log_seek: - delta_csec = float(ev.time_usec)/1000/10 - time_origin_relative - if delta_csec >= 0 and delta_csec < width_csec: - # don't dump synthetic events - if ev.raw_log_seek and ctx.SWEEP_render_serial == ctx.render_serial: - ctx.event_dump_list.append(ev) + # don't dump synthetic events + if ctx.event_dump_list != None and ctx.SWEEP_CSEC and ev.raw_log_seek: + ev_time_csec = float(ev.time_usec)/1000/10 + if ev_time_csec >= ctx.SWEEP_CSEC[0] and ev_time_csec < ctx.SWEEP_CSEC[1]: + ctx.event_dump_list.append(ev) ctx.cr.move_to(tx-W, y+C.proc_h) # bottom-left ctx.cr.rel_line_to(W,-H) # top ctx.cr.rel_line_to(W, H) # bottom-right diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index 82afa31..6aa1d1e 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -177,6 +177,10 @@ def absolute_uptime_event_times(self, button): self.drawctx.app_options.absolute_uptime_event_times = button.get_property ('active') self.queue_draw() + def dump_raw_event_context(self, button): + self.drawctx.app_options.dump_raw_event_context = button.get_property ('active') + self.queue_draw() + POS_INCREMENT = 100 def on_key_press_event(self, widget, event): @@ -204,11 +208,10 @@ def on_area_button_press(self, area, event): self.queue_draw() if event.button == 3: if not self.sweep_csec: - self.sweep_csec = [self.device_to_csec_user_y(event.x, 0)[0], - self.device_to_csec_user_y(event.x + 500, 0)[0]] + self.sweep_csec = [self.device_to_csec_user_y(event.x, 0)[0], None] else: self.sweep_csec = None - self.queue_draw() + self.queue_draw() if event.type not in (gtk.gdk.BUTTON_PRESS, gtk.gdk.BUTTON_RELEASE): return False return False @@ -219,6 +222,15 @@ def on_area_button_release(self, area, event): self.prevmousex = None self.prevmousey = None return True + if event.button == 3: + if self.sweep_csec: + self.sweep_csec[1] = self.device_to_csec_user_y(event.x, 0)[0] + # if no motion between click and release, draw a one-sided sweep window, and don't dump events + if self.sweep_csec[1] == self.sweep_csec[0]: + self.sweep_csec[1] = self.device_to_csec_user_y(self.trace.ps_stats.end_time, 0)[0] + else: + self.drawctx.event_dump_list = [] + self.queue_draw() return False def on_area_scroll_event(self, area, event): @@ -250,6 +262,9 @@ def on_area_motion_notify(self, area, event): self.prevmousex = x self.prevmousey = y self.position_changed() + elif state & gtk.gdk.BUTTON3_MASK and self.sweep_csec: + self.sweep_csec[1] = self.device_to_csec_user_y(event.x, 0)[0] + self.queue_draw() return True def on_set_scroll_adjustments(self, area, hadj, vadj): @@ -401,6 +416,11 @@ def gtk_CheckButton(name): button.set_active (drawctx.app_options.absolute_uptime_event_times) hbox.pack_start (button, False) + button = gtk_CheckButton("dump raw event context") + button.connect ('toggled', self.widget.dump_raw_event_context) + button.set_active (drawctx.app_options.dump_raw_event_context) + hbox.pack_start (button, False) + self.pack_start(hbox, False) self.pack_start(scrolled) self.show_all() diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 9895f41..bb5e3fe 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -92,6 +92,11 @@ def _mk_options_parser(): help="print time of each event, inside the box of the reporting process") parser.add_option("--absolute-uptime-event-times", action="store_true", dest="absolute_uptime_event_times", default=False, help="print time of each event, inside the box of the reporting process") + + parser.add_option("--dump-raw-event-context", action="store_true", dest="dump_raw_event_context", default=False, + help="in addition to log lines for selected events, dump log lines as well," + + " retaining original log sort order, which may not be temporally correct.") + parser.add_option("--synthesize-sample-start-events", action="store_true", dest="synthesize_sample_start_events", help="synthesize an event marking each boundary between sample periods -- " + "helpful in analyzing collector timing issues") From caee2550c063b52ad8712ec3145f1a30a963c569 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:17 -0800 Subject: [PATCH 123/182] events -- add -- hide less active option -- allow pruning of threads with events --- pybootchartgui/main.py.in | 10 +++++----- pybootchartgui/parsing.py | 29 ++++++++++++++--------------- pybootchartgui/process_tree.py | 29 +++++++++++++++++------------ pybootchartgui/samples.py | 13 +++++++++---- 4 files changed, 45 insertions(+), 36 deletions(-) diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index bb5e3fe..9c27874 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -55,11 +55,11 @@ def _mk_options_parser(): parser.add_option("--merge", dest="merge", default=False, help="Prune the process tree of all activity from dull children: " + "stats are added to parent, child process is lost in drawing of chart.") - parser.add_option("--hide-processes", dest="hide_processes", default="idle", - choices=["none", "idle"], - help="Hide uninteresting processes on initial rendering, " + - "though recoverable in GUI by mouse click: " + - "\"none\", show all; [\"%default\"], hide idle processes.") + parser.add_option("-A", "--show-active", dest="show_active", type="int", default=1, metavar="N", + help="Suppress hiding of any thread active in more than N of the samples collector." + + " Hiding means it's not shown on initial rendering, but can be shown by a mouse click.") + parser.add_option("-C", "--show-high-CPU", dest="show_high_CPU", type="int", default=1, metavar="CSEC", + help="Suppress hiding of any thread consuming more than CSEC of CPU time.") parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="suppress informational messages") parser.add_option("--verbose", action="store_true", dest="verbose", default=False, diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 1e80012..0dd8264 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -269,11 +269,12 @@ def parse(block): blocks = file.read().split('\n\n') return [parse(block) for block in blocks if block.strip() and not block.endswith(' not running\n')] -def _handle_sample(processMap, writer, ltime, - time, pid, tid, cmd, state, ppid, userCpu, sysCpu, starttime): +def _handle_sample(processMap, writer, ltime, time, + pid, tid, cmd, state, ppid, userCpu, sysCpu, starttime): if tid in processMap: process = processMap[tid] process.cmd = cmd.strip('()') # why rename after latest name?? + userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) else: if time < starttime: # large values signify a collector problem, e.g. resource starvation @@ -282,14 +283,12 @@ def _handle_sample(processMap, writer, ltime, process = Process(writer, pid, tid, cmd.strip('()'), ppid, starttime) processMap[tid] = process + process.first_user_cpu_time = userCpu + process.first_sys_cpu_time = sysCpu + userCpuLoad, sysCpuLoad = 0, 0 - if process.last_user_cpu_time is not None and process.last_sys_cpu_time is not None: - if ltime is None: - userCpuLoad, sysCpuLoad = 0, 0 - else: - userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) - cpuSample = ProcessCPUSample('null', userCpuLoad, sysCpuLoad, 0.0, 0.0) - process.samples.append(ProcessSample(time, state, cpuSample)) + cpuSample = ProcessCPUSample('null', userCpuLoad, sysCpuLoad, 0.0, 0.0) + process.samples.append(ProcessSample(time, state, cpuSample)) process.last_user_cpu_time = userCpu process.last_sys_cpu_time = sysCpu @@ -304,7 +303,7 @@ def _parse_proc_ps_log(options, writer, file): * kstkesp, kstkeip} """ processMap = {} - ltime = 0 + ltime = None timed_blocks = _parse_timed_blocks(file) for time, lines in timed_blocks: for line in lines: @@ -320,8 +319,8 @@ def _parse_proc_ps_log(options, writer, file): # magic fixed point-ness ... pid *= 1000 ppid *= 1000 - processMap = _handle_sample(processMap, writer, ltime, - time, pid, pid, cmd, state, ppid, userCpu, sysCpu, starttime) + processMap = _handle_sample(processMap, writer, ltime, time, + pid, pid, cmd, state, ppid, userCpu, sysCpu, starttime) ltime = time if len (timed_blocks) < 2: @@ -354,7 +353,7 @@ def _parse_proc_ps_threads_log(options, writer, file): * 16 scheduling_policy """ processMap = {} - ltime = 0 + ltime = None timed_blocks = _parse_timed_blocks(file) for time, lines in timed_blocks: for line in lines: @@ -373,8 +372,8 @@ def _parse_proc_ps_threads_log(options, writer, file): tid *= 1000 ppid *= 1000 - processMap = _handle_sample(processMap, writer, ltime, - time, pid, tid, cmd, state, ppid, userCpu, sysCpu, starttime) + processMap = _handle_sample(processMap, writer, ltime, time, + pid, tid, cmd, state, ppid, userCpu, sysCpu, starttime) ltime = time if len (timed_blocks) < 2: diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index e27f72d..fa67e49 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -63,6 +63,7 @@ def __init__(self, writer, kernel, psstats, sample_period, self.start_time = self.get_start_time(self.process_tree) self.end_time = self.get_end_time(self.process_tree) + self.options = options self.idle = idle if for_testing: @@ -74,14 +75,15 @@ def __init__(self, writer, kernel, psstats, sample_period, removed = self.merge_logger(self.process_tree, self.LOGGER_PROC, monitoredApp, False) writer.status("merged %i logger processes" % removed) - if options.hide_processes != "none": - p_processes = self.prune(self.process_tree, None, self.is_idle_background_process_without_children) - if options.merge: - p_exploders = self.merge_exploders(self.process_tree, self.EXPLODER_PROCESSES) - p_threads = self.merge_siblings(self.process_tree) - p_runs = self.merge_runs(self.process_tree) - writer.status("pruned %i process, %i exploders, %i threads, and %i runs" % - (p_processes, p_exploders, p_threads, p_runs)) + p_processes = self.prune(self.process_tree, None, self.is_inactive_process) + writer.status("hid %i processes" % p_processes) + + if options.merge: + p_exploders = self.merge_exploders(self.process_tree, self.EXPLODER_PROCESSES) + p_threads = self.merge_siblings(self.process_tree) + p_runs = self.merge_runs(self.process_tree) + writer.status("pruned %i exploders, %i threads, and %i runs" % + (p_exploders, p_threads, p_runs)) self.sort(self.process_tree) @@ -172,10 +174,13 @@ def update_ppids_for_daemons(self, process_list): p.child_list = [] self.build() - def is_idle_background_process_without_children(self, p): - return not p.active and \ - self.num_nodes(p.child_list) == 0 and \ - len(p.events) == 0 # never prune a process that reports an event + def is_inactive_process(self, p): + return p.activeCount < self.options.show_active and \ + p.CPUCount < self.options.show_high_CPU + + def is_inactive_process_without_children(self, p): + return is_inactive_process and \ + self.num_nodes(p.child_list) == 0 def prune(self, process_subtree, parent, pruning_test): """Prunes the process tree by removing idle processes and processes diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 498a68d..923b7b8 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -112,7 +112,10 @@ def __init__(self, writer, pid, tid, cmd, ppid, start_time): self.parent = None self.child_list = [] - self.active = None + self.activeCount = 0 + self.CPUCount = 0 + self.first_user_cpu_time = None + self.first_sys_cpu_time = None self.last_user_cpu_time = None self.last_sys_cpu_time = None @@ -145,9 +148,11 @@ def calc_stats(self, samplePeriod): # one-half of a sample period beyond the instant at which lastSample was taken. self.duration = lastSample.time - self.start_time + samplePeriod / 2 - activeCount = sum( [1 for sample in self.samples if sample.cpu_sample and sample.cpu_sample.sys + sample.cpu_sample.user + sample.cpu_sample.io > 0.0] ) - activeCount = activeCount + sum( [1 for sample in self.samples if sample.state == 'D'] ) - self.active = (activeCount>0) # controls pruning during process_tree creation time + self.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) \ + or sample.state == 'D']) + self.CPUCount = self.last_user_cpu_time + self.last_sys_cpu_time \ + - (self.first_user_cpu_time + self.first_sys_cpu_time) def calc_load(self, userCpu, sysCpu, interval): userCpuLoad = float(userCpu - self.last_user_cpu_time) / interval From d1c67cefd2ba80c40e0e3f85b5cb38ecbd201564 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:17 -0800 Subject: [PATCH 124/182] simplify pruning test --- pybootchartgui/process_tree.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index fa67e49..45f10a2 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -183,24 +183,14 @@ def is_inactive_process_without_children(self, p): self.num_nodes(p.child_list) == 0 def prune(self, process_subtree, parent, pruning_test): - """Prunes the process tree by removing idle processes and processes - that only live for the duration of a single top sample. Sibling - processes with the same command line (i.e. threads) are merged - together. This filters out sleepy background processes, short-lived - processes and bootcharts' analysis tools. - """ n_pruned = 0 idx = 0 while idx < len(process_subtree): p = process_subtree[idx] - if parent != None or len(p.child_list) == 0: - if pruning_test(p): - p.draw = False - n_pruned += 1 + self.prune(p.child_list, p, lambda p: True) - else: - n_pruned += self.prune(p.child_list, p, pruning_test) - else: - n_pruned += self.prune(p.child_list, p, pruning_test) + if pruning_test(p): + p.draw = False + n_pruned += 1 + n_pruned += self.prune(p.child_list, p, pruning_test) idx += 1 return n_pruned From 69f5c44804d6309f4c052e7105a751a802f6287c Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:17 -0800 Subject: [PATCH 125/182] If thread ran at all_ draw at least one user space unit in height --- pybootchartgui/draw.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 481a6cc..5efba5e 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -841,8 +841,11 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w): ctx_save__csec_to_xscaled(ctx) for sample in proc.samples[1:] : normalized = sample.cpu_sample.user + sample.cpu_sample.sys + # XX If thread ran at all, draw at least one user-space unit in height. + height = math.floor(normalized * C.proc_h + 1.0) + draw_fill_rect(ctx.cr, PROC_COLOR_R, (last_time, y+C.proc_h, - sample.time - last_time, -normalized * C.proc_h)) + sample.time - last_time, -height)) last_time = sample.time ctx.cr.restore() From 558089004ef7b9c1ed1b3de1bd8942a89a6e9126 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:17 -0800 Subject: [PATCH 126/182] minimum activity mark.first attempt --- pybootchartgui/draw.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 5efba5e..f129dcf 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -841,11 +841,23 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w): ctx_save__csec_to_xscaled(ctx) for sample in proc.samples[1:] : normalized = sample.cpu_sample.user + sample.cpu_sample.sys - # XX If thread ran at all, draw at least one user-space unit in height. - height = math.floor(normalized * C.proc_h + 1.0) + if normalized > 0: + width = sample.time - last_time + height = normalized * C.proc_h + draw_fill_rect(ctx.cr, PROC_COLOR_R, (last_time, y+C.proc_h, width, -height)) + + # XX If thread ran at all, draw a pair of tick marks, in case rect was too short to resolve. + tick_width = width/3 + tick_height = C.proc_h/3 + #draw_fill_rect(ctx.cr, PROC_COLOR_R, (last_time, y+C.proc_h, width/6, -tick_height)) + #draw_fill_rect(ctx.cr, PROC_COLOR_R, (last_time + width, y+C.proc_h, -width/6, -tick_height)) + + ctx.cr.move_to(last_time + width/2, y+C.proc_h-tick_height) + ctx.cr.rel_line_to(-tick_width/2, tick_height) + ctx.cr.rel_line_to(tick_width, 0) + ctx.cr.close_path() + ctx.cr.fill() - draw_fill_rect(ctx.cr, PROC_COLOR_R, (last_time, y+C.proc_h, - sample.time - last_time, -height)) last_time = sample.time ctx.cr.restore() From e9a28c858e51b63f28085450a08797a1cf7cb0f1 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:17 -0800 Subject: [PATCH 127/182] FIX collect CPU time of first sample --- pybootchartgui/draw.py | 13 +++++------ pybootchartgui/parsing.py | 47 +++++++++++++++++++++------------------ pybootchartgui/samples.py | 4 ++-- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index f129dcf..43bef53 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -830,27 +830,26 @@ def draw_again(): ctx.cr.restore() def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w): + # draw_fill_rect(ctx.cr, PROC_COLOR_S, (x, y, w, C.proc_h)) - if len(proc.samples) <= 0: - return + # cases: # 1. proc started before sampling did # XX should look up time of previous sample, not assume 'proc_tree.sample_period' # 2. proc start after sampling - last_time = max(proc.start_time, proc.samples[0].time - proc_tree.sample_period) + last_time = max(proc.start_time, + proc.samples[0].time - proc_tree.sample_period) ctx_save__csec_to_xscaled(ctx) - for sample in proc.samples[1:] : + for sample in proc.samples: normalized = sample.cpu_sample.user + sample.cpu_sample.sys if normalized > 0: width = sample.time - last_time height = normalized * C.proc_h draw_fill_rect(ctx.cr, PROC_COLOR_R, (last_time, y+C.proc_h, width, -height)) - # XX If thread ran at all, draw a pair of tick marks, in case rect was too short to resolve. + # If thread ran at all, draw a pair of tick marks, in case rect was too short to resolve. tick_width = width/3 tick_height = C.proc_h/3 - #draw_fill_rect(ctx.cr, PROC_COLOR_R, (last_time, y+C.proc_h, width/6, -tick_height)) - #draw_fill_rect(ctx.cr, PROC_COLOR_R, (last_time + width, y+C.proc_h, -width/6, -tick_height)) ctx.cr.move_to(last_time + width/2, y+C.proc_h-tick_height) ctx.cr.rel_line_to(-tick_width/2, tick_height) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 0dd8264..39eec5d 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -271,28 +271,31 @@ def parse(block): def _handle_sample(processMap, writer, ltime, time, pid, tid, cmd, state, ppid, userCpu, sysCpu, starttime): - if tid in processMap: - process = processMap[tid] - process.cmd = cmd.strip('()') # why rename after latest name?? - userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) - else: - if time < starttime: - # large values signify a collector problem, e.g. resource starvation - writer.status("time (%dcs) < starttime (%dcs), diff %d -- TID %d" % - (time, starttime, time-starttime, tid/1000)) - - process = Process(writer, pid, tid, cmd.strip('()'), ppid, starttime) - processMap[tid] = process - process.first_user_cpu_time = userCpu - process.first_sys_cpu_time = sysCpu - userCpuLoad, sysCpuLoad = 0, 0 - - cpuSample = ProcessCPUSample('null', userCpuLoad, sysCpuLoad, 0.0, 0.0) - process.samples.append(ProcessSample(time, state, cpuSample)) - - process.last_user_cpu_time = userCpu - process.last_sys_cpu_time = sysCpu - return processMap + if tid in processMap: + process = processMap[tid] + process.cmd = cmd.strip('()') # XX loses name changes prior to the final sample + else: + if time < starttime: + # large values signify a collector problem, e.g. resource starvation + writer.status("time (%dcs) < starttime (%dcs), diff %d -- TID %d" % + (time, starttime, time-starttime, tid/1000)) + + process = Process(writer, pid, tid, cmd.strip('()'), ppid, starttime) + processMap[tid] = process + process.first_user_cpu_time = userCpu + process.first_sys_cpu_time = sysCpu + + if ltime == None: # collector startup, not usually coinciding with thread startup + userCpuLoad, sysCpuLoad = 0, 0 + else: + userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) + + cpuSample = ProcessCPUSample('null', userCpuLoad, sysCpuLoad, 0.0, 0.0) + process.samples.append(ProcessSample(time, state, cpuSample)) + + process.last_user_cpu_time = userCpu + process.last_sys_cpu_time = sysCpu + return processMap def _parse_proc_ps_log(options, writer, file): """ diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 923b7b8..5a6e8ea 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -116,8 +116,8 @@ def __init__(self, writer, pid, tid, cmd, ppid, start_time): self.CPUCount = 0 self.first_user_cpu_time = None self.first_sys_cpu_time = None - self.last_user_cpu_time = None - self.last_sys_cpu_time = None + self.last_user_cpu_time = 0 + self.last_sys_cpu_time = 0 self.last_cpu_ns = 0 self.last_blkio_delay_ns = 0 From 322af7053cd52c9ea2a7e71d6f59723ef1ab84c4 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:18 -0800 Subject: [PATCH 128/182] remove show active option -- record cuser+csys times --- pybootchartgui/main.py.in | 6 ++---- pybootchartgui/parsing.py | 26 ++++++++++++++++---------- pybootchartgui/process_tree.py | 11 +++++++---- pybootchartgui/samples.py | 31 ++++++++++++++++++------------- 4 files changed, 43 insertions(+), 31 deletions(-) diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 9c27874..31c61f7 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -55,11 +55,9 @@ def _mk_options_parser(): parser.add_option("--merge", dest="merge", default=False, help="Prune the process tree of all activity from dull children: " + "stats are added to parent, child process is lost in drawing of chart.") - parser.add_option("-A", "--show-active", dest="show_active", type="int", default=1, metavar="N", - help="Suppress hiding of any thread active in more than N of the samples collector." + - " Hiding means it's not shown on initial rendering, but can be shown by a mouse click.") parser.add_option("-C", "--show-high-CPU", dest="show_high_CPU", type="int", default=1, metavar="CSEC", - help="Suppress hiding of any thread consuming more than CSEC of CPU time.") + help="Suppress hiding of any thread consuming more than CSEC of CPU time." + + " Hiding means it's not shown on initial rendering, but can be shown by a mouse click.") parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="suppress informational messages") parser.add_option("--verbose", action="store_true", dest="verbose", default=False, diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 39eec5d..1b59292 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -270,7 +270,7 @@ def parse(block): return [parse(block) for block in blocks if block.strip() and not block.endswith(' not running\n')] def _handle_sample(processMap, writer, ltime, time, - pid, tid, cmd, state, ppid, userCpu, sysCpu, starttime): + pid, tid, cmd, state, ppid, userCpu, sysCpu, c_userCpu, c_sysCpu, starttime): if tid in processMap: process = processMap[tid] process.cmd = cmd.strip('()') # XX loses name changes prior to the final sample @@ -282,19 +282,21 @@ def _handle_sample(processMap, writer, ltime, time, process = Process(writer, pid, tid, cmd.strip('()'), ppid, starttime) processMap[tid] = process - process.first_user_cpu_time = userCpu - process.first_sys_cpu_time = sysCpu + process.user_cpu_time[0] = userCpu + process.sys_cpu_time[0] = sysCpu if ltime == None: # collector startup, not usually coinciding with thread startup - userCpuLoad, sysCpuLoad = 0, 0 + userCpuLoad, sysCpuLoad, c_userCpuLoad, c_sysCpuLoad = 0, 0, 0, 0 else: userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) cpuSample = ProcessCPUSample('null', userCpuLoad, sysCpuLoad, 0.0, 0.0) process.samples.append(ProcessSample(time, state, cpuSample)) - process.last_user_cpu_time = userCpu - process.last_sys_cpu_time = sysCpu + process.user_cpu_time[-1] = userCpu + process.sys_cpu_time[-1] = sysCpu + process.c_user_cpu_time[-1] = c_userCpu + process.c_sys_cpu_time[-1] = c_sysCpu return processMap def _parse_proc_ps_log(options, writer, file): @@ -317,13 +319,15 @@ def _parse_proc_ps_log(options, writer, file): offset = [index for index, token in enumerate(tokens[1:]) if token[-1] == ')'][0] pid, cmd, state, ppid = int(tokens[0]), ' '.join(tokens[1:2+offset]), tokens[2+offset], int(tokens[3+offset]) - userCpu, sysCpu, starttime = int(tokens[13+offset]), int(tokens[14+offset]), int(tokens[21+offset]) + userCpu, sysCpu = int(tokens[13+offset]), int(tokens[14+offset]), + c_userCpu, c_sysCpu = int(tokens[15+offset]), int(tokens[16+offset]) + starttime = int(tokens[21+offset]) # magic fixed point-ness ... pid *= 1000 ppid *= 1000 processMap = _handle_sample(processMap, writer, ltime, time, - pid, pid, cmd, state, ppid, userCpu, sysCpu, starttime) + pid, pid, cmd, state, ppid, userCpu, sysCpu, c_userCpu, c_sysCpu, starttime) ltime = time if len (timed_blocks) < 2: @@ -368,7 +372,9 @@ def _parse_proc_ps_threads_log(options, writer, file): offset = [index for index, token in enumerate(tokens[2:]) if (len(token) > 0 and token[-1] == ')')][0] pid, tid, cmd, state, ppid = int(tokens[0]), int(tokens[1]), ' '.join(tokens[2:3+offset]), tokens[3+offset], int(tokens[4+offset]) - userCpu, sysCpu, starttime = int(tokens[7+offset]), int(tokens[8+offset]), int(tokens[13+offset]) + userCpu, sysCpu = int(tokens[7+offset]), int(tokens[8+offset]) + c_userCpu, c_sysCpu = int(tokens[9+offset]), int(tokens[10+offset]) + starttime = int(tokens[13+offset]) # magic fixed point-ness ... pid *= 1000 @@ -376,7 +382,7 @@ def _parse_proc_ps_threads_log(options, writer, file): ppid *= 1000 processMap = _handle_sample(processMap, writer, ltime, time, - pid, tid, cmd, state, ppid, userCpu, sysCpu, starttime) + pid, tid, cmd, state, ppid, userCpu, sysCpu, c_userCpu, c_sysCpu, starttime) ltime = time if len (timed_blocks) < 2: diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index 45f10a2..c36b2d1 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -75,7 +75,7 @@ def __init__(self, writer, kernel, psstats, sample_period, removed = self.merge_logger(self.process_tree, self.LOGGER_PROC, monitoredApp, False) writer.status("merged %i logger processes" % removed) - p_processes = self.prune(self.process_tree, None, self.is_inactive_process) + p_processes = self.prune(self.process_tree, None, self.is_inactive_process_without_children) writer.status("hid %i processes" % p_processes) if options.merge: @@ -175,13 +175,16 @@ def update_ppids_for_daemons(self, process_list): self.build() def is_inactive_process(self, p): - return p.activeCount < self.options.show_active and \ - p.CPUCount < self.options.show_high_CPU + return p.CPUCount() < self.options.show_high_CPU def is_inactive_process_without_children(self, p): - return is_inactive_process and \ + return self.is_inactive_process(p) and \ self.num_nodes(p.child_list) == 0 + def is_inactive_process_with_inactive_children(self, p): + return self.is_inactive_process(p) and \ + p.c_CPUCount() < self.options.show_high_CPU + def prune(self, process_subtree, parent, pruning_test): n_pruned = 0 idx = 0 diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 5a6e8ea..55aed15 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -57,8 +57,8 @@ def __init__(self, time, user, sys, io, swap): self.time = time self.user = user self.sys = sys - self.io = io - self.swap = swap + self.io = io # taskstats-specific + self.swap = swap # taskstats-specific @property def cpu(self): @@ -112,12 +112,11 @@ def __init__(self, writer, pid, tid, cmd, ppid, start_time): self.parent = None self.child_list = [] - self.activeCount = 0 - self.CPUCount = 0 - self.first_user_cpu_time = None - self.first_sys_cpu_time = None - self.last_user_cpu_time = 0 - self.last_sys_cpu_time = 0 + + self.user_cpu_time = [-1, -1] + self.sys_cpu_time = [-1, -1] + self.c_user_cpu_time = [-1, -1] + self.c_sys_cpu_time = [-1, -1] self.last_cpu_ns = 0 self.last_blkio_delay_ns = 0 @@ -125,7 +124,15 @@ def __init__(self, writer, pid, tid, cmd, ppid, start_time): self.draw = True # dynamic, view-dependent per-process state boolean - # split this process' run - triggered by a name change + def CPUCount(self): + return self.user_cpu_time[-1] + self.sys_cpu_time[-1] \ + - (self.user_cpu_time[0] + self.sys_cpu_time[0]) + + def c_CPUCount(self): + return self.c_user_cpu_time[-1] + self.c_sys_cpu_time[-1] \ + - (self.c_user_cpu_time[0] + self.c_sys_cpu_time[0]) + + # split this process' run - triggered by a name change # XX called only if taskstats.log is provided (bootchart2 daemon) def split(self, writer, pid, cmd, ppid, start_time): split = Process (writer, pid, cmd, ppid, start_time) @@ -151,12 +158,10 @@ def calc_stats(self, samplePeriod): self.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) \ or sample.state == 'D']) - self.CPUCount = self.last_user_cpu_time + self.last_sys_cpu_time \ - - (self.first_user_cpu_time + self.first_sys_cpu_time) def calc_load(self, userCpu, sysCpu, interval): - userCpuLoad = float(userCpu - self.last_user_cpu_time) / interval - sysCpuLoad = float(sysCpu - self.last_sys_cpu_time) / interval + userCpuLoad = float(userCpu - self.user_cpu_time[-1]) / interval + sysCpuLoad = float(sysCpu - self.sys_cpu_time[-1]) / interval cpuLoad = userCpuLoad + sysCpuLoad # normalize if cpuLoad > 1.0: From dd85224a376fb52eec9e37010a07347d8e737fea Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:18 -0800 Subject: [PATCH 129/182] FIX dont hide numerical issue --- pybootchartgui/samples.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 55aed15..2686c2a 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -162,11 +162,6 @@ def calc_stats(self, samplePeriod): def calc_load(self, userCpu, sysCpu, interval): userCpuLoad = float(userCpu - self.user_cpu_time[-1]) / interval sysCpuLoad = float(sysCpu - self.sys_cpu_time[-1]) / interval - cpuLoad = userCpuLoad + sysCpuLoad - # normalize - if cpuLoad > 1.0: - userCpuLoad = userCpuLoad / cpuLoad - sysCpuLoad = sysCpuLoad / cpuLoad return (userCpuLoad, sysCpuLoad) def set_parent(self, processMap): From 2ddd8ff24f7c43a7dcd223200f36a13c34cdf89a Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:18 -0800 Subject: [PATCH 130/182] draw process duration more faithfully --- pybootchartgui/draw.py | 12 +++++++++--- pybootchartgui/samples.py | 5 ++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 43bef53..3ce6237 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -776,7 +776,7 @@ def draw_processes_recursively(ctx, proc, proc_tree, y): xmin = ctx.cr.device_to_user(0, 0)[0] # work around numeric overflow at high xscale factors xmin = max(xmin, 0) x = max(xmin, csec_to_xscaled(ctx, proc.start_time)) - w = max(xmin, csec_to_xscaled(ctx, proc.start_time + proc.duration)) - x # XX parser fudges duration upward + w = max(xmin, csec_to_xscaled(ctx, proc.start_time + proc.duration)) - x if ctx.hide_process_y: if ctx.hide_process_y < y - C.proc_h/4: @@ -830,16 +830,22 @@ def draw_again(): ctx.cr.restore() def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w): - # draw_fill_rect(ctx.cr, PROC_COLOR_S, (x, y, w, C.proc_h)) + ctx_save__csec_to_xscaled(ctx) + # draw visual reminder of unknowability of thread end time + ctx.cr.move_to(proc.samples[-1].time + proc_tree.sample_period, y+C.proc_h/2) + ctx.cr.line_to(proc.samples[-1].time, y+C.proc_h) + ctx.cr.line_to(proc.samples[-1].time, y) + ctx.cr.close_path() + ctx.cr.fill() + # cases: # 1. proc started before sampling did # XX should look up time of previous sample, not assume 'proc_tree.sample_period' # 2. proc start after sampling last_time = max(proc.start_time, proc.samples[0].time - proc_tree.sample_period) - ctx_save__csec_to_xscaled(ctx) for sample in proc.samples: normalized = sample.cpu_sample.user + sample.cpu_sample.sys if normalized > 0: diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 2686c2a..83d4e1e 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -151,9 +151,8 @@ def calc_stats(self, samplePeriod): firstSample = self.samples[0] lastSample = self.samples[-1] self.start_time = min(firstSample.time, self.start_time) - # self.duration is a heuristic: process may be expected to continue running at least - # one-half of a sample period beyond the instant at which lastSample was taken. - self.duration = lastSample.time - self.start_time + samplePeriod / 2 + # self.duration is the _minimum_ known duration of the thread + self.duration = lastSample.time - self.start_time self.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) \ From 191c7d47436a92fc469009941d98662d33aa2f4e Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:18 -0800 Subject: [PATCH 131/182] simplify process label positioning --- pybootchartgui/draw.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 3ce6237..f09a54e 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -142,6 +142,7 @@ PROC_COLOR_T, PROC_COLOR_Z, PROC_COLOR_X, PROC_COLOR_W] JUSTIFY_LEFT = "left" +JUSTIFY_CENTER = "center" # CumulativeStats Types STAT_TYPE_CPU = 0 @@ -249,15 +250,10 @@ def draw_label_in_box(self, color, label, label_y_advance = extents[5] y += C.proc_h - if self.app_options.justify == JUSTIFY_LEFT: - label_x = x - label_w - else: + if self.app_options.justify == JUSTIFY_CENTER and label_w + 10 <= w: label_x = x + w / 2 - label_w / 2 # CENTER - - if label_w + 10 > w: # if wider than the process box - label_x = x + w + 5 # push outside to right - if label_x + label_w > maxx: # if that's too far right - label_x = x - label_w - 5 # push outside to the left + else: + label_x = x - label_w - 2 if label_x < minx: label_x = minx # XX ugly magic constants, tuned by trial-and-error From 23b4bc8edae2a3f63353f04b801087fc19e20173 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:19 -0800 Subject: [PATCH 132/182] show soft left border for preexisting threads --- pybootchartgui/draw.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index f09a54e..b194f80 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -736,7 +736,11 @@ def draw_process(ctx, proc, proc_tree, x, y, w): ctx.cr.set_line_width(1.0) ctx.cr.move_to(x+w, y) ctx.cr.rel_line_to(-w, 0) - ctx.cr.rel_line_to(0, C.proc_h) + if proc.start_time < ctx.time_origin_drawn: + ctx.cr.stroke() + ctx.cr.move_to(x, y+C.proc_h) + else: + ctx.cr.rel_line_to(0, C.proc_h) ctx.cr.rel_line_to(w, 0) ctx.cr.stroke() @@ -806,7 +810,7 @@ def draw_processes_recursively(ctx, proc, proc_tree, y): child_x, child_y = draw_processes_recursively(ctx, child, proc_tree, next_y) if proc.draw and child.draw: # XX draws lines on top of the process name label - draw_process_connecting_lines(ctx, x, y, child_x, child_y) + pass # draw_process_connecting_lines(ctx, x, y, child_x, child_y) next_y += C.proc_h * proc_tree.num_nodes_drawn([child]) # XX why a second recursion? return x, y From 29d0680cfc20d5f966609100eabcabddbb9a71af Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:19 -0800 Subject: [PATCH 133/182] FIX connecting lines cleanup --- pybootchartgui/draw.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index b194f80..76f28da 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -119,7 +119,7 @@ SIGNATURE = "http://github.com/mmeeks/bootchart" # Process dependency line color. -DEP_COLOR = (0.75, 0.75, 0.75, 1.0) +DEP_COLOR = (0.75, 0.6, 0.75, 1.0) # Process dependency line stroke. DEP_STROKE = 1.0 @@ -806,11 +806,13 @@ def draw_processes_recursively(ctx, proc, proc_tree, y): next_y = y + C.proc_h + elder_sibling_y = None for child in proc.child_list: child_x, child_y = draw_processes_recursively(ctx, child, proc_tree, next_y) if proc.draw and child.draw: # XX draws lines on top of the process name label - pass # draw_process_connecting_lines(ctx, x, y, child_x, child_y) + draw_process_connecting_lines(ctx, x, y, child_x, child_y, elder_sibling_y) + elder_sibling_y = child_y next_y += C.proc_h * proc_tree.num_nodes_drawn([child]) # XX why a second recursion? return x, y @@ -940,22 +942,28 @@ def draw_process_state_colors(ctx, proc, proc_tree, x, y, w): ctx.cr.set_source_rgba(*color) draw_diamond(ctx.cr, tx, y + C.proc_h/2, 2.5, C.proc_h) -def draw_process_connecting_lines(ctx, px, py, x, y): +def draw_process_connecting_lines(ctx, px, py, x, y, elder_sibling_y): + ON = 1 + OFF = 2 + DASH_LENGTH = ON + OFF + + ctx.cr.save() ctx.cr.set_source_rgba(*DEP_COLOR) - ctx.cr.set_dash([1, 2]) # XX repeated draws are not phase-synchronized, resulting in a solid line - if abs(px - x) < 3: - dep_off_x = 3 - dep_off_y = C.proc_h / 4 - ctx.cr.move_to(x, y + C.proc_h / 2) - ctx.cr.line_to(px - dep_off_x, y + C.proc_h / 2) - ctx.cr.line_to(px - dep_off_x, py - dep_off_y) - ctx.cr.line_to(px, py - dep_off_y) + ctx.cr.set_dash([ON, OFF]) # repeated draws are not phase-synchronized, resulting in a solid line + ctx.cr.set_line_width(DEP_STROKE) + + ctx.cr.move_to(x, y + C.proc_h / 2) # child's center + # exdent the connecting lines; otherwise the horizontal would be too short to see + dep_off_x = 3 + dep_off_y = 0 # C.proc_h / 4 + ctx.cr.line_to(px - dep_off_x, y + C.proc_h / 2) # leftward + if elder_sibling_y is not None: + ctx.cr.line_to(px - dep_off_x, elder_sibling_y + C.proc_h/2) # upward else: - ctx.cr.move_to(x, y + C.proc_h / 2) - ctx.cr.line_to(px, y + C.proc_h / 2) - ctx.cr.line_to(px, py) + ctx.cr.line_to(px - dep_off_x, py + C.proc_h/2) # upward + ctx.cr.rel_line_to(dep_off_x, 0) # rightward ctx.cr.stroke() - ctx.cr.set_dash([]) + ctx.cr.restore() class CumlSample: def __init__(self, proc): From 25c0282337e40189ff1fbda886cb23076b16853d Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:19 -0800 Subject: [PATCH 134/182] label highlighted events -- cleanup --- pybootchartgui/draw.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 76f28da..b9d47e1 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -26,9 +26,10 @@ # XX The syntax is awkward, but more elegant alternatives have run-time overhead. # http://stackoverflow.com/questions/4996815/ways-to-make-a-class-immutable-in-python DrawConsts = collections.namedtuple('XXtypename', + # |height of a process, in user-space + # | |taskstats-specific ['CSEC','bar_h','off_x','off_y','proc_h','leg_s','CUML_HEIGHT','MIN_IMG_W']) C = DrawConsts( 100, 55, 10, 10, 16, 11, 2000, 800) -# height of a process, in user-space # Derived constants # XX create another namedtuple for these @@ -221,12 +222,14 @@ def __init__(self, app_options, trace, cumulative = True, charts = True, kernel_ self.kernel_only = kernel_only # set iff collector daemon saved output of `dmesg` self.SWEEP_CSEC = None self.event_dump_list = None + self.trace = trace self.cr = None # Cairo rendering context self.time_origin_drawn = None # time of leftmost plotted data, as integer csecs self.SEC_W = None + self.time_origin_relative = None # currently used only to locate events - # per-rendering state + # intra-rendering state self.proc_above_was_hidden = False def per_render_init(self, cr, time_origin_drawn, SEC_W): @@ -235,6 +238,14 @@ def per_render_init(self, cr, time_origin_drawn, SEC_W): self.SEC_W = SEC_W self.highlight_event__func_file_line_RE = re.compile(self.app_options.event_regex) + if self.SWEEP_CSEC: + self.time_origin_relative = self.SWEEP_CSEC[0] + elif self.app_options.absolute_uptime_event_times: + self.time_origin_relative = 0 + else: + # align to time of first sample + self.time_origin_relative = self.time_origin_drawn + self.trace.proc_tree.sample_period + def proc_tree (self, trace): return trace.kernel_tree if self.kernel_only else trace.proc_tree @@ -796,26 +807,25 @@ def draw_processes_recursively(ctx, proc, proc_tree, y): pass # ignore hits on already-hidden processes if not proc.draw: - y -= C.proc_h ctx.proc_above_was_hidden = True + child_y = y else: draw_process(ctx, proc, proc_tree, x, y, w) if ctx.proc_above_was_hidden: draw_hidden_process_separator(ctx, y) ctx.proc_above_was_hidden = False - - next_y = y + C.proc_h + child_y = y + C.proc_h elder_sibling_y = None for child in proc.child_list: - child_x, child_y = draw_processes_recursively(ctx, child, proc_tree, next_y) + child_x, next_y = draw_processes_recursively(ctx, child, proc_tree, child_y) if proc.draw and child.draw: + # draw upward from child to elder sibling or parent (proc) # XX draws lines on top of the process name label draw_process_connecting_lines(ctx, x, y, child_x, child_y, elder_sibling_y) elder_sibling_y = child_y - next_y += C.proc_h * proc_tree.num_nodes_drawn([child]) # XX why a second recursion? - - return x, y + child_y = next_y + return x, child_y def draw_hidden_process_separator(ctx, y): ctx.cr.save() @@ -877,13 +887,6 @@ def draw_process_events(ctx, proc, proc_tree, x, y): for ev in proc.events] if not ev_list: return - if ctx.SWEEP_CSEC: - time_origin_relative = ctx.SWEEP_CSEC[0] - elif ctx.app_options.absolute_uptime_event_times: - time_origin_relative = 0 - else: - # align to time of first sample - time_origin_relative = ctx.time_origin_drawn + proc_tree.sample_period # draw ticks, maybe add to dump list for (ev, tx) in ev_list: @@ -914,7 +917,7 @@ def draw_process_events(ctx, proc, proc_tree, x, y): for (ev, tx) in ev_list: if tx < last_x_touched + spacing: continue - delta = float(ev.time_usec)/1000/10 - time_origin_relative + delta = float(ev.time_usec)/1000/10 - ctx.time_origin_relative if ctx.SWEEP_CSEC: if abs(delta) < C.CSEC: label_str = '{0:3d}'.format(int(delta*10)) From 178c20c78d6f4b902ce5c3f411edac1ea175d04c Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:19 -0800 Subject: [PATCH 135/182] label highlighted events -- regression test only --- pybootchartgui/draw.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index b9d47e1..8b0004f 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -760,8 +760,8 @@ def draw_process(ctx, proc, proc_tree, x, y, w): # Event ticks step on the rectangle painted by draw_process_state_colors(), # e.g. for non-interruptible wait. # User can work around this by toggling off the event ticks. - if not ctx.app_options.hide_events: - draw_process_events(ctx, proc, proc_tree, x, y) + n_highlighted_events = 0 if ctx.app_options.hide_events else \ + draw_process_events(ctx, proc, proc_tree, x, y) if proc_tree.taskstats and ctx.app_options.show_all: cmdString = '' @@ -782,6 +782,7 @@ def draw_process(ctx, proc, proc_tree, x, y, w): w, ctx.cr.device_to_user(0, 0)[0], ctx.cr.clip_extents()[2]) + return n_highlighted_events def draw_processes_recursively(ctx, proc, proc_tree, y): xmin = ctx.cr.device_to_user(0, 0)[0] # work around numeric overflow at high xscale factors @@ -883,16 +884,18 @@ def usec_to_csec(usec): return float(usec) / 1000 / 10 def draw_process_events(ctx, proc, proc_tree, x, y): + n_highlighted_events = 0 ev_list = [(ev, csec_to_xscaled(ctx, usec_to_csec(ev.time_usec))) for ev in proc.events] if not ev_list: - return + return n_highlighted_events # draw ticks, maybe add to dump list for (ev, tx) in ev_list: if re.search(ctx.highlight_event__func_file_line_RE, ev.func_file_line): ctx.cr.set_source_rgba(*HIGHLIGHT_EVENT_COLOR) W,H = 2,8 + n_highlighted_events += 1 else: ctx.cr.set_source_rgba(*EVENT_COLOR) W,H = 1,5 @@ -909,7 +912,7 @@ def draw_process_events(ctx, proc, proc_tree, x, y): # draw numbers if not ctx.app_options.print_event_times: - return + return n_highlighted_events ctx.cr.set_source_rgba(*EVENT_COLOR) spacing = ctx.cr.text_extents("00")[2] last_x_touched = 0 @@ -934,6 +937,7 @@ def draw_process_events(ctx, proc, proc_tree, x, y): label_str, y + C.proc_h - 4, tx) last_label_str = label_str + return n_highlighted_events def draw_process_state_colors(ctx, proc, proc_tree, x, y, w): last_tx = -1 From 889e78516261680fa8e375bfb3e3ac36a54c374a Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:19 -0800 Subject: [PATCH 136/182] label highlighted events -- efficacy --- pybootchartgui/draw.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 8b0004f..7f78797 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -110,7 +110,7 @@ # Event tick color. DIM_EVENT_COLOR = (0.3, 0.3, 0.3) EVENT_COLOR = (0.1, 0.1, 0.1) -HIGHLIGHT_EVENT_COLOR = (2.0, 0.0, 4.0) +HIGHLIGHT_EVENT_COLOR = (0.6, 0.0, 0.6) # Signature color. SIG_COLOR = (0.0, 0.0, 0.0, 0.3125) @@ -700,8 +700,7 @@ def draw_process_bar_chart(ctx, proc_tree, times, curr_y, w, h): curr_y += 15 for root in proc_tree.process_tree: - draw_processes_recursively(ctx, root, proc_tree, curr_y) - curr_y += C.proc_h * proc_tree.num_nodes_drawn([root]) + curr_y = draw_processes_recursively(ctx, root, proc_tree, curr_y)[1] if ctx.proc_above_was_hidden: draw_hidden_process_separator(ctx, curr_y) ctx.proc_above_was_hidden = False @@ -811,11 +810,11 @@ def draw_processes_recursively(ctx, proc, proc_tree, y): ctx.proc_above_was_hidden = True child_y = y else: - draw_process(ctx, proc, proc_tree, x, y, w) + n_highlighted_events = draw_process(ctx, proc, proc_tree, x, y, w) if ctx.proc_above_was_hidden: draw_hidden_process_separator(ctx, y) ctx.proc_above_was_hidden = False - child_y = y + C.proc_h + child_y = y + C.proc_h*(1 if n_highlighted_events <= 0 else 2) elder_sibling_y = None for child in proc.child_list: @@ -883,6 +882,9 @@ def usec_to_csec(usec): '''would drop precision without the float() cast''' return float(usec) / 1000 / 10 +def draw_event_label(ctx, label, tx, y): + draw_label_in_box_at_time(ctx.cr, HIGHLIGHT_EVENT_COLOR, label, y, tx) + def draw_process_events(ctx, proc, proc_tree, x, y): n_highlighted_events = 0 ev_list = [(ev, csec_to_xscaled(ctx, usec_to_csec(ev.time_usec))) @@ -892,21 +894,32 @@ def draw_process_events(ctx, proc, proc_tree, x, y): # draw ticks, maybe add to dump list for (ev, tx) in ev_list: - if re.search(ctx.highlight_event__func_file_line_RE, ev.func_file_line): - ctx.cr.set_source_rgba(*HIGHLIGHT_EVENT_COLOR) + m = re.search(ctx.highlight_event__func_file_line_RE, ev.func_file_line) + if m: + ctx.cr.set_source_rgb(*HIGHLIGHT_EVENT_COLOR) W,H = 2,8 + if m.lastindex: + groups_concat = "" + for g in m.groups(): + groups_concat += str(g) + else: + groups_concat = m.group(0) + draw_event_label(ctx, + groups_concat, + tx, y+2*C.proc_h-4) n_highlighted_events += 1 else: - ctx.cr.set_source_rgba(*EVENT_COLOR) + ctx.cr.set_source_rgb(*EVENT_COLOR) W,H = 1,5 # don't dump synthetic events if ctx.event_dump_list != None and ctx.SWEEP_CSEC and ev.raw_log_seek: ev_time_csec = float(ev.time_usec)/1000/10 if ev_time_csec >= ctx.SWEEP_CSEC[0] and ev_time_csec < ctx.SWEEP_CSEC[1]: ctx.event_dump_list.append(ev) - ctx.cr.move_to(tx-W, y+C.proc_h) # bottom-left - ctx.cr.rel_line_to(W,-H) # top - ctx.cr.rel_line_to(W, H) # bottom-right + ctx.cr.move_to(tx, y+C.proc_h-6) # top + + ctx.cr.rel_line_to(-W,H) # bottom-left + ctx.cr.rel_line_to(2*W,0) # bottom-right ctx.cr.close_path() ctx.cr.fill() From 50efaa0eb96f52edf318be10786a5c0f301b2b30 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:20 -0800 Subject: [PATCH 137/182] events rename --- pybootchartgui/draw.py | 4 ++-- pybootchartgui/parsing.py | 6 +++--- pybootchartgui/samples.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 7f78797..60b6b64 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -236,7 +236,7 @@ def per_render_init(self, cr, time_origin_drawn, SEC_W): self.cr = cr self.time_origin_drawn = time_origin_drawn self.SEC_W = SEC_W - self.highlight_event__func_file_line_RE = re.compile(self.app_options.event_regex) + self.highlight_event__match_RE = re.compile(self.app_options.event_regex) if self.SWEEP_CSEC: self.time_origin_relative = self.SWEEP_CSEC[0] @@ -894,7 +894,7 @@ def draw_process_events(ctx, proc, proc_tree, x, y): # draw ticks, maybe add to dump list for (ev, tx) in ev_list: - m = re.search(ctx.highlight_event__func_file_line_RE, ev.func_file_line) + m = re.search(ctx.highlight_event__match_RE, ev.match) if m: ctx.cr.set_source_rgb(*HIGHLIGHT_EVENT_COLOR) W,H = 2,8 diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 1b59292..9ec8c63 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -125,7 +125,7 @@ def find_parent_id_for(pid): for cpu in self.cpu_stats: # assign to the init process's bar, for lack of any better ev = EventSample(cpu.time, cpu.time*10*1000, init_pid, init_pid, - "comm", "func_file_line", None, None) + "comm", "match", None, None) proc.events.append(ev) # merge in events @@ -720,10 +720,10 @@ def _parse_events_log(writer, tf, file): pid = int(m.group(2)) tid = int(m.group(3)) comm = m.group(4) - func_file_line = m.group(5) + match = m.group(5) raw_log_filename = m.group(6) raw_log_seek = int(m.group(7)) - samples.append( EventSample(time, time_usec, pid, tid, comm, func_file_line, + samples.append( EventSample(time, time_usec, pid, tid, comm, match, tf.extractfile(raw_log_filename), raw_log_seek)) return samples diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 83d4e1e..c16d001 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -15,13 +15,13 @@ class EventSample: - def __init__(self, time, time_usec, pid, tid, comm, func_file_line, raw_log_file, raw_log_seek): + def __init__(self, time, time_usec, pid, tid, comm, match, raw_log_file, raw_log_seek): self.time = time self.time_usec = time_usec self.pid = pid self.tid = tid self.comm = comm - self.func_file_line = func_file_line + self.match = match self.raw_log_file = raw_log_file # a File object self.raw_log_seek = raw_log_seek From 10b3530e5851b0a8490688eb11696aa6072df4d7 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:20 -0800 Subject: [PATCH 138/182] draw sys time bar --- pybootchartgui/draw.py | 58 ++++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 60b6b64..6aa01f1 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -66,6 +66,8 @@ # CPU load chart color. CPU_COLOR = (0.60, 0.65, 0.75, 1.0) +# CPU system-mode load chart color. +CPU_SYS_COLOR = (0.70, 0.65, 0.40, 1.0) # IO wait chart color. IO_COLOR = (0.76, 0.48, 0.48, 0.5) PROCS_RUNNING_COLOR = (0.0, 1.0, 0.0, 1.0) @@ -157,6 +159,7 @@ def draw_text(cr, text, color, x, y): cr.set_source_rgba(*color) cr.move_to(x, y) cr.show_text(text) + return cr.text_extents(text)[2] def draw_fill_rect(cr, color, rect): cr.set_source_rgba(*color) @@ -182,12 +185,11 @@ def draw_diamond(cr, x, y, w, h): def draw_legend_diamond(cr, label, fill_color, x, y, w, h): cr.set_source_rgba(*fill_color) draw_diamond(cr, x, y-h/2, w, h) - draw_text(cr, label, TEXT_COLOR, x + w + 5, y) + return draw_text(cr, label, TEXT_COLOR, x + w + 5, y) def draw_legend_box(cr, label, fill_color, x, y, s): draw_fill_rect(cr, fill_color, (x, y - s, s, s)) - #draw_rect(cr, PROC_BORDER_COLOR, (x, y - s, s, s)) - draw_text(cr, label, TEXT_COLOR, x + s + 5, y) + return s + 5 + draw_text(cr, label, TEXT_COLOR, x + s + 5, y) def draw_legend_line(cr, label, fill_color, x, y, s): draw_fill_rect(cr, fill_color, (x, y - s/2, s + 1, 3)) @@ -435,12 +437,14 @@ def render_charts(ctx, trace, curr_y, w, h): # render bar legend ctx.cr.set_font_size(LEGEND_FONT_SIZE) curr_y += 20 - draw_legend_box(ctx.cr, "CPU (user+sys)", CPU_COLOR, 0, curr_y, C.leg_s) - draw_legend_box(ctx.cr, "I/O (wait)", IO_COLOR, 120, curr_y, C.leg_s) - draw_legend_diamond(ctx.cr, "Runnable threads", PROCS_RUNNING_COLOR, - 120 +90, curr_y, C.leg_s, C.leg_s) - draw_legend_diamond(ctx.cr, "Blocked threads -- Uninterruptible Syscall", PROCS_BLOCKED_COLOR, - 120 +90 +140, curr_y, C.leg_s, C.leg_s) + curr_x = 0 + curr_x += 20 + draw_legend_box(ctx.cr, "CPU (user)", CPU_COLOR, curr_x, curr_y, C.leg_s) + curr_x += 20 + draw_legend_box(ctx.cr, "CPU (sys)", CPU_SYS_COLOR, curr_x, curr_y, C.leg_s) + curr_x += 20 + draw_legend_box(ctx.cr, "I/O (wait)", IO_COLOR, curr_x, curr_y, C.leg_s) + curr_x += draw_legend_diamond(ctx.cr, "Runnable threads", PROCS_RUNNING_COLOR, + curr_x +10, curr_y, C.leg_s, C.leg_s) + curr_x += draw_legend_diamond(ctx.cr, "Blocked threads -- Uninterruptible Syscall", PROCS_BLOCKED_COLOR, + curr_x +70, curr_y, C.leg_s, C.leg_s) chart_rect = (0, curr_y+10, w, C.bar_h) draw_box (ctx, chart_rect) @@ -453,6 +457,10 @@ def render_charts(ctx, trace, curr_y, w, h): draw_chart (ctx, CPU_COLOR, True, chart_rect, \ [(sample.time, sample.user + sample.sys) for sample in trace.cpu_stats], \ proc_tree, None, plot_square) + # render CPU load -- a backwards delta + draw_chart (ctx, CPU_SYS_COLOR, True, chart_rect, \ + [(sample.time, sample.sys) for sample in trace.cpu_stats], \ + proc_tree, None, plot_square) # instantaneous sample draw_chart (ctx, PROCS_BLOCKED_COLOR, False, chart_rect, @@ -680,12 +688,15 @@ def draw_process_bar_chart(ctx, proc_tree, times, curr_y, w, h): PROCS_RUNNING_COLOR, 10, curr_y, C.leg_s*3/4, C.proc_h) draw_legend_diamond (ctx.cr, "Uninterruptible Syscall", PROC_COLOR_D, 10+100, curr_y, C.leg_s*3/4, C.proc_h) - draw_legend_box (ctx.cr, "Running (%cpu)", - PROC_COLOR_R, 10+100+180, curr_y, C.leg_s) - draw_legend_box (ctx.cr, "Sleeping", - PROC_COLOR_S, 10+100+180+130, curr_y, C.leg_s) - draw_legend_box (ctx.cr, "Zombie", - PROC_COLOR_Z, 10+100+180+130+90, curr_y, C.leg_s) + curr_x = 10+100+40 + curr_x += 20 + draw_legend_box (ctx.cr, "Running (user)", + PROC_COLOR_R, 10+100+curr_x, curr_y, C.leg_s) + curr_x += 20 + draw_legend_box (ctx.cr, "Running (sys)", + CPU_SYS_COLOR, 10+100+curr_x, curr_y, C.leg_s) + curr_x += 20 + draw_legend_box (ctx.cr, "Sleeping", + PROC_COLOR_S, 10+100+curr_x, curr_y, C.leg_s) + curr_x += 20 + draw_legend_box (ctx.cr, "Zombie", + PROC_COLOR_Z, 10+100+curr_x, curr_y, C.leg_s) curr_y -= 9 chart_rect = [-1, -1, -1, -1] @@ -865,7 +876,22 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w): height = normalized * C.proc_h draw_fill_rect(ctx.cr, PROC_COLOR_R, (last_time, y+C.proc_h, width, -height)) - # If thread ran at all, draw a pair of tick marks, in case rect was too short to resolve. + # in unlikely event of no sys time at all, skip setting of color to CPU_SYS_COLOR + if sample.cpu_sample.sys > 0: + height = sample.cpu_sample.sys * C.proc_h + draw_fill_rect(ctx.cr, CPU_SYS_COLOR, (last_time, y+C.proc_h, width, -height)) + if sample.cpu_sample.sys < normalized: + # draw a separator between the bar segments, to aid the eye in + # resolving the boundary + ctx.cr.save() + ctx.cr.move_to(last_time, y+C.proc_h-height) + ctx.cr.rel_line_to(width,0) + ctx.cr.set_source_rgba(*PROC_COLOR_S) + ctx.cr.set_line_width(DEP_STROKE/2) + ctx.cr.stroke() + ctx.cr.restore() + + # If thread ran at all, draw a tick mark, in case rect was too short to resolve. tick_width = width/3 tick_height = C.proc_h/3 From 74694a02657d6b4aaec1d798107f9e969ed08a4f Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:20 -0800 Subject: [PATCH 139/182] FIX total system cpu time not scaled to total CPU -- was independent therefore exaggerated --- pybootchartgui/draw.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 6aa01f1..0d343ed 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -404,6 +404,7 @@ def transform_point_coords(point, y_base, yscale): else: ctx.cr.stroke() ctx.cr.set_line_width(1.0) + return max_y def in_chart_X_margin(proc_tree): return proc_tree.sample_period @@ -452,15 +453,21 @@ def render_charts(ctx, trace, curr_y, w, h): # render I/O wait -- a backwards delta draw_chart (ctx, IO_COLOR, True, chart_rect, \ [(sample.time, sample.user + sample.sys + sample.io) for sample in trace.cpu_stats], \ - proc_tree, None, plot_square) + proc_tree, + [0, 1], + plot_square) # render CPU load -- a backwards delta draw_chart (ctx, CPU_COLOR, True, chart_rect, \ [(sample.time, sample.user + sample.sys) for sample in trace.cpu_stats], \ - proc_tree, None, plot_square) - # render CPU load -- a backwards delta + proc_tree, + [0, 1], + plot_square) + # superimpose "sys time", the fraction of CPU load spent in kernel -- a backwards delta draw_chart (ctx, CPU_SYS_COLOR, True, chart_rect, \ [(sample.time, sample.sys) for sample in trace.cpu_stats], \ - proc_tree, None, plot_square) + proc_tree, + [0, 1], + plot_square) # instantaneous sample draw_chart (ctx, PROCS_BLOCKED_COLOR, False, chart_rect, From 76d9e1277b1e482d5efa0cfcea25f4efbe42f79a Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:20 -0800 Subject: [PATCH 140/182] minimum activity mark now a cairo arc --- pybootchartgui/draw.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 0d343ed..f4a4909 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -863,7 +863,8 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w): draw_fill_rect(ctx.cr, PROC_COLOR_S, (x, y, w, C.proc_h)) ctx_save__csec_to_xscaled(ctx) - # draw visual reminder of unknowability of thread end time + ctx.cr.set_line_width(0.0) + # draw visual reminder of unknowability of thread end time ctx.cr.move_to(proc.samples[-1].time + proc_tree.sample_period, y+C.proc_h/2) ctx.cr.line_to(proc.samples[-1].time, y+C.proc_h) ctx.cr.line_to(proc.samples[-1].time, y) @@ -898,13 +899,9 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w): ctx.cr.stroke() ctx.cr.restore() - # If thread ran at all, draw a tick mark, in case rect was too short to resolve. - tick_width = width/3 - tick_height = C.proc_h/3 - - ctx.cr.move_to(last_time + width/2, y+C.proc_h-tick_height) - ctx.cr.rel_line_to(-tick_width/2, tick_height) - ctx.cr.rel_line_to(tick_width, 0) + # If thread ran at all, draw a "speed bump", in case rect was too short to resolve. + tick_height = C.proc_h/5 + ctx.cr.arc((last_time + sample.time)/2, y+C.proc_h, tick_height, math.pi, 0.0) ctx.cr.close_path() ctx.cr.fill() From 9a6f2f06dde1813ca93d1a0b4c8e558464865f2c Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:20 -0800 Subject: [PATCH 141/182] hide process range -- unhide all --- pybootchartgui/draw.py | 79 ++++++++++++++++++++++++------------------ pybootchartgui/gui.py | 20 +++++++---- 2 files changed, 59 insertions(+), 40 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index f4a4909..23d40b7 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -232,6 +232,8 @@ def __init__(self, app_options, trace, cumulative = True, charts = True, kernel_ self.time_origin_relative = None # currently used only to locate events # intra-rendering state + self.hide_process_y = None + self.unhide_process_y = None self.proc_above_was_hidden = False def per_render_init(self, cr, time_origin_drawn, SEC_W): @@ -615,12 +617,26 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): if ctx.charts: curr_y = render_charts (ctx, trace, curr_y, w, h) + if ctx.app_options.show_legends and not ctx.kernel_only: + curr_y = draw_process_bar_chart_legends(ctx, curr_y) + # draw process boxes proc_height = h if proc_tree.taskstats and ctx.cumulative: proc_height -= C.CUML_HEIGHT - ctx.hide_process_y = hide_process_y + # curr_y points to the *top* of the first per-process line + if hide_process_y and hide_process_y[0] > (curr_y - C.proc_h/4): + hide_mod_proc_h = (hide_process_y[0] - curr_y) % C.proc_h + # if first button-down (hide_process_y[0]) falls in middle half of any process bar, then set up for hiding + if hide_mod_proc_h >= C.proc_h/4 and hide_mod_proc_h < C.proc_h*3/4: + hide_process_y.sort() + ctx.hide_process_y = hide_process_y + ctx.unhide_process_y = None + else: # unhide + ctx.hide_process_y = None + ctx.unhide_process_y = hide_process_y[0] + draw_process_bar_chart(ctx, proc_tree, trace.times, curr_y, w, proc_height) @@ -688,24 +704,25 @@ def draw_vertical(cr, x): draw_shading(ctx.cr, (int(x),0,int(ctx.cr.clip_extents()[2]-x),height)) draw_vertical(ctx.cr, x) -def draw_process_bar_chart(ctx, proc_tree, times, curr_y, w, h): - if ctx.app_options.show_legends and not ctx.kernel_only: - curr_y += 30 - draw_legend_diamond (ctx.cr, "Runnable", - PROCS_RUNNING_COLOR, 10, curr_y, C.leg_s*3/4, C.proc_h) - draw_legend_diamond (ctx.cr, "Uninterruptible Syscall", - PROC_COLOR_D, 10+100, curr_y, C.leg_s*3/4, C.proc_h) - curr_x = 10+100+40 - curr_x += 20 + draw_legend_box (ctx.cr, "Running (user)", - PROC_COLOR_R, 10+100+curr_x, curr_y, C.leg_s) - curr_x += 20 + draw_legend_box (ctx.cr, "Running (sys)", - CPU_SYS_COLOR, 10+100+curr_x, curr_y, C.leg_s) - curr_x += 20 + draw_legend_box (ctx.cr, "Sleeping", - PROC_COLOR_S, 10+100+curr_x, curr_y, C.leg_s) - curr_x += 20 + draw_legend_box (ctx.cr, "Zombie", - PROC_COLOR_Z, 10+100+curr_x, curr_y, C.leg_s) - curr_y -= 9 +def draw_process_bar_chart_legends(ctx, curr_y): + curr_y += 30 + draw_legend_diamond (ctx.cr, "Runnable", + PROCS_RUNNING_COLOR, 10, curr_y, C.leg_s*3/4, C.proc_h) + draw_legend_diamond (ctx.cr, "Uninterruptible Syscall", + PROC_COLOR_D, 10+100, curr_y, C.leg_s*3/4, C.proc_h) + curr_x = 10+100+40 + curr_x += 20 + draw_legend_box (ctx.cr, "Running (user)", + PROC_COLOR_R, 10+100+curr_x, curr_y, C.leg_s) + curr_x += 20 + draw_legend_box (ctx.cr, "Running (sys)", + CPU_SYS_COLOR, 10+100+curr_x, curr_y, C.leg_s) + curr_x += 20 + draw_legend_box (ctx.cr, "Sleeping", + PROC_COLOR_S, 10+100+curr_x, curr_y, C.leg_s) + curr_x += 20 + draw_legend_box (ctx.cr, "Zombie", + PROC_COLOR_Z, 10+100+curr_x, curr_y, C.leg_s) + curr_y -= 9 + return curr_y +def draw_process_bar_chart(ctx, proc_tree, times, curr_y, w, h): chart_rect = [-1, -1, -1, -1] ctx.cr.set_font_size (PROC_TEXT_FONT_SIZE) @@ -807,22 +824,18 @@ def draw_processes_recursively(ctx, proc, proc_tree, y): x = max(xmin, csec_to_xscaled(ctx, proc.start_time)) w = max(xmin, csec_to_xscaled(ctx, proc.start_time + proc.duration)) - x - if ctx.hide_process_y: - if ctx.hide_process_y < y - C.proc_h/4: - ctx.hide_process_y = None # no further hits in traversal are possible + if ctx.hide_process_y and y+C.proc_h > ctx.hide_process_y[0] and proc.draw: + proc.draw = False + ctx.hide_process_y[1] -= C.proc_h + if y > (ctx.hide_process_y[1]) / C.proc_h * C.proc_h: + ctx.hide_process_y = None + + elif ctx.unhide_process_y and y+C.proc_h*3/4 > ctx.unhide_process_y: + if proc.draw: # found end of run of hidden processes + ctx.unhide_process_y = None else: - if ctx.hide_process_y < y + C.proc_h/4: - if not proc.draw: - proc.draw = True - ctx.hide_process_y += C.proc_h # unhide all in consecutive hidden processes - else: - pass # ignore hits on the border region if the process is not hidden - elif ctx.hide_process_y < y + C.proc_h*3/4: - if proc.draw: - proc.draw = False - ctx.hide_process_y = None - else: - pass # ignore hits on already-hidden processes + proc.draw = True + ctx.unhide_process_y += C.proc_h if not proc.draw: ctx.proc_above_was_hidden = True diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index 6aa1d1e..2e476a4 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -60,7 +60,7 @@ def __init__(self, trace, drawctx, xscale): self.sweep_csec = None - self.hide_process_y = None # XX valid only between self.on_area_button_press() and self.draw() + self.hide_process_y = [] # XX valid only between self.on_area_button_press() and self.draw() def do_expose_event(self, event): # XX called on mouse entering or leaving window -- can these be disabled? cr = self.window.cairo_create() @@ -76,7 +76,7 @@ def draw(self, cr): cr.paint() # fill whole DrawingArea with white self.cr_set_up_transform(cr) draw.render(cr, self.drawctx, self.xscale, self.trace, self.sweep_csec, self.hide_process_y) - self.hide_process_y = None + self.hide_process_y = [] def position_changed(self): self.emit("position-changed", self.x, self.y) @@ -199,14 +199,17 @@ def on_key_press_event(self, widget, event): return True def on_area_button_press(self, area, event): + # cancel any pending action based on an earlier button pressed and now held down + self.hide_process_y = [] + self.sweep_csec = None + if event.button == 1: area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR)) self.prevmousex = event.x self.prevmousey = event.y - if event.button == 2: - self.hide_process_y = self.device_to_csec_user_y(event.x, event.y)[1] - self.queue_draw() - if event.button == 3: + elif event.button == 2 and len(self.hide_process_y) == 0: + self.hide_process_y.append( self.device_to_csec_user_y(event.x, event.y)[1]) + elif event.button == 3: if not self.sweep_csec: self.sweep_csec = [self.device_to_csec_user_y(event.x, 0)[0], None] else: @@ -222,7 +225,10 @@ def on_area_button_release(self, area, event): self.prevmousex = None self.prevmousey = None return True - if event.button == 3: + elif event.button == 2 and len(self.hide_process_y) == 1: + self.hide_process_y.append( self.device_to_csec_user_y(event.x, event.y)[1]) + self.queue_draw() + elif event.button == 3: if self.sweep_csec: self.sweep_csec[1] = self.device_to_csec_user_y(event.x, 0)[0] # if no motion between click and release, draw a one-sided sweep window, and don't dump events From 942cc2aa52e461758e690bf83228c97b2513758a Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:21 -0800 Subject: [PATCH 142/182] FIX sweep --- pybootchartgui/draw.py | 24 +++++++++++------------- pybootchartgui/gui.py | 28 +++++++++++++++------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 23d40b7..6b04f8a 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -19,8 +19,8 @@ import re import random import colorsys -import traceback import collections +import traceback # debug # Constants: Put the more heavily used, non-derived constants in a named tuple, for immutability. # XX The syntax is awkward, but more elegant alternatives have run-time overhead. @@ -236,13 +236,15 @@ def __init__(self, app_options, trace, cumulative = True, charts = True, kernel_ self.unhide_process_y = None self.proc_above_was_hidden = False - def per_render_init(self, cr, time_origin_drawn, SEC_W): + def per_render_init(self, cr, time_origin_drawn, SEC_W, sweep_csec): self.cr = cr self.time_origin_drawn = time_origin_drawn self.SEC_W = SEC_W self.highlight_event__match_RE = re.compile(self.app_options.event_regex) + self.SWEEP_CSEC = sweep_csec if self.SWEEP_CSEC: + self.SWEEP_CSEC.sort() self.time_origin_relative = self.SWEEP_CSEC[0] elif self.app_options.absolute_uptime_event_times: self.time_origin_relative = 0 @@ -588,11 +590,9 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): "ctx" is a DrawContext object. ''' #traceback.print_stack() + ctx.per_render_init(cr, _time_origin_drawn(ctx, trace), _sec_w(xscale), sweep_csec) (w, h) = extents(ctx, xscale, trace) - ctx.per_render_init(cr, _time_origin_drawn(ctx, trace), _sec_w(xscale)) - ctx.SWEEP_CSEC = sweep_csec - ctx.cr.set_line_width(1.0) ctx.cr.select_font_face(FONT_NAME) draw_fill_rect(ctx.cr, WHITE, (0, 0, max(w, C.MIN_IMG_W), h)) @@ -652,9 +652,8 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): cuml_rect = (0, curr_y + C.off_y * 100, w, C.CUML_HEIGHT/2 - C.off_y * 2) draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_IO) - if sweep_csec: - draw_sweep(ctx, sweep_csec[0], sweep_csec[1] - sweep_csec[0]) - #dump_pseudo_event(ctx, "start of event window, width " + int(width*1000) + "msec") + if ctx.SWEEP_CSEC: + draw_sweep(ctx) ctx.cr.restore() @@ -680,7 +679,7 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): ctx.event_dump_list = None -def draw_sweep(ctx, sweep_csec, width_csec): +def draw_sweep(ctx): def draw_shading(cr, rect): # alpha value of the rgba strikes a compromise between appearance on screen, and in printed screenshot cr.set_source_rgba(0.0, 0.0, 0.0, 0.08) @@ -696,11 +695,10 @@ def draw_vertical(cr, x): cr.stroke() height = int(ctx.cr.device_to_user(0,2000)[1]) - x = csec_to_xscaled(ctx, sweep_csec) + x = csec_to_xscaled(ctx, ctx.SWEEP_CSEC[0]) draw_shading(ctx.cr, (int(x),0,int(ctx.cr.clip_extents()[0]-x),height)) draw_vertical(ctx.cr, x) - - x = csec_to_xscaled(ctx, sweep_csec + width_csec) + x = csec_to_xscaled(ctx, ctx.SWEEP_CSEC[1]) draw_shading(ctx.cr, (int(x),0,int(ctx.cr.clip_extents()[2]-x),height)) draw_vertical(ctx.cr, x) @@ -976,7 +974,7 @@ def draw_process_events(ctx, proc, proc_tree, x, y): for (ev, tx) in ev_list: if tx < last_x_touched + spacing: continue - delta = float(ev.time_usec)/1000/10 - ctx.time_origin_relative + delta= float(ev.time_usec)/1000/10 - ctx.time_origin_relative if ctx.SWEEP_CSEC: if abs(delta) < C.CSEC: label_str = '{0:3d}'.format(int(delta*10)) diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index 2e476a4..b311e25 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -75,7 +75,9 @@ def draw(self, cr): cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) cr.paint() # fill whole DrawingArea with white self.cr_set_up_transform(cr) - draw.render(cr, self.drawctx, self.xscale, self.trace, self.sweep_csec, self.hide_process_y) + draw.render(cr, self.drawctx, self.xscale, self.trace, + list(self.sweep_csec) if self.sweep_csec else None, # pass by value not ref + self.hide_process_y) self.hide_process_y = [] def position_changed(self): @@ -183,6 +185,10 @@ def dump_raw_event_context(self, button): POS_INCREMENT = 100 + # file:///usr/share/doc/libgtk2.0-doc/gtk/GtkWidget.html says: + # Returns : + # TRUE to stop other handlers from being invoked for the event. + # FALSE to propagate the event further def on_key_press_event(self, widget, event): if event.keyval == gtk.keysyms.Left: self.x -= self.POS_INCREMENT/self.zoom_ratio @@ -201,7 +207,6 @@ def on_key_press_event(self, widget, event): def on_area_button_press(self, area, event): # cancel any pending action based on an earlier button pressed and now held down self.hide_process_y = [] - self.sweep_csec = None if event.button == 1: area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR)) @@ -211,13 +216,14 @@ def on_area_button_press(self, area, event): self.hide_process_y.append( self.device_to_csec_user_y(event.x, event.y)[1]) elif event.button == 3: if not self.sweep_csec: - self.sweep_csec = [self.device_to_csec_user_y(event.x, 0)[0], None] + self.sweep_csec = [self.device_to_csec_user_y(event.x, 0)[0], + self.device_to_csec_user_y(self.trace.ps_stats.end_time, 0)[0]] else: self.sweep_csec = None - self.queue_draw() + self.queue_draw() if event.type not in (gtk.gdk.BUTTON_PRESS, gtk.gdk.BUTTON_RELEASE): return False - return False + return True def on_area_button_release(self, area, event): if event.button == 1: @@ -229,15 +235,11 @@ def on_area_button_release(self, area, event): self.hide_process_y.append( self.device_to_csec_user_y(event.x, event.y)[1]) self.queue_draw() elif event.button == 3: - if self.sweep_csec: - self.sweep_csec[1] = self.device_to_csec_user_y(event.x, 0)[0] - # if no motion between click and release, draw a one-sided sweep window, and don't dump events - if self.sweep_csec[1] == self.sweep_csec[0]: - self.sweep_csec[1] = self.device_to_csec_user_y(self.trace.ps_stats.end_time, 0)[0] - else: - self.drawctx.event_dump_list = [] + if self.sweep_csec and \ + self.sweep_csec[1] != self.device_to_csec_user_y(self.trace.ps_stats.end_time, 0)[0]: + self.drawctx.event_dump_list = [] # XX self.queue_draw() - return False + return True def on_area_scroll_event(self, area, event): if event.state & gtk.gdk.CONTROL_MASK: From e3652643b876aa83e507e3e576c3d64137f0b7f9 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:21 -0800 Subject: [PATCH 143/182] gui zoom by radical 2 increments -- for pixel alignment --- pybootchartgui/gui.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index b311e25..2734a48 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -17,6 +17,7 @@ import gtk import gtk.gdk import gtk.keysyms +import math from . import draw from .draw import DrawContext @@ -112,7 +113,8 @@ def set_center (self, ctr_csec_x, ctr_user_y): self.y = (ctr_user_y - float(self.get_allocation()[3])/self.zoom_ratio/2) self.position_changed() - ZOOM_INCREMENT = 1.25 + ZOOM_INCREMENT = 2 + ZOOM_HALF_INCREMENT = math.sqrt(2) # Zoom maintaining the content at window's current center untranslated. # "Center" is irrespective of any occlusion. def zoom_image (self, zoom_ratio): @@ -251,10 +253,10 @@ def on_area_scroll_event(self, area, event): return True elif event.state & gtk.gdk.MOD1_MASK: if event.direction == gtk.gdk.SCROLL_UP: - self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT) + self.zoom_image(self.zoom_ratio * self.ZOOM_HALF_INCREMENT) return True if event.direction == gtk.gdk.SCROLL_DOWN: - self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT) + self.zoom_image(self.zoom_ratio / self.ZOOM_HALF_INCREMENT) return True def on_area_motion_notify(self, area, event): From a42e96b40ec2810c8cf9b291ff524595ac1075a6 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:21 -0800 Subject: [PATCH 144/182] dont hide processes that do anything but sleep --- pybootchartgui/main.py.in | 4 ++-- pybootchartgui/process_tree.py | 9 +++++---- pybootchartgui/samples.py | 5 ++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 31c61f7..6014416 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -55,8 +55,8 @@ def _mk_options_parser(): parser.add_option("--merge", dest="merge", default=False, help="Prune the process tree of all activity from dull children: " + "stats are added to parent, child process is lost in drawing of chart.") - parser.add_option("-C", "--show-high-CPU", dest="show_high_CPU", type="int", default=1, metavar="CSEC", - help="Suppress hiding of any thread consuming more than CSEC of CPU time." + + parser.add_option("-C", "--show-high-CPU", dest="show_high_CPU", type="int", default=-1, metavar="CSEC", + help="Suppress hiding of any thread consuming more than CSEC of CPU time [default %default]." + " Hiding means it's not shown on initial rendering, but can be shown by a mouse click.") parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="suppress informational messages") diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index c36b2d1..1ed5c42 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -175,15 +175,16 @@ def update_ppids_for_daemons(self, process_list): self.build() def is_inactive_process(self, p): - return p.CPUCount() < self.options.show_high_CPU + return p.CPUCount() < self.options.show_high_CPU and \ + (p.activeCount < 1 and len(p.events) == 0) def is_inactive_process_without_children(self, p): return self.is_inactive_process(p) and \ self.num_nodes(p.child_list) == 0 - def is_inactive_process_with_inactive_children(self, p): - return self.is_inactive_process(p) and \ - p.c_CPUCount() < self.options.show_high_CPU +# def is_inactive_process_with_inactive_children(self, p): +# return self.is_inactive_process(p) and \ +# p.c_CPUCount() < self.options.show_high_CPU def prune(self, process_subtree, parent, pruning_test): n_pruned = 0 diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index c16d001..bdac675 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -154,9 +154,8 @@ def calc_stats(self, samplePeriod): # self.duration is the _minimum_ known duration of the thread self.duration = lastSample.time - self.start_time - self.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) \ - or sample.state == 'D']) + # XX add in page faults, including "minor" + self.activeCount = sum( [1 for sample in self.samples if sample.state != 'S']) def calc_load(self, userCpu, sysCpu, interval): userCpuLoad = float(userCpu - self.user_cpu_time[-1]) / interval From 1869fd4d48f174d490abb151d067123f7eb966d0 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:21 -0800 Subject: [PATCH 145/182] command line -- change defaults --- pybootchartgui/main.py.in | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 6014416..2a180e4 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -50,8 +50,8 @@ def _mk_options_parser(): description="PATH must point to a bootchart*.tgz", epilog=None, option_class=ExtendOption) - parser.add_option("-i", "--interactive", action="store_true", dest="interactive", default=False, - help="start in active mode") + parser.add_option("--batch", action="store_false", dest="interactive", default=True, + help="print a bootchart image, rather than starting the interactive GUI") parser.add_option("--merge", dest="merge", default=False, help="Prune the process tree of all activity from dull children: " + "stats are added to parent, child process is lost in drawing of chart.") @@ -86,16 +86,18 @@ def _mk_options_parser(): " (file:///usr/share/doc/python2.6/html/library/re.htm)") parser.add_option("--hide-events", action="store_true", dest="hide_events", default=False, help="hide event ticks (small black triangles)") - parser.add_option("--print-event-times", action="store_true", dest="print_event_times", default=False, - help="print time of each event, inside the box of the reporting process") - parser.add_option("--absolute-uptime-event-times", action="store_true", dest="absolute_uptime_event_times", default=False, - help="print time of each event, inside the box of the reporting process") + parser.add_option("--no-print-event-times", action="store_false", default=True, dest="print_event_times", + help="suppress printing time of each event, inside the box of the reporting process") + parser.add_option("--absolute-uptime-event-times", action="store_true", default=False, + dest="absolute_uptime_event_times", + help="event times shown are relative to system boot, rather than beginning of sampling") parser.add_option("--dump-raw-event-context", action="store_true", dest="dump_raw_event_context", default=False, help="in addition to log lines for selected events, dump log lines as well," + " retaining original log sort order, which may not be temporally correct.") - parser.add_option("--synthesize-sample-start-events", action="store_true", dest="synthesize_sample_start_events", + parser.add_option("--no-synthesize-sample-start-events", action="store_false", default=True, + dest="synthesize_sample_start_events", help="synthesize an event marking each boundary between sample periods -- " + "helpful in analyzing collector timing issues") From 5184be62d45331909e4ae54385aed0167a972b0a Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:22 -0800 Subject: [PATCH 146/182] main -- events command line option takes a list --- pybootchartgui/draw.py | 11 +++++++++-- pybootchartgui/main.py.in | 28 +++++++--------------------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 6b04f8a..d0a82b8 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -230,6 +230,7 @@ def __init__(self, app_options, trace, cumulative = True, charts = True, kernel_ self.time_origin_drawn = None # time of leftmost plotted data, as integer csecs self.SEC_W = None self.time_origin_relative = None # currently used only to locate events + self.highlight_event__match_RE = [] # intra-rendering state self.hide_process_y = None @@ -240,7 +241,10 @@ def per_render_init(self, cr, time_origin_drawn, SEC_W, sweep_csec): self.cr = cr self.time_origin_drawn = time_origin_drawn self.SEC_W = SEC_W - self.highlight_event__match_RE = re.compile(self.app_options.event_regex) + + self.highlight_event__match_RE = [] + for ev_regex in self.app_options.event_regex: + self.highlight_event__match_RE.append( re.compile(ev_regex)) self.SWEEP_CSEC = sweep_csec if self.SWEEP_CSEC: @@ -935,7 +939,10 @@ def draw_process_events(ctx, proc, proc_tree, x, y): # draw ticks, maybe add to dump list for (ev, tx) in ev_list: - m = re.search(ctx.highlight_event__match_RE, ev.match) + for ev_re in ctx.highlight_event__match_RE: + m = re.search(ev_re, ev.match) + if m: + break; if m: ctx.cr.set_source_rgb(*HIGHLIGHT_EVENT_COLOR) W,H = 2,8 diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 2a180e4..d03f75a 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -28,28 +28,13 @@ from optparse import Option, OptionValueError from . import parsing from . import batch -class ExtendOption(Option): - ACTIONS = Option.ACTIONS + ("extend",) - STORE_ACTIONS = Option.STORE_ACTIONS + ("extend",) - TYPED_ACTIONS = Option.TYPED_ACTIONS + ("extend",) - ALWAYS_TYPED_ACTIONS = Option.ALWAYS_TYPED_ACTIONS + ("extend",) - - def take_action(self, action, dest, opt, value, values, parser): - if action == "extend": - lvalue = value.split(",") - values.ensure_value(dest, []).extend(lvalue) - else: - Option.take_action( - self, action, dest, opt, value, values, parser) - def _mk_options_parser(): """Make an options parser.""" usage = "%prog [options] PATH" version = "%prog v@VER@" parser = optparse.OptionParser(usage=usage, version=version, description="PATH must point to a bootchart*.tgz", - epilog=None, - option_class=ExtendOption) + epilog=None) parser.add_option("--batch", action="store_false", dest="interactive", default=True, help="print a bootchart image, rather than starting the interactive GUI") parser.add_option("--merge", dest="merge", default=False, @@ -66,10 +51,11 @@ def _mk_options_parser(): help="show legend lines with keys to chart symbols") # disk stats - parser.add_option("-p", "--show-partitions", action="extend", dest="partitions", type="string", default=[], - help="draw a disk stat chart for any block device partitions in this comma-separated list") - parser.add_option("-P", "--relabel-partitions", action="extend", dest="partition_labels", default=[], - help="list of per-partition strings, to be drawn instead of the raw per-partition device names") + parser.add_option("--partition", action="append", dest="partitions", type="string", default=[], + help="draw a disk stat chart for any block device partition whose basename matches PARTITION") + parser.add_option("--relabel-partition", action="append", dest="partition_labels", default=[], + help="list of per-partition strings, each replacing the raw per-partition device name" + + "in the corresponding position") parser.add_option("--show-ops-not-bytes", action="store_true", dest="show_ops_not_bytes", default=False, help="chart number of I/O operations handed to driver, rather than bytes transferred per sample") @@ -79,7 +65,7 @@ def _mk_options_parser(): help="relocate the text within process bars (left, center)") # event plotting - parser.add_option("-e", "--events", dest="event_regex", metavar="REGEX", default="^$", + parser.add_option("-e", "--events", action="append", dest="event_regex", metavar="REGEX", default=["^$"], help="Highlight events matching REGEX." + " Syntax is similar to grep 'extended' REs." + " To match FOO or BAR anywhere on the log line, use 'FOO|BAR'." + From c4dc2523b050ae5f263f49a8e905135869d9f1c8 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:22 -0800 Subject: [PATCH 147/182] sweep line time labels --- pybootchartgui/draw.py | 75 +++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index d0a82b8..4f233e1 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -39,6 +39,7 @@ BACK_COLOR = (1.0, 1.0, 1.0, 1.0) WHITE = (1.0, 1.0, 1.0, 1.0) +BLACK = (0.0, 0.0, 0.0, 1.0) NOTEPAD_YELLLOW = (0.95, 0.95, 0.8, 1.0) PURPLE = (0.6, 0.1, 0.6, 1.0) @@ -241,6 +242,8 @@ def per_render_init(self, cr, time_origin_drawn, SEC_W, sweep_csec): self.cr = cr self.time_origin_drawn = time_origin_drawn self.SEC_W = SEC_W + self.n_WIDTH = cr.text_extents("n")[2] + self.M_HEIGHT = cr.text_extents("M")[3] self.highlight_event__match_RE = [] for ev_regex in self.app_options.event_regex: @@ -583,7 +586,7 @@ def late_init_transform(cr): cr.translate(C.off_x, 0) # current window-coord clip shrinks with loss of the C.off_x-wide strip on left # -# Render the chart. Central method of this module. +# Render the chart. Main entry point of this module. # def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): ''' @@ -629,6 +632,9 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): if proc_tree.taskstats and ctx.cumulative: proc_height -= C.CUML_HEIGHT + curr_y += ctx.M_HEIGHT + sweep_text_box_y = [] # [curr_y+ctx.M_HEIGHT] + # curr_y points to the *top* of the first per-process line if hide_process_y and hide_process_y[0] > (curr_y - C.proc_h/4): hide_mod_proc_h = (hide_process_y[0] - curr_y) % C.proc_h @@ -641,23 +647,21 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): ctx.hide_process_y = None ctx.unhide_process_y = hide_process_y[0] - draw_process_bar_chart(ctx, proc_tree, trace.times, - curr_y, w, proc_height) - - curr_y = proc_height + curr_y = draw_process_bar_chart(ctx, proc_tree, trace.times, + curr_y, w, proc_height) - # draw a cumulative CPU-time-per-process graph if proc_tree.taskstats and ctx.cumulative: - cuml_rect = (0, curr_y + C.off_y, w, C.CUML_HEIGHT/2 - C.off_y * 2) + # draw a cumulative CPU-time-per-process graph + cuml_rect = (0, proc_height + C.off_y, w, C.CUML_HEIGHT/2 - C.off_y * 2) draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_CPU) - # draw a cumulative I/O-time-per-process graph - if proc_tree.taskstats and ctx.cumulative: - cuml_rect = (0, curr_y + C.off_y * 100, w, C.CUML_HEIGHT/2 - C.off_y * 2) + # draw a cumulative I/O-time-per-process graph + cuml_rect = (0, proc_height + C.off_y * 100, w, C.CUML_HEIGHT/2 - C.off_y * 2) draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_IO) if ctx.SWEEP_CSEC: - draw_sweep(ctx) + sweep_text_box_y.append(curr_y+ctx.M_HEIGHT) + draw_sweep(ctx, sweep_text_box_y) ctx.cr.restore() @@ -683,28 +687,32 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): ctx.event_dump_list = None -def draw_sweep(ctx): +def draw_sweep(ctx, sweep_text_box_y): def draw_shading(cr, rect): # alpha value of the rgba strikes a compromise between appearance on screen, and in printed screenshot cr.set_source_rgba(0.0, 0.0, 0.0, 0.08) cr.set_line_width(0.0) cr.rectangle(rect) cr.fill() - def draw_vertical(cr, x): + def draw_vertical(ctx, time, x, sweep_text_box_y): + cr = ctx.cr cr.set_dash([1, 3]) cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) cr.set_line_width(1.0) cr.move_to(x, 0) cr.line_to(x, height) cr.stroke() + for y in sweep_text_box_y: + draw_label_in_box_at_time(ctx.cr, BLACK, + format_label_time(ctx, time - ctx.time_origin_relative), + y+ctx.M_HEIGHT, x+ctx.n_WIDTH/2) height = int(ctx.cr.device_to_user(0,2000)[1]) - x = csec_to_xscaled(ctx, ctx.SWEEP_CSEC[0]) - draw_shading(ctx.cr, (int(x),0,int(ctx.cr.clip_extents()[0]-x),height)) - draw_vertical(ctx.cr, x) - x = csec_to_xscaled(ctx, ctx.SWEEP_CSEC[1]) - draw_shading(ctx.cr, (int(x),0,int(ctx.cr.clip_extents()[2]-x),height)) - draw_vertical(ctx.cr, x) + for i_time in [0,1]: + time = ctx.SWEEP_CSEC[i_time] + x = csec_to_xscaled(ctx, time) + draw_shading(ctx.cr, (int(x),0,int(ctx.cr.clip_extents()[i_time*2]-x),height)) + draw_vertical(ctx, time, x, sweep_text_box_y) def draw_process_bar_chart_legends(ctx, curr_y): curr_y += 30 @@ -741,6 +749,7 @@ def draw_process_bar_chart(ctx, proc_tree, times, curr_y, w, h): if ctx.proc_above_was_hidden: draw_hidden_process_separator(ctx, curr_y) ctx.proc_above_was_hidden = False + return curr_y def draw_header (ctx, headers, duration): toshow = [ @@ -930,6 +939,20 @@ def usec_to_csec(usec): def draw_event_label(ctx, label, tx, y): draw_label_in_box_at_time(ctx.cr, HIGHLIGHT_EVENT_COLOR, label, y, tx) +def format_label_time(ctx, delta): + if ctx.SWEEP_CSEC: + if abs(delta) < C.CSEC: + # less than a second, so format as whole milliseconds + return '{0:d}'.format(int(delta*10)) + else: + # format as seconds, plus a variable number of digits after the decimal point + return '{0:.{prec}f}'.format(float(delta)/C.CSEC, + prec=min(3, max(1, abs(int(3*C.CSEC/delta))))) + else: + # formatting is independent of delta value + return '{0:.{prec}f}'.format(float(delta)/C.CSEC, + prec=min(3, max(0, int(ctx.SEC_W/100)))) + def draw_process_events(ctx, proc, proc_tree, x, y): n_highlighted_events = 0 ev_list = [(ev, csec_to_xscaled(ctx, usec_to_csec(ev.time_usec))) @@ -982,21 +1005,13 @@ def draw_process_events(ctx, proc, proc_tree, x, y): if tx < last_x_touched + spacing: continue delta= float(ev.time_usec)/1000/10 - ctx.time_origin_relative - if ctx.SWEEP_CSEC: - if abs(delta) < C.CSEC: - label_str = '{0:3d}'.format(int(delta*10)) - else: - label_str = '{0:.{prec}f}'.format(float(delta)/C.CSEC, - prec=min(3, max(1, abs(int(3*C.CSEC/delta))))) - else: - # format independent of delta - label_str = '{0:.{prec}f}'.format(float(delta)/C.CSEC, - prec=min(3, max(0, int(ctx.SEC_W/100)))) + + label_str = format_label_time(ctx, delta) if label_str != last_label_str: last_x_touched = tx + draw_label_in_box_at_time( ctx.cr, PROC_TEXT_COLOR, label_str, - y + C.proc_h - 4, tx) + y + C.proc_h - 4, tx + ctx.n_WIDTH/2) last_label_str = label_str return n_highlighted_events From b1b69f6cdd4e5627895cea66799fd1ed54c45d39 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:22 -0800 Subject: [PATCH 148/182] FIX CPU accounting off by one -- cleanup --- pybootchartgui/parsing.py | 8 +++----- pybootchartgui/process_tree.py | 4 ---- pybootchartgui/samples.py | 17 ++++++----------- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 9ec8c63..a7a6157 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -282,11 +282,11 @@ def _handle_sample(processMap, writer, ltime, time, process = Process(writer, pid, tid, cmd.strip('()'), ppid, starttime) processMap[tid] = process - process.user_cpu_time[0] = userCpu - process.sys_cpu_time[0] = sysCpu + process.user_cpu_time[0] = process.user_cpu_time[1] = userCpu + process.sys_cpu_time [0] = process.sys_cpu_time [1] = sysCpu if ltime == None: # collector startup, not usually coinciding with thread startup - userCpuLoad, sysCpuLoad, c_userCpuLoad, c_sysCpuLoad = 0, 0, 0, 0 + userCpuLoad, sysCpuLoad = 0, 0 else: userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) @@ -295,8 +295,6 @@ def _handle_sample(processMap, writer, ltime, time, process.user_cpu_time[-1] = userCpu process.sys_cpu_time[-1] = sysCpu - process.c_user_cpu_time[-1] = c_userCpu - process.c_sys_cpu_time[-1] = c_sysCpu return processMap def _parse_proc_ps_log(options, writer, file): diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index 1ed5c42..e82c4f2 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -182,10 +182,6 @@ def is_inactive_process_without_children(self, p): return self.is_inactive_process(p) and \ self.num_nodes(p.child_list) == 0 -# def is_inactive_process_with_inactive_children(self, p): -# return self.is_inactive_process(p) and \ -# p.c_CPUCount() < self.options.show_high_CPU - def prune(self, process_subtree, parent, pruning_test): n_pruned = 0 idx = 0 diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index bdac675..1e9a08e 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -107,16 +107,13 @@ def __init__(self, writer, pid, tid, cmd, ppid, start_time): self.ppid = ppid self.start_time = start_time self.duration = 0 - self.samples = [] + self.samples = [] # list of ProcessCPUSample self.events = [] # time-ordered list of EventSample self.parent = None self.child_list = [] - - self.user_cpu_time = [-1, -1] - self.sys_cpu_time = [-1, -1] - self.c_user_cpu_time = [-1, -1] - self.c_sys_cpu_time = [-1, -1] + self.user_cpu_time = [None, None] # [first, last] + self.sys_cpu_time = [None, None] self.last_cpu_ns = 0 self.last_blkio_delay_ns = 0 @@ -125,14 +122,11 @@ def __init__(self, writer, pid, tid, cmd, ppid, start_time): self.draw = True # dynamic, view-dependent per-process state boolean def CPUCount(self): + ''' total CPU clock ticks reported for this process during the profiling run''' return self.user_cpu_time[-1] + self.sys_cpu_time[-1] \ - (self.user_cpu_time[0] + self.sys_cpu_time[0]) - def c_CPUCount(self): - return self.c_user_cpu_time[-1] + self.c_sys_cpu_time[-1] \ - - (self.c_user_cpu_time[0] + self.c_sys_cpu_time[0]) - - # split this process' run - triggered by a name change + # split this process' run - triggered by a name change # XX called only if taskstats.log is provided (bootchart2 daemon) def split(self, writer, pid, cmd, ppid, start_time): split = Process (writer, pid, cmd, ppid, start_time) @@ -158,6 +152,7 @@ def calc_stats(self, samplePeriod): self.activeCount = sum( [1 for sample in self.samples if sample.state != 'S']) def calc_load(self, userCpu, sysCpu, interval): + # all args in units of clock ticks userCpuLoad = float(userCpu - self.user_cpu_time[-1]) / interval sysCpuLoad = float(sysCpu - self.sys_cpu_time[-1]) / interval return (userCpuLoad, sysCpuLoad) From 2520caaae158bf61bddc42de31d2f7474dceaaf2 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:22 -0800 Subject: [PATCH 149/182] process samples show childrens time -- requires proc_ps.log --- pybootchartgui/draw.py | 70 ++++++++++++--------- pybootchartgui/parsing.py | 111 +++++++++++++++++++++++++++++---- pybootchartgui/process_tree.py | 16 ++++- pybootchartgui/samples.py | 49 ++++++++++----- 4 files changed, 188 insertions(+), 58 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 4f233e1..3b3e6b4 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -71,6 +71,7 @@ CPU_SYS_COLOR = (0.70, 0.65, 0.40, 1.0) # IO wait chart color. IO_COLOR = (0.76, 0.48, 0.48, 0.5) + PROCS_RUNNING_COLOR = (0.0, 1.0, 0.0, 1.0) PROCS_BLOCKED_COLOR = (0.7, 0.0, 0.0, 1.0) @@ -88,6 +89,8 @@ # Swap color MEM_SWAP_COLOR = DISK_TPUT_COLOR +# Process CPU load of children -- including those waited for by the parent, but not captured by any collector sample +CPU_CHILD_COLOR = (1.00, 0.70, 0.00, 1.0) # Process border color. PROC_BORDER_COLOR = (0.71, 0.71, 0.71, 1.0) # Waiting process color. @@ -725,6 +728,8 @@ def draw_process_bar_chart_legends(ctx, curr_y): PROC_COLOR_R, 10+100+curr_x, curr_y, C.leg_s) curr_x += 20 + draw_legend_box (ctx.cr, "Running (sys)", CPU_SYS_COLOR, 10+100+curr_x, curr_y, C.leg_s) + curr_x += 20 + draw_legend_box (ctx.cr, "Child CPU time lost, charged to parent", + CPU_CHILD_COLOR, 10+100+curr_x, curr_y, C.leg_s) curr_x += 20 + draw_legend_box (ctx.cr, "Sleeping", PROC_COLOR_S, 10+100+curr_x, curr_y, C.leg_s) curr_x += 20 + draw_legend_box (ctx.cr, "Zombie", @@ -902,34 +907,43 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w): last_time = max(proc.start_time, proc.samples[0].time - proc_tree.sample_period) for sample in proc.samples: - normalized = sample.cpu_sample.user + sample.cpu_sample.sys - if normalized > 0: - width = sample.time - last_time - height = normalized * C.proc_h - draw_fill_rect(ctx.cr, PROC_COLOR_R, (last_time, y+C.proc_h, width, -height)) - - # in unlikely event of no sys time at all, skip setting of color to CPU_SYS_COLOR - if sample.cpu_sample.sys > 0: - height = sample.cpu_sample.sys * C.proc_h - draw_fill_rect(ctx.cr, CPU_SYS_COLOR, (last_time, y+C.proc_h, width, -height)) - if sample.cpu_sample.sys < normalized: - # draw a separator between the bar segments, to aid the eye in - # resolving the boundary - ctx.cr.save() - ctx.cr.move_to(last_time, y+C.proc_h-height) - ctx.cr.rel_line_to(width,0) - ctx.cr.set_source_rgba(*PROC_COLOR_S) - ctx.cr.set_line_width(DEP_STROKE/2) - ctx.cr.stroke() - ctx.cr.restore() - - # If thread ran at all, draw a "speed bump", in case rect was too short to resolve. - tick_height = C.proc_h/5 - ctx.cr.arc((last_time + sample.time)/2, y+C.proc_h, tick_height, math.pi, 0.0) - ctx.cr.close_path() - ctx.cr.fill() - - last_time = sample.time + cpu_self = sample.cpu_sample.user + sample.cpu_sample.sys + cpu_exited_child = 0 # XXXX sample.exited_child_user + sample.exited_child_sys + width = sample.time - last_time + + if cpu_exited_child > 0: + height = (cpu_exited_child + cpu_self) * C.proc_h + draw_fill_rect(ctx.cr, CPU_CHILD_COLOR, (last_time, y+C.proc_h, width, -height)) + + if cpu_exited_child != 0: + print "cpu_exited_child == " + str(cpu_exited_child) + + if cpu_self > 0: + height = cpu_self * C.proc_h + draw_fill_rect(ctx.cr, PROC_COLOR_R, (last_time, y+C.proc_h, width, -height)) + + # in unlikely event of no sys time at all, skip setting of color to CPU_SYS_COLOR + if sample.cpu_sample.sys > 0: + height = sample.cpu_sample.sys * C.proc_h + draw_fill_rect(ctx.cr, CPU_SYS_COLOR, (last_time, y+C.proc_h, width, -height)) + if sample.cpu_sample.sys < cpu_self: + # draw a separator between the bar segments, to aid the eye in + # resolving the boundary + ctx.cr.save() + ctx.cr.move_to(last_time, y+C.proc_h-height) + ctx.cr.rel_line_to(width,0) + ctx.cr.set_source_rgba(*PROC_COLOR_S) + ctx.cr.set_line_width(DEP_STROKE/2) + ctx.cr.stroke() + ctx.cr.restore() + + # If thread ran at all, draw a "speed bump", in case rect was too short to resolve. + tick_height = C.proc_h/5 + ctx.cr.arc((last_time + sample.time)/2, y+C.proc_h, tick_height, math.pi, 0.0) + ctx.cr.close_path() + ctx.cr.fill() + + last_time = sample.time ctx.cr.restore() def usec_to_csec(usec): diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index a7a6157..35b1cdd 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -23,6 +23,7 @@ from time import clock from collections import defaultdict from functools import reduce +from types import * from .samples import * from .process_tree import ProcessTree @@ -141,7 +142,7 @@ def find_parent_id_for(pid): (ev.pid, ev.tid, ev.raw_log_line())) # re-parent any stray orphans if we can - if self.parent_map is not None: + if self.parent_map is not None: # requires either "kernel_pacct" or "paternity.log" for process in self.ps_stats.process_map.values(): ppid = find_parent_id_for (int(process.pid / 1000)) if ppid: @@ -270,7 +271,10 @@ def parse(block): return [parse(block) for block in blocks if block.strip() and not block.endswith(' not running\n')] def _handle_sample(processMap, writer, ltime, time, - pid, tid, cmd, state, ppid, userCpu, sysCpu, c_userCpu, c_sysCpu, starttime): + pid, tid, cmd, state, ppid, userCpu, sysCpu, c_user, c_sys, starttime): + assert(type(c_user) is IntType) + assert(type(c_sys) is IntType) + if tid in processMap: process = processMap[tid] process.cmd = cmd.strip('()') # XX loses name changes prior to the final sample @@ -281,22 +285,97 @@ def _handle_sample(processMap, writer, ltime, time, (time, starttime, time-starttime, tid/1000)) process = Process(writer, pid, tid, cmd.strip('()'), ppid, starttime) - processMap[tid] = process - process.user_cpu_time[0] = process.user_cpu_time[1] = userCpu - process.sys_cpu_time [0] = process.sys_cpu_time [1] = sysCpu + processMap[tid] = process # insert new process into the dict + process.user_cpu_ticks[0] = process.user_cpu_ticks[1] = userCpu + process.sys_cpu_ticks [0] = process.sys_cpu_ticks [1] = sysCpu if ltime == None: # collector startup, not usually coinciding with thread startup userCpuLoad, sysCpuLoad = 0, 0 else: userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) - cpuSample = ProcessCPUSample('null', userCpuLoad, sysCpuLoad, 0.0, 0.0) + cpuSample = ProcessCPUSample('null', userCpuLoad, sysCpuLoad, c_user, c_sys, 0.0, 0.0) process.samples.append(ProcessSample(time, state, cpuSample)) - process.user_cpu_time[-1] = userCpu - process.sys_cpu_time[-1] = sysCpu + # per-tid store for use by a later phase of parsing of samples gathered at this 'time' + process.user_cpu_ticks[-1] = userCpu + process.sys_cpu_ticks[-1] = sysCpu return processMap +# Consider this case: a process P and three waited-for children C1, C2, C3. +# The collector daemon bootchartd takes samples at times t-2 and t-1. +# +# t-2 t-2+e t-1-e t-1 --> +# . . . . +# bootchartd sample . . sample +# . . . . +# P R R R R +# C1 -- long-lived R R R R +# C2 -- exited R exit - - +# C3 -- phantom - fork exit - +# +# C1's CPU usage will be reported at both sample times t-2 and t-1 in the +# {utime,stime} field of /proc/PID/stat, in units of clock ticks. +# C2's usage will be reported at t-2. Any whole clock ticks thereafter will be +# accumulated by the kernel, and reported to bootchartd at t-1 in the +# {cutime,cstime} fields of its parent P, along with the rest of the clock ticks +# charged to C2 during its entire lifetime. +# C3's clock ticks will never be seen directly in any sample taken by +# bootchartd, rather only in increments to P's {cutime,cstime} fields as +# reported at t-1. +# +# We wish to graph on P's process bar at time t-1 all clock ticks consumed by +# any of its children between t-2 and t-1 that cannot be reported on the +# children's process bars -- C2's process bar ends at t-2 and C3 has none at +# all. We'll call it "lost child time". The lost clock ticks may be counted +# so: +# +# P{cutime,cstime}(t-1) - P{cutime,cstime}(t-2) - C2{utime,stime}(t-2) + +def accumulate_missing_child_ltime(processMap, ltime): + """ For only whole-process children found to have gone missing between 'ltime' and 'time' i.e. now, + accumulate clock ticks of each child's lifetime total to a counter + in the parent's Process""" + for p_p in processMap.itervalues(): + p_p.missing_child_ticks = 0 + + for c_p in processMap.itervalues(): + if c_p.ppid != 0 and \ + c_p.tid == c_p.pid and \ + c_p.samples[-1].time == ltime: # must have exited at 'time' + p_p = processMap[c_p.ppid] + p_p.missing_child_ticks += c_p.user_cpu_ticks[1] + c_p.sys_cpu_ticks[1] + continue + print "gone_missing,_last_seen_at", ltime, \ + c_p.ppid/1000, ":", c_p.pid/1000, ":", c_p.tid/1000, p_p.missing_child_ticks + +def compute_lost_child_times(processMap, ltime, time): + """ For each parent process live at 'time', find clock ticks reported by + children exiting between 'ltime' and 'time'. + calculate CPU consumption during + the sample period of newly-lost children. + Insert time-weighted value into current sample.""" + interval = time - ltime + for p_p in processMap.itervalues(): + if p_p.pid != p_p.tid or \ + p_p.samples[-1].time != time or \ + len(p_p.samples) < 2: + continue + def total_c_ticks(sample): + return sample.cpu_sample.c_user + sample.cpu_sample.c_sys + parent_c_tick_delta = total_c_ticks(p_p.samples[-1]) \ + - total_c_ticks(p_p.samples[-2]) + # See this line in the diagram and comment above. + # P{cutime,cstime}(t-1) - P{cutime,cstime}(t-2) - C2{utime,stime}(t-2) + # XX Aggregating user and sys at this phase, before stuffing result into a per-sample + # object. Some other time might be better. + lost_child_ticks = parent_c_tick_delta - p_p.missing_child_ticks + + p_p.samples[-1].lost_child = float(lost_child_ticks)/interval + if (parent_c_tick_delta != 0 or p_p.missing_child_ticks != 0): + print "compute_lost_child_times()", time, p_p.pid/1000, \ + parent_c_tick_delta, p_p.missing_child_ticks, lost_child_ticks, interval #, p_p.samples[-1].lost_child + def _parse_proc_ps_log(options, writer, file): """ * See proc(5) for details. @@ -318,14 +397,18 @@ def _parse_proc_ps_log(options, writer, file): offset = [index for index, token in enumerate(tokens[1:]) if token[-1] == ')'][0] pid, cmd, state, ppid = int(tokens[0]), ' '.join(tokens[1:2+offset]), tokens[2+offset], int(tokens[3+offset]) userCpu, sysCpu = int(tokens[13+offset]), int(tokens[14+offset]), - c_userCpu, c_sysCpu = int(tokens[15+offset]), int(tokens[16+offset]) + c_user, c_sys = int(tokens[15+offset]), int(tokens[16+offset]) starttime = int(tokens[21+offset]) # magic fixed point-ness ... pid *= 1000 ppid *= 1000 processMap = _handle_sample(processMap, writer, ltime, time, - pid, pid, cmd, state, ppid, userCpu, sysCpu, c_userCpu, c_sysCpu, starttime) + pid, pid, cmd, state, ppid, + userCpu, sysCpu, c_user, c_sys, starttime) + if ltime: + accumulate_missing_child_ltime(processMap, ltime) + compute_lost_child_times(processMap, ltime, time) ltime = time if len (timed_blocks) < 2: @@ -371,7 +454,9 @@ def _parse_proc_ps_threads_log(options, writer, file): offset = [index for index, token in enumerate(tokens[2:]) if (len(token) > 0 and token[-1] == ')')][0] pid, tid, cmd, state, ppid = int(tokens[0]), int(tokens[1]), ' '.join(tokens[2:3+offset]), tokens[3+offset], int(tokens[4+offset]) userCpu, sysCpu = int(tokens[7+offset]), int(tokens[8+offset]) - c_userCpu, c_sysCpu = int(tokens[9+offset]), int(tokens[10+offset]) + c_user, c_sys = int(tokens[9+offset]), int(tokens[10+offset]) + assert(type(c_user) is IntType) + assert(type(c_sys) is IntType) starttime = int(tokens[13+offset]) # magic fixed point-ness ... @@ -380,7 +465,8 @@ def _parse_proc_ps_threads_log(options, writer, file): ppid *= 1000 processMap = _handle_sample(processMap, writer, ltime, time, - pid, tid, cmd, state, ppid, userCpu, sysCpu, c_userCpu, c_sysCpu, starttime) + pid, tid, cmd, state, ppid, + userCpu, sysCpu, c_user, c_sys, starttime) ltime = time if len (timed_blocks) < 2: @@ -458,6 +544,7 @@ def _parse_taskstats_log(writer, file): if delta_cpu_ns + delta_blkio_delay_ns + delta_swapin_delay_ns > 0: # print "proc %s cpu_ns %g delta_cpu %g" % (cmd, cpu_ns, delta_cpu_ns) cpuSample = ProcessCPUSample('null', delta_cpu_ns, 0.0, + 0, 0, delta_blkio_delay_ns, delta_swapin_delay_ns) process.samples.append(ProcessSample(time, state, cpuSample)) diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index e82c4f2..75a802e 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -61,6 +61,8 @@ def __init__(self, writer, kernel, psstats, sample_period, if not accurate_parentage: self.update_ppids_for_daemons(self.process_list) + self.init_lost_child_times() # time delta + self.start_time = self.get_start_time(self.process_tree) self.end_time = self.get_end_time(self.process_tree) self.options = options @@ -104,6 +106,18 @@ def build(self): else: proc.parent.child_list.append(proc) + def init_lost_child_times(self): + for c in self.process_list: + p = c.parent + return + for p in self.process_list: + for s in p.samples: + child_user_ticks = 0 + child_sys_ticks = 0 + for c in proc.child_list: + child_user_ticks += c.samples[s.time].user + child_sys_ticks += c.samples[s.time].sys + def sort(self, process_subtree): """Sort process tree.""" for p in process_subtree: @@ -175,7 +189,7 @@ def update_ppids_for_daemons(self, process_list): self.build() def is_inactive_process(self, p): - return p.CPUCount() < self.options.show_high_CPU and \ + return p.cpu_tick_count_during_run() < self.options.show_high_CPU and \ (p.activeCount < 1 and len(p.events) == 0) def is_inactive_process_without_children(self, p): diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 1e9a08e..3c899ae 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU General Public License # along with pybootchartgui. If not, see . +from types import * class EventSample: def __init__(self, time, time_usec, pid, tid, comm, match, raw_log_file, raw_log_seek): @@ -53,13 +54,18 @@ def __init__(self, time, user, sys, io, procs_running, procs_blocked): self.procs_blocked = procs_blocked class ProcessCPUSample: - def __init__(self, time, user, sys, io, swap): + def __init__(self, time, user, sys, c_user, c_sys, io, swap): self.time = time self.user = user self.sys = sys + self.c_user = c_user # directly from /proc: accumulates upon exit of waited-for child + self.c_sys = c_sys self.io = io # taskstats-specific self.swap = swap # taskstats-specific + assert(type(self.c_user) is IntType) + assert(type(self.c_sys) is IntType) + @property def cpu(self): return self.user + self.sys @@ -68,6 +74,18 @@ def __str__(self): return str(self.time) + "\t" + str(self.user) + "\t" + \ str(self.sys) + "\t" + str(self.io) + "\t" + str (self.swap) +class ProcessSample: + def __init__(self, time, state, cpu_sample): + self.time = time + self.state = state + self.cpu_sample = cpu_sample # ProcessCPUSample + + # delta per sample interval. Computed later in parsing. + self.lost_child = None + + def __str__(self): + return str(self.time) + "\t" + str(self.state) + "\t" + str(self.cpu_sample) + class MemSample: def __init__(self, time): self.time = time @@ -76,14 +94,6 @@ def __init__(self, time): def add_value(self, name, value): self.records[name] = value -class ProcessSample: - def __init__(self, time, state, cpu_sample): - self.time = time - self.state = state - self.cpu_sample = cpu_sample # tuple - - def __str__(self): - return str(self.time) + "\t" + str(self.state) + "\t" + str(self.cpu_sample) class ProcessStats: """stats over the collection of all processes, all samples""" @@ -112,19 +122,24 @@ def __init__(self, writer, pid, tid, cmd, ppid, start_time): self.parent = None self.child_list = [] - self.user_cpu_time = [None, None] # [first, last] - self.sys_cpu_time = [None, None] + self.user_cpu_ticks = [None, None] # [first, last] + self.sys_cpu_ticks = [None, None] + + # For transient use as an accumulator during early parsing -- when + # concurrent samples of all threads can be accessed O(1). + self.missing_child_ticks = None self.last_cpu_ns = 0 self.last_blkio_delay_ns = 0 self.last_swapin_delay_ns = 0 - self.draw = True # dynamic, view-dependent per-process state boolean + # dynamic, view-dependent per-process state boolean + self.draw = True - def CPUCount(self): + def cpu_tick_count_during_run(self): ''' total CPU clock ticks reported for this process during the profiling run''' - return self.user_cpu_time[-1] + self.sys_cpu_time[-1] \ - - (self.user_cpu_time[0] + self.sys_cpu_time[0]) + return self.user_cpu_ticks[-1] + self.sys_cpu_ticks[-1] \ + - (self.user_cpu_ticks[0] + self.sys_cpu_ticks[0]) # split this process' run - triggered by a name change # XX called only if taskstats.log is provided (bootchart2 daemon) @@ -153,8 +168,8 @@ def calc_stats(self, samplePeriod): def calc_load(self, userCpu, sysCpu, interval): # all args in units of clock ticks - userCpuLoad = float(userCpu - self.user_cpu_time[-1]) / interval - sysCpuLoad = float(sysCpu - self.sys_cpu_time[-1]) / interval + userCpuLoad = float(userCpu - self.user_cpu_ticks[-1]) / interval + sysCpuLoad = float(sysCpu - self.sys_cpu_ticks[-1]) / interval return (userCpuLoad, sysCpuLoad) def set_parent(self, processMap): From df52172e07d4324a1b4a015c47707f962f27c8ae Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:23 -0800 Subject: [PATCH 150/182] DISABLE compute_lost_child_times --- pybootchartgui/parsing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 35b1cdd..9cd9b7c 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -408,7 +408,7 @@ def _parse_proc_ps_log(options, writer, file): userCpu, sysCpu, c_user, c_sys, starttime) if ltime: accumulate_missing_child_ltime(processMap, ltime) - compute_lost_child_times(processMap, ltime, time) + # compute_lost_child_times(processMap, ltime, time) ltime = time if len (timed_blocks) < 2: From 9341ffad96ae0f4010bfceb9c4c32fc714b84797 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:23 -0800 Subject: [PATCH 151/182] FIX per thread CPU accounting --- pybootchartgui/parsing.py | 22 ++++++++++++++++------ pybootchartgui/samples.py | 4 ++-- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 9cd9b7c..7e3f6ea 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -270,6 +270,13 @@ def parse(block): blocks = file.read().split('\n\n') return [parse(block) for block in blocks if block.strip() and not block.endswith(' not running\n')] +# Cases to handle: +# 1. run starting (ltime==None) +# 1.1 thread started in preceding sample_period +# 1.2 thread started earlier +# 2. run continuing +# 2.1 thread continues (tid in processMap) +# 2.2 thread starts def _handle_sample(processMap, writer, ltime, time, pid, tid, cmd, state, ppid, userCpu, sysCpu, c_user, c_sys, starttime): assert(type(c_user) is IntType) @@ -285,14 +292,17 @@ def _handle_sample(processMap, writer, ltime, time, (time, starttime, time-starttime, tid/1000)) process = Process(writer, pid, tid, cmd.strip('()'), ppid, starttime) + if ltime: # process is starting during profiling run + process.user_cpu_ticks[0] = 0 + process.sys_cpu_ticks [0] = 0 + ltime = starttime + else: + process.user_cpu_ticks[0] = userCpu + process.sys_cpu_ticks [0] = sysCpu + ltime = -100000 # XX hacky way of forcing reported load toward zero processMap[tid] = process # insert new process into the dict - process.user_cpu_ticks[0] = process.user_cpu_ticks[1] = userCpu - process.sys_cpu_ticks [0] = process.sys_cpu_ticks [1] = sysCpu - if ltime == None: # collector startup, not usually coinciding with thread startup - userCpuLoad, sysCpuLoad = 0, 0 - else: - userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) + userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) cpuSample = ProcessCPUSample('null', userCpuLoad, sysCpuLoad, c_user, c_sys, 0.0, 0.0) process.samples.append(ProcessSample(time, state, cpuSample)) diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 3c899ae..c4db9a3 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -122,8 +122,8 @@ def __init__(self, writer, pid, tid, cmd, ppid, start_time): self.parent = None self.child_list = [] - self.user_cpu_ticks = [None, None] # [first, last] - self.sys_cpu_ticks = [None, None] + self.user_cpu_ticks = [None, 0] # [first, last] + self.sys_cpu_ticks = [None, 0] # For transient use as an accumulator during early parsing -- when # concurrent samples of all threads can be accessed O(1). From 5de2b046da4de87589623adadc6be7b4b19b8a33 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:23 -0800 Subject: [PATCH 152/182] FIX process hiding heuristic --- pybootchartgui/main.py.in | 9 ++++++--- pybootchartgui/process_tree.py | 12 +++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index d03f75a..2f28618 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -40,9 +40,12 @@ def _mk_options_parser(): parser.add_option("--merge", dest="merge", default=False, help="Prune the process tree of all activity from dull children: " + "stats are added to parent, child process is lost in drawing of chart.") - parser.add_option("-C", "--show-high-CPU", dest="show_high_CPU", type="int", default=-1, metavar="CSEC", - help="Suppress hiding of any thread consuming more than CSEC of CPU time [default %default]." + - " Hiding means it's not shown on initial rendering, but can be shown by a mouse click.") + parser.add_option("-H", "--hide-low-CPU", dest="hide_low_CPU", type="int", default=-1, metavar="CSEC", + help="Hide any thread consuming less than CSEC of CPU time." + + " Hiding means the thread is not shown on initial rendering, but can be shown by a mouse click." + + " A value of zero may be specified, meaning the special case of threads that report 0 CSECs" + "consumed, but are known to have executed due to having been born, died, or reported some" + " state other than 'S' when sampled by the collector.") parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="suppress informational messages") parser.add_option("--verbose", action="store_true", dest="verbose", default=False, diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index 75a802e..4db71f2 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -77,7 +77,7 @@ def __init__(self, writer, kernel, psstats, sample_period, removed = self.merge_logger(self.process_tree, self.LOGGER_PROC, monitoredApp, False) writer.status("merged %i logger processes" % removed) - p_processes = self.prune(self.process_tree, None, self.is_inactive_process_without_children) + p_processes = self.prune(self.process_tree, None, self.is_inactive_process) writer.status("hid %i processes" % p_processes) if options.merge: @@ -188,9 +188,15 @@ def update_ppids_for_daemons(self, process_list): p.child_list = [] self.build() + def is_active_process(self, p): + # (self.options.hide_low_CPU == 0) is a special case, documented in the usage message. + return (self.options.hide_low_CPU == 0 and \ + (p.activeCount > 0 or len(p.samples) != len(self.process_list[0].samples))) or \ + p.cpu_tick_count_during_run() > self.options.hide_low_CPU or \ + len(p.events) > 0 # any event counts as activity + def is_inactive_process(self, p): - return p.cpu_tick_count_during_run() < self.options.show_high_CPU and \ - (p.activeCount < 1 and len(p.events) == 0) + return not self.is_active_process(p) def is_inactive_process_without_children(self, p): return self.is_inactive_process(p) and \ From 9d0670fc2dfa54ecf33f263a3068c70a7b6dac00 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:23 -0800 Subject: [PATCH 153/182] draw show overflows --- pybootchartgui/draw.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 3b3e6b4..d5461ea 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -42,6 +42,7 @@ BLACK = (0.0, 0.0, 0.0, 1.0) NOTEPAD_YELLLOW = (0.95, 0.95, 0.8, 1.0) PURPLE = (0.6, 0.1, 0.6, 1.0) +RED = (1.0, 0.0, 0.0) # Process tree border color. BORDER_COLOR = (0.63, 0.63, 0.63, 1.0) @@ -918,6 +919,11 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w): if cpu_exited_child != 0: print "cpu_exited_child == " + str(cpu_exited_child) + if cpu_self > 1.0: + print "process CPU time overflow: ", proc.tid, sample.time, width, cpu_self + OVERFLOW_BAR_HEIGHT=2 + draw_fill_rect(ctx.cr, PURPLE, (last_time, y, width, OVERFLOW_BAR_HEIGHT)) + cpu_self = 1.0 - float(OVERFLOW_BAR_HEIGHT)/C.proc_h if cpu_self > 0: height = cpu_self * C.proc_h draw_fill_rect(ctx.cr, PROC_COLOR_R, (last_time, y+C.proc_h, width, -height)) From 96533050b1b784178ba3de449c35ad550fde8305 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:23 -0800 Subject: [PATCH 154/182] FIX process drawing --- pybootchartgui/draw.py | 80 ++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index d5461ea..70daa51 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -790,21 +790,52 @@ def draw_header (ctx, headers, duration): return header_y -def draw_process(ctx, proc, proc_tree, x, y, w): - draw_process_activity_colors(ctx, proc, proc_tree, x, y, w) - - # Do not draw right-hand vertical border -- process exit never exactly known +# Cairo draws lines "on-center", so to draw a one-pixel width horizontal line +# using the (default) 1:1 transform from user-space to device-space, +# the Y coordinate must be offset by 1/2 user-coord. +SEPARATOR_THICKNESS = 1.0 +SEP_HALF = SEPARATOR_THICKNESS / 2 +USER_HALF = 0.5 +BAR_HEIGHT = C.proc_h - SEPARATOR_THICKNESS + +def draw_visible_process_separator(ctx, proc, x, y, w): + ctx.cr.save() ctx.cr.set_source_rgba(*PROC_BORDER_COLOR) - ctx.cr.set_line_width(1.0) + ctx.cr.set_line_width(SEPARATOR_THICKNESS) ctx.cr.move_to(x+w, y) ctx.cr.rel_line_to(-w, 0) if proc.start_time < ctx.time_origin_drawn: ctx.cr.stroke() ctx.cr.move_to(x, y+C.proc_h) else: + # XX No attempt to align the vertical line with the device pixel grid ctx.cr.rel_line_to(0, C.proc_h) ctx.cr.rel_line_to(w, 0) ctx.cr.stroke() + ctx.cr.restore() + +def draw_hidden_process_separator(ctx, y): + DARK_GREY = 1.0, 1.0, 1.0 + GREY = 0.3, 0.3, 0.3 + ctx.cr.save() + ctx.cr.set_source_rgb(0.0, 1.0, 0.0) + ctx.cr.set_line_width(SEPARATOR_THICKNESS) + def draw_again(): + ctx.cr.move_to(ctx.cr.clip_extents()[0], y) + ctx.cr.line_to(ctx.cr.clip_extents()[2], y) + ctx.cr.stroke() + ctx.cr.set_source_rgb(*DARK_GREY) + draw_again() + ctx.cr.set_source_rgb(*GREY) + ctx.cr.set_dash([1, 4]) + draw_again() + ctx.cr.restore() + +def draw_process(ctx, proc, proc_tree, x, y, w): + draw_process_activity_colors(ctx, proc, proc_tree, x, y, w) + + # Do not draw right-hand vertical border -- process exit never exactly known + draw_visible_process_separator(ctx, proc, x, y, w) draw_process_state_colors(ctx, proc, proc_tree, x, y, w) @@ -858,9 +889,9 @@ def draw_processes_recursively(ctx, proc, proc_tree, y): ctx.proc_above_was_hidden = True child_y = y else: - n_highlighted_events = draw_process(ctx, proc, proc_tree, x, y, w) + n_highlighted_events = draw_process(ctx, proc, proc_tree, x, y+USER_HALF, w) if ctx.proc_above_was_hidden: - draw_hidden_process_separator(ctx, y) + draw_hidden_process_separator(ctx, y+USER_HALF) ctx.proc_above_was_hidden = False child_y = y + C.proc_h*(1 if n_highlighted_events <= 0 else 2) @@ -870,25 +901,11 @@ def draw_processes_recursively(ctx, proc, proc_tree, y): if proc.draw and child.draw: # draw upward from child to elder sibling or parent (proc) # XX draws lines on top of the process name label - draw_process_connecting_lines(ctx, x, y, child_x, child_y, elder_sibling_y) + #draw_process_connecting_lines(ctx, x, y, child_x, child_y, elder_sibling_y) elder_sibling_y = child_y child_y = next_y return x, child_y -def draw_hidden_process_separator(ctx, y): - ctx.cr.save() - def draw_again(): - ctx.cr.move_to(ctx.cr.clip_extents()[0], y) - ctx.cr.line_to(ctx.cr.clip_extents()[2], y) - ctx.cr.stroke() - ctx.cr.set_line_width(1.0) - ctx.cr.set_source_rgb(1.0, 1.0, 1.0) - draw_again() - ctx.cr.set_source_rgb(0.3, 0.3, 0.3) - ctx.cr.set_dash([1, 6]) - draw_again() - ctx.cr.restore() - def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w): draw_fill_rect(ctx.cr, PROC_COLOR_S, (x, y, w, C.proc_h)) @@ -907,14 +924,15 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w): # 2. proc start after sampling last_time = max(proc.start_time, proc.samples[0].time - proc_tree.sample_period) + for sample in proc.samples: cpu_self = sample.cpu_sample.user + sample.cpu_sample.sys cpu_exited_child = 0 # XXXX sample.exited_child_user + sample.exited_child_sys width = sample.time - last_time if cpu_exited_child > 0: - height = (cpu_exited_child + cpu_self) * C.proc_h - draw_fill_rect(ctx.cr, CPU_CHILD_COLOR, (last_time, y+C.proc_h, width, -height)) + height = (cpu_exited_child + cpu_self) * BAR_HEIGHT + draw_fill_rect(ctx.cr, CPU_CHILD_COLOR, (last_time, y+C.proc_h-SEP_HALF, width, -height)) if cpu_exited_child != 0: print "cpu_exited_child == " + str(cpu_exited_child) @@ -922,21 +940,21 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w): if cpu_self > 1.0: print "process CPU time overflow: ", proc.tid, sample.time, width, cpu_self OVERFLOW_BAR_HEIGHT=2 - draw_fill_rect(ctx.cr, PURPLE, (last_time, y, width, OVERFLOW_BAR_HEIGHT)) + draw_fill_rect(ctx.cr, PURPLE, (last_time, y+SEP_HALF, width, OVERFLOW_BAR_HEIGHT)) cpu_self = 1.0 - float(OVERFLOW_BAR_HEIGHT)/C.proc_h if cpu_self > 0: - height = cpu_self * C.proc_h - draw_fill_rect(ctx.cr, PROC_COLOR_R, (last_time, y+C.proc_h, width, -height)) + height = cpu_self * BAR_HEIGHT + draw_fill_rect(ctx.cr, PROC_COLOR_R, (last_time, y+C.proc_h-SEP_HALF, width, -height)) # in unlikely event of no sys time at all, skip setting of color to CPU_SYS_COLOR if sample.cpu_sample.sys > 0: - height = sample.cpu_sample.sys * C.proc_h - draw_fill_rect(ctx.cr, CPU_SYS_COLOR, (last_time, y+C.proc_h, width, -height)) + height = sample.cpu_sample.sys * BAR_HEIGHT + draw_fill_rect(ctx.cr, CPU_SYS_COLOR, (last_time, y+C.proc_h-SEP_HALF, width, -height)) if sample.cpu_sample.sys < cpu_self: # draw a separator between the bar segments, to aid the eye in # resolving the boundary ctx.cr.save() - ctx.cr.move_to(last_time, y+C.proc_h-height) + ctx.cr.move_to(last_time, y+C.proc_h-SEP_HALF-height) ctx.cr.rel_line_to(width,0) ctx.cr.set_source_rgba(*PROC_COLOR_S) ctx.cr.set_line_width(DEP_STROKE/2) @@ -945,7 +963,7 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w): # If thread ran at all, draw a "speed bump", in case rect was too short to resolve. tick_height = C.proc_h/5 - ctx.cr.arc((last_time + sample.time)/2, y+C.proc_h, tick_height, math.pi, 0.0) + ctx.cr.arc((last_time + sample.time)/2, y+C.proc_h-SEP_HALF, tick_height, math.pi, 0.0) ctx.cr.close_path() ctx.cr.fill() From 75e30a65e2b2057f584cc57a52c56bf828724c69 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:24 -0800 Subject: [PATCH 155/182] whole system bars on exact pixels --- pybootchartgui/draw.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 70daa51..be65fc5 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -362,7 +362,7 @@ def plot_segment_positive(cr, point, x, y): if point[1] <= 0: # zero-Y samples draw nothing cr.move_to(x, y) return - cr.set_line_width(1.5) + cr.set_line_width(1.0) cr.line_to(x, y) def _plot_scatter_positive(cr, point, x, y, w, h): @@ -462,7 +462,7 @@ def render_charts(ctx, trace, curr_y, w, h): curr_x += draw_legend_diamond(ctx.cr, "Blocked threads -- Uninterruptible Syscall", PROCS_BLOCKED_COLOR, curr_x +70, curr_y, C.leg_s, C.leg_s) - chart_rect = (0, curr_y+10, w, C.bar_h) + chart_rect = (0, curr_y+10+USER_HALF, w, C.bar_h) draw_box (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) # render I/O wait -- a backwards delta @@ -518,7 +518,7 @@ def render_charts(ctx, trace, curr_y, w, h): draw_text(ctx.cr, partition.name, TEXT_COLOR, 0, curr_y+18) # utilization -- inherently normalized [0,1] - chart_rect = (0, curr_y+18+5, w, C.bar_h) + chart_rect = (0, curr_y+18+5+USER_HALF, w, C.bar_h) draw_box (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) # a backwards delta @@ -556,7 +556,7 @@ def render_charts(ctx, trace, curr_y, w, h): curr_y += 18+C.bar_h # render mem usage - chart_rect = (0, curr_y+30, w, meminfo_bar_h) + chart_rect = (0, curr_y+30+USER_HALF, w, meminfo_bar_h) mem_stats = trace.mem_stats if mem_stats: mem_scale = max(sample.records['MemTotal'] - sample.records['MemFree'] for sample in mem_stats) From 8b2a6364e2e4eeb51b94c29de2352bca088209ac Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:24 -0800 Subject: [PATCH 156/182] events highlighted event times step on everything else --- pybootchartgui/draw.py | 83 ++++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index be65fc5..af5ffce 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -38,9 +38,12 @@ # Process tree background color. BACK_COLOR = (1.0, 1.0, 1.0, 1.0) +TRANSPARENT = (0.0, 0.0, 0.0, 0.0) + WHITE = (1.0, 1.0, 1.0, 1.0) BLACK = (0.0, 0.0, 0.0, 1.0) -NOTEPAD_YELLLOW = (0.95, 0.95, 0.8, 1.0) +DARK_GREY = (0.1, 0.1, 0.1) +NOTEPAD_YELLOW = (0.95, 0.95, 0.8, 1.0) PURPLE = (0.6, 0.1, 0.6, 1.0) RED = (1.0, 0.0, 0.0) @@ -201,10 +204,19 @@ def draw_legend_line(cr, label, fill_color, x, y, s): cr.fill() draw_text(cr, label, TEXT_COLOR, x + s + 5, y) -def draw_label_in_box_at_time(cr, color, label, y, label_x): +def draw_label_at_time(cr, color, label, y, label_x): draw_text(cr, label, color, label_x, y) - return cr.text_extents(label)[2] - + return cr.text_extents(label)[2] # return width + +# FIXME: Name assumes cr includes time->user transform +# y is at process bar boundary above +def draw_label_on_bg(cr, bg_color, color, label, y, label_x): + TOP_MARGIN=3 # user-space + label = label + x_bearing, y_bearing, width, height, x_advance, y_advance = cr.text_extents(label) + draw_fill_rect(cr, bg_color, (label_x, y+1, x_advance, C.proc_h-2)) + draw_text(cr, label, color, label_x, y-y_bearing+TOP_MARGIN) + return x_advance def _time_origin_drawn(ctx, trace): if ctx.app_options.prehistory: @@ -266,7 +278,7 @@ def per_render_init(self, cr, time_origin_drawn, SEC_W, sweep_csec): def proc_tree (self, trace): return trace.kernel_tree if self.kernel_only else trace.proc_tree - def draw_label_in_box(self, color, label, + def draw_process_label_in_box(self, color, label, x, y, w, minx, maxx): # hack in a pair of left and right margins extents = self.cr.text_extents("j" + label + "k") # XX "j", "k" were found by tedious trial-and-error @@ -285,7 +297,7 @@ def draw_label_in_box(self, color, label, if label_x < minx: label_x = minx # XX ugly magic constants, tuned by trial-and-error - draw_fill_rect(self.cr, NOTEPAD_YELLLOW, (label_x, y-1, label_w, -(C.proc_h-2))) + draw_fill_rect(self.cr, NOTEPAD_YELLOW, (label_x, y-1, label_w, -(C.proc_h-2))) draw_text(self.cr, label, color, label_x, y-4) # XX Should be "_csec_to_user" @@ -707,7 +719,7 @@ def draw_vertical(ctx, time, x, sweep_text_box_y): cr.line_to(x, height) cr.stroke() for y in sweep_text_box_y: - draw_label_in_box_at_time(ctx.cr, BLACK, + draw_label_at_time(ctx.cr, BLACK, format_label_time(ctx, time - ctx.time_origin_relative), y+ctx.M_HEIGHT, x+ctx.n_WIDTH/2) @@ -858,7 +870,7 @@ def draw_process(ctx, proc, proc_tree, x, y, w): if ctx.app_options.show_all and proc.args: cmdString += " '" + "' '".join(proc.args) + "'" - ctx.draw_label_in_box( PROC_TEXT_COLOR, cmdString, + ctx.draw_process_label_in_box( PROC_TEXT_COLOR, cmdString, csec_to_xscaled(ctx, max(proc.start_time,ctx.time_origin_drawn)), y, w, @@ -975,7 +987,7 @@ def usec_to_csec(usec): return float(usec) / 1000 / 10 def draw_event_label(ctx, label, tx, y): - draw_label_in_box_at_time(ctx.cr, HIGHLIGHT_EVENT_COLOR, label, y, tx) + draw_label_at_time(ctx.cr, HIGHLIGHT_EVENT_COLOR, label, y, tx) def format_label_time(ctx, delta): if ctx.SWEEP_CSEC: @@ -991,6 +1003,35 @@ def format_label_time(ctx, delta): return '{0:.{prec}f}'.format(float(delta)/C.CSEC, prec=min(3, max(0, int(ctx.SEC_W/100)))) +def print_event_times(ctx, y, ev_list): + ctx.cr.set_source_rgba(*EVENT_COLOR) + width = ctx.cr.text_extents("00")[2] + last_x_touched = 0 + last_label_str = None + for (ev, tx) in ev_list: + for ev_re in ctx.highlight_event__match_RE: + m = re.search(ev_re, ev.match) + if m: + break; + if not m and tx < last_x_touched + width: + continue + delta= float(ev.time_usec)/1000/10 - ctx.time_origin_relative + + label_str = format_label_time(ctx, delta) + if m or label_str != last_label_str: + if m: + # freely step on top of any time labels drawn earlier + last_x_touched = tx + draw_label_on_bg( + ctx.cr, + WHITE, + HIGHLIGHT_EVENT_COLOR, label_str, y, tx) + else: + last_x_touched = tx + draw_label_on_bg( + ctx.cr, + TRANSPARENT, + DARK_GREY, label_str, y, tx) + last_label_str = label_str + def draw_process_events(ctx, proc, proc_tree, x, y): n_highlighted_events = 0 ev_list = [(ev, csec_to_xscaled(ctx, usec_to_csec(ev.time_usec))) @@ -998,8 +1039,13 @@ def draw_process_events(ctx, proc, proc_tree, x, y): if not ev_list: return n_highlighted_events + # draw numbers + if ctx.app_options.print_event_times: + print_event_times(ctx, y, ev_list) + # draw ticks, maybe add to dump list for (ev, tx) in ev_list: + # FIXME: Optimize by doing re.search() per-regexp change, rather than per-rendering? for ev_re in ctx.highlight_event__match_RE: m = re.search(ev_re, ev.match) if m: @@ -1032,25 +1078,6 @@ def draw_process_events(ctx, proc, proc_tree, x, y): ctx.cr.close_path() ctx.cr.fill() - # draw numbers - if not ctx.app_options.print_event_times: - return n_highlighted_events - ctx.cr.set_source_rgba(*EVENT_COLOR) - spacing = ctx.cr.text_extents("00")[2] - last_x_touched = 0 - last_label_str = None - for (ev, tx) in ev_list: - if tx < last_x_touched + spacing: - continue - delta= float(ev.time_usec)/1000/10 - ctx.time_origin_relative - - label_str = format_label_time(ctx, delta) - if label_str != last_label_str: - last_x_touched = tx + draw_label_in_box_at_time( - ctx.cr, PROC_TEXT_COLOR, - label_str, - y + C.proc_h - 4, tx + ctx.n_WIDTH/2) - last_label_str = label_str return n_highlighted_events def draw_process_state_colors(ctx, proc, proc_tree, x, y, w): From ee5cd3ea9e3eb9ab0805ba0f0eefb77a8ef00af2 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Thu, 6 Dec 2012 18:52:42 -0800 Subject: [PATCH 157/182] draw -- sweep time labels always on screen --- pybootchartgui/draw.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index af5ffce..c37485c 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -208,7 +208,6 @@ def draw_label_at_time(cr, color, label, y, label_x): draw_text(cr, label, color, label_x, y) return cr.text_extents(label)[2] # return width -# FIXME: Name assumes cr includes time->user transform # y is at process bar boundary above def draw_label_on_bg(cr, bg_color, color, label, y, label_x): TOP_MARGIN=3 # user-space @@ -649,7 +648,6 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): proc_height -= C.CUML_HEIGHT curr_y += ctx.M_HEIGHT - sweep_text_box_y = [] # [curr_y+ctx.M_HEIGHT] # curr_y points to the *top* of the first per-process line if hide_process_y and hide_process_y[0] > (curr_y - C.proc_h/4): @@ -676,8 +674,7 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): draw_cuml_graph(ctx, proc_tree, cuml_rect, duration, STAT_TYPE_IO) if ctx.SWEEP_CSEC: - sweep_text_box_y.append(curr_y+ctx.M_HEIGHT) - draw_sweep(ctx, sweep_text_box_y) + draw_sweep(ctx) ctx.cr.restore() @@ -703,14 +700,14 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): ctx.event_dump_list = None -def draw_sweep(ctx, sweep_text_box_y): +def draw_sweep(ctx): def draw_shading(cr, rect): # alpha value of the rgba strikes a compromise between appearance on screen, and in printed screenshot cr.set_source_rgba(0.0, 0.0, 0.0, 0.08) cr.set_line_width(0.0) cr.rectangle(rect) cr.fill() - def draw_vertical(ctx, time, x, sweep_text_box_y): + def draw_vertical(ctx, time, x): cr = ctx.cr cr.set_dash([1, 3]) cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) @@ -718,17 +715,19 @@ def draw_vertical(ctx, time, x, sweep_text_box_y): cr.move_to(x, 0) cr.line_to(x, height) cr.stroke() - for y in sweep_text_box_y: - draw_label_at_time(ctx.cr, BLACK, - format_label_time(ctx, time - ctx.time_origin_relative), - y+ctx.M_HEIGHT, x+ctx.n_WIDTH/2) height = int(ctx.cr.device_to_user(0,2000)[1]) - for i_time in [0,1]: + for i_time, label_offset in enumerate([-ctx.cr.text_extents("_0.0")[2], 0]): time = ctx.SWEEP_CSEC[i_time] x = csec_to_xscaled(ctx, time) draw_shading(ctx.cr, (int(x),0,int(ctx.cr.clip_extents()[i_time*2]-x),height)) - draw_vertical(ctx, time, x, sweep_text_box_y) + draw_vertical(ctx, time, x) + draw_label_on_bg(ctx.cr, NOTEPAD_YELLOW, BLACK, + "0.0" if + i_time==0 else + "{0:.6f}".format((time - ctx.time_origin_relative)/C.CSEC), + ctx.cr.device_to_user(0, 0)[1], + x + label_offset + ctx.n_WIDTH/2) def draw_process_bar_chart_legends(ctx, curr_y): curr_y += 30 From d9291c2d75acd9a960f88ab026b23de9f856515f Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 12 Dec 2012 13:30:08 -0800 Subject: [PATCH 158/182] draw -- better time labels --- pybootchartgui/draw.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index c37485c..1eb0410 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -717,17 +717,27 @@ def draw_vertical(ctx, time, x): cr.stroke() height = int(ctx.cr.device_to_user(0,2000)[1]) - for i_time, label_offset in enumerate([-ctx.cr.text_extents("_0.0")[2], 0]): + x_itime = [None, None] + for i_time, time in enumerate(ctx.SWEEP_CSEC): time = ctx.SWEEP_CSEC[i_time] x = csec_to_xscaled(ctx, time) draw_shading(ctx.cr, (int(x),0,int(ctx.cr.clip_extents()[i_time*2]-x),height)) draw_vertical(ctx, time, x) - draw_label_on_bg(ctx.cr, NOTEPAD_YELLOW, BLACK, - "0.0" if - i_time==0 else - "{0:.6f}".format((time - ctx.time_origin_relative)/C.CSEC), - ctx.cr.device_to_user(0, 0)[1], - x + label_offset + ctx.n_WIDTH/2) + x_itime[i_time] = x + + top = ctx.cr.device_to_user(0, 0)[1] + origin = 0 if ctx.app_options.absolute_uptime_event_times else \ + ctx.time_origin_drawn + ctx.trace.proc_tree.sample_period + draw_label_on_bg(ctx.cr, NOTEPAD_YELLOW, BLACK, + "{0:.6f}".format((ctx.SWEEP_CSEC[0] - origin)/C.CSEC), + top, + x_itime[0] + ctx.n_WIDTH/2) + if i_time == 0: + return + draw_label_on_bg(ctx.cr, NOTEPAD_YELLOW, BLACK, + "+{0:.6f}".format((ctx.SWEEP_CSEC[1] - ctx.SWEEP_CSEC[0])/C.CSEC), + top + ctx.M_HEIGHT*2, + x_itime[1] + ctx.n_WIDTH/2) def draw_process_bar_chart_legends(ctx, curr_y): curr_y += 30 From f786b8a5b76ce4e98a0ebd045b9b1455e609a6e7 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:24 -0800 Subject: [PATCH 159/182] legends stay onscreen --- pybootchartgui/draw.py | 55 +++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 1eb0410..088803d 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -28,8 +28,8 @@ DrawConsts = collections.namedtuple('XXtypename', # |height of a process, in user-space # | |taskstats-specific - ['CSEC','bar_h','off_x','off_y','proc_h','leg_s','CUML_HEIGHT','MIN_IMG_W']) -C = DrawConsts( 100, 55, 10, 10, 16, 11, 2000, 800) + ['CSEC','bar_h','off_x','off_y','proc_h','leg_s','CUML_HEIGHT','MIN_IMG_W','legend_indent']) +C = DrawConsts( 100, 55, 10, 10, 16, 11, 2000, 800, 5) # Derived constants # XX create another namedtuple for these @@ -118,9 +118,8 @@ PROC_TEXT_FONT_SIZE = 12 # Event tick color. -DIM_EVENT_COLOR = (0.3, 0.3, 0.3) -EVENT_COLOR = (0.1, 0.1, 0.1) -HIGHLIGHT_EVENT_COLOR = (0.6, 0.0, 0.6) +DIM_EVENT_COLOR = (0.2, 0.2, 0.2) +HIGHLIGHT_EVENT_COLOR = (0.4, 0.0, 0.6) # Signature color. SIG_COLOR = (0.0, 0.0, 0.0, 0.3125) @@ -193,7 +192,7 @@ def draw_diamond(cr, x, y, w, h): def draw_legend_diamond(cr, label, fill_color, x, y, w, h): cr.set_source_rgba(*fill_color) draw_diamond(cr, x, y-h/2, w, h) - return draw_text(cr, label, TEXT_COLOR, x + w + 5, y) + return draw_text(cr, label, TEXT_COLOR, x + w, y) def draw_legend_box(cr, label, fill_color, x, y, s): draw_fill_rect(cr, fill_color, (x, y - s, s, s)) @@ -459,12 +458,13 @@ def extents(ctx, xscale, trace): def render_charts(ctx, trace, curr_y, w, h): proc_tree = ctx.proc_tree(trace) + x_onscreen = max(0, ctx.cr.device_to_user(0, 0)[0]) if ctx.app_options.show_legends: # render bar legend ctx.cr.set_font_size(LEGEND_FONT_SIZE) curr_y += 20 - curr_x = 0 + curr_x = C.legend_indent + x_onscreen curr_x += 20 + draw_legend_box(ctx.cr, "CPU (user)", CPU_COLOR, curr_x, curr_y, C.leg_s) curr_x += 20 + draw_legend_box(ctx.cr, "CPU (sys)", CPU_SYS_COLOR, curr_x, curr_y, C.leg_s) curr_x += 20 + draw_legend_box(ctx.cr, "I/O (wait)", IO_COLOR, curr_x, curr_y, C.leg_s) @@ -508,25 +508,25 @@ def render_charts(ctx, trace, curr_y, w, h): curr_y += 8 + C.bar_h if ctx.app_options.show_legends: - curr_y += 30 + curr_y += 34 # render second chart draw_legend_box(ctx.cr, "Disk utilization -- fraction of sample interval I/O queue was not empty", - IO_COLOR, 0, curr_y, C.leg_s) + IO_COLOR, C.legend_indent+x_onscreen, curr_y, C.leg_s) if ctx.app_options.show_ops_not_bytes: unit = "ops" else: unit = "bytes" draw_legend_line(ctx.cr, "Disk writes -- " + unit + "/sample", - DISK_WRITE_COLOR, 470, curr_y, C.leg_s) + DISK_WRITE_COLOR, C.legend_indent+x_onscreen+470, curr_y, C.leg_s) draw_legend_line(ctx.cr, "Disk reads+writes -- " + unit + "/sample", - DISK_TPUT_COLOR, 470+120*2, curr_y, C.leg_s) + DISK_TPUT_COLOR, C.legend_indent+x_onscreen+470+220, curr_y, C.leg_s) # render disk throughput max_sample = None # render I/O utilization for partition in trace.disk_stats: - draw_text(ctx.cr, partition.name, TEXT_COLOR, 0, curr_y+18) + draw_text(ctx.cr, partition.name, TEXT_COLOR, C.legend_indent+x_onscreen, curr_y+18) # utilization -- inherently normalized [0,1] chart_rect = (0, curr_y+18+5+USER_HALF, w, C.bar_h) @@ -741,21 +741,21 @@ def draw_vertical(ctx, time, x): def draw_process_bar_chart_legends(ctx, curr_y): curr_y += 30 - draw_legend_diamond (ctx.cr, "Runnable", - PROCS_RUNNING_COLOR, 10, curr_y, C.leg_s*3/4, C.proc_h) - draw_legend_diamond (ctx.cr, "Uninterruptible Syscall", - PROC_COLOR_D, 10+100, curr_y, C.leg_s*3/4, C.proc_h) - curr_x = 10+100+40 + curr_x = 10 + C.legend_indent + max(0, ctx.cr.device_to_user(0, 0)[0]) + curr_x += 30 + draw_legend_diamond (ctx.cr, "Runnable", + PROCS_RUNNING_COLOR, curr_x, curr_y, C.leg_s*3/4, C.proc_h) + curr_x += 30 + draw_legend_diamond (ctx.cr, "Uninterruptible Syscall", + PROC_COLOR_D, curr_x, curr_y, C.leg_s*3/4, C.proc_h) curr_x += 20 + draw_legend_box (ctx.cr, "Running (user)", - PROC_COLOR_R, 10+100+curr_x, curr_y, C.leg_s) + PROC_COLOR_R, curr_x, curr_y, C.leg_s) curr_x += 20 + draw_legend_box (ctx.cr, "Running (sys)", - CPU_SYS_COLOR, 10+100+curr_x, curr_y, C.leg_s) + CPU_SYS_COLOR, curr_x, curr_y, C.leg_s) curr_x += 20 + draw_legend_box (ctx.cr, "Child CPU time lost, charged to parent", - CPU_CHILD_COLOR, 10+100+curr_x, curr_y, C.leg_s) + CPU_CHILD_COLOR, curr_x, curr_y, C.leg_s) curr_x += 20 + draw_legend_box (ctx.cr, "Sleeping", - PROC_COLOR_S, 10+100+curr_x, curr_y, C.leg_s) + PROC_COLOR_S, curr_x, curr_y, C.leg_s) curr_x += 20 + draw_legend_box (ctx.cr, "Zombie", - PROC_COLOR_Z, 10+100+curr_x, curr_y, C.leg_s) + PROC_COLOR_Z, curr_x, curr_y, C.leg_s) curr_y -= 9 return curr_y @@ -789,7 +789,8 @@ def draw_header (ctx, headers, duration): cr = ctx.cr header_y = cr.font_extents()[2] + 10 cr.set_font_size(TITLE_FONT_SIZE) - draw_text(cr, headers['title'], TEXT_COLOR, 0, header_y) + x_onscreen = C.legend_indent + max(0, ctx.cr.device_to_user(0, 0)[0]) + draw_text(cr, headers['title'], TEXT_COLOR, x_onscreen, header_y) cr.set_font_size(TEXT_FONT_SIZE) for (headerkey, headertitle, mangle) in toshow: @@ -799,7 +800,7 @@ def draw_header (ctx, headers, duration): else: value = "" txt = headertitle + ': ' + mangle(value) - draw_text(cr, txt, TEXT_COLOR, 0, header_y) + draw_text(cr, txt, TEXT_COLOR, x_onscreen, header_y) # dur = duration / 100.0 # txt = 'time : %02d:%05.2f' % (math.floor(dur/60), dur - 60 * math.floor(dur/60)) @@ -1013,7 +1014,7 @@ def format_label_time(ctx, delta): prec=min(3, max(0, int(ctx.SEC_W/100)))) def print_event_times(ctx, y, ev_list): - ctx.cr.set_source_rgba(*EVENT_COLOR) + ctx.cr.set_source_rgba(*DIM_EVENT_COLOR) width = ctx.cr.text_extents("00")[2] last_x_touched = 0 last_label_str = None @@ -1038,7 +1039,7 @@ def print_event_times(ctx, y, ev_list): last_x_touched = tx + draw_label_on_bg( ctx.cr, TRANSPARENT, - DARK_GREY, label_str, y, tx) + DIM_EVENT_COLOR, label_str, y, tx) last_label_str = label_str def draw_process_events(ctx, proc, proc_tree, x, y): @@ -1073,7 +1074,7 @@ def draw_process_events(ctx, proc, proc_tree, x, y): tx, y+2*C.proc_h-4) n_highlighted_events += 1 else: - ctx.cr.set_source_rgb(*EVENT_COLOR) + ctx.cr.set_source_rgb(*DIM_EVENT_COLOR) W,H = 1,5 # don't dump synthetic events if ctx.event_dump_list != None and ctx.SWEEP_CSEC and ev.raw_log_seek: From bafa56510053afc2dc2066541c46c65012b30b16 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:24 -0800 Subject: [PATCH 160/182] legend spacing touch up2 --- pybootchartgui/draw.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 088803d..b5040d1 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -70,7 +70,7 @@ LEGEND_FONT_SIZE = 12 # CPU load chart color. -CPU_COLOR = (0.60, 0.65, 0.75, 1.0) +CPU_COLOR = (0.60, 0.60, 0.70, 1.0) # CPU system-mode load chart color. CPU_SYS_COLOR = (0.70, 0.65, 0.40, 1.0) # IO wait chart color. @@ -102,7 +102,7 @@ # Running process color. PROC_COLOR_R = CPU_COLOR # (0.40, 0.50, 0.80, 1.0) # should look similar to CPU_COLOR # Sleeping process color. -PROC_COLOR_S = (0.95, 0.95, 0.95, 1.0) +PROC_COLOR_S = (0.95, 0.93, 0.93, 1.0) # Stopped process color. PROC_COLOR_T = (0.94, 0.50, 0.50, 1.0) # Zombie process color. @@ -756,7 +756,7 @@ def draw_process_bar_chart_legends(ctx, curr_y): PROC_COLOR_S, curr_x, curr_y, C.leg_s) curr_x += 20 + draw_legend_box (ctx.cr, "Zombie", PROC_COLOR_Z, curr_x, curr_y, C.leg_s) - curr_y -= 9 + curr_y -= 15 return curr_y def draw_process_bar_chart(ctx, proc_tree, times, curr_y, w, h): From 96ad01b0751958c87056fff08ad4855af5974827 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:24 -0800 Subject: [PATCH 161/182] draw charts on pixel center --- pybootchartgui/draw.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index b5040d1..9cfa535 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -337,8 +337,19 @@ def draw_sec_labels(ctx, rect, nsecs): draw_text(ctx.cr, label, TEXT_COLOR, x, rect[1] - 2) prev_x = x + label_w -def draw_box(ctx, rect): +def draw_box_1(ctx, rect): + """ draws an outline one user-space unit in width around rect. + For best appearance with the default transform (or zoom to an odd multiple), + the corners of rect should be offset by USER_HALF (0.5). + This is a consequence of Cairo's line-drawing model.""" + # XX But zooming currently is in steps of sqrt(2) -- what solution would give + # both pixel-aligned lines, and reasonble zoom steps? + # ... 1/8, 1/6, 1/4, 1/3, 1/2, 1/sqrt(2), 1, sqrt(2), 2, 3, 4, 5, 6, 8, ... + # XX Drawing within the chart should be similarly aligned. + ctx.cr.save() + ctx.cr.set_line_width(1.0) draw_rect(ctx.cr, BORDER_COLOR, tuple(rect)) + ctx.cr.restore() def draw_annotations(ctx, proc_tree, times, rect): ctx.cr.set_line_cap(cairo.LINE_CAP_SQUARE) @@ -387,6 +398,7 @@ def plot_scatter_positive_small(cr, point, x, y): return _plot_scatter_positive(cr, point, x, y, 3.6, 3.6) # All charts assumed to be full-width +# XX horizontal coords in chart_bounds are now on-pixel-center def draw_chart(ctx, color, fill, chart_bounds, data, proc_tree, data_range, plot_point_func): def transform_point_coords(point, y_base, yscale): x = csec_to_xscaled(ctx, point[0]) @@ -474,7 +486,7 @@ def render_charts(ctx, trace, curr_y, w, h): curr_x +70, curr_y, C.leg_s, C.leg_s) chart_rect = (0, curr_y+10+USER_HALF, w, C.bar_h) - draw_box (ctx, chart_rect) + draw_box_1 (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) # render I/O wait -- a backwards delta draw_chart (ctx, IO_COLOR, True, chart_rect, \ @@ -530,7 +542,7 @@ def render_charts(ctx, trace, curr_y, w, h): # utilization -- inherently normalized [0,1] chart_rect = (0, curr_y+18+5+USER_HALF, w, C.bar_h) - draw_box (ctx, chart_rect) + draw_box_1 (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) # a backwards delta draw_chart (ctx, IO_COLOR, True, chart_rect, @@ -578,7 +590,7 @@ def render_charts(ctx, trace, curr_y, w, h): draw_legend_box(ctx.cr, "Buffers", MEM_BUFFERS_COLOR, 360, curr_y, C.leg_s) draw_legend_line(ctx.cr, "Swap (scale: %u MiB)" % max([(sample.records['SwapTotal'] - sample.records['SwapFree'])/1024 for sample in mem_stats]), \ MEM_SWAP_COLOR, 480, curr_y, C.leg_s) - draw_box (ctx, chart_rect) + draw_box_1 (ctx, chart_rect) draw_annotations (ctx, proc_tree, trace.times, chart_rect) draw_chart(ctx, MEM_BUFFERS_COLOR, True, chart_rect, \ [(sample.time, sample.records['MemTotal'] - sample.records['MemFree']) for sample in trace.mem_stats], \ @@ -1276,7 +1288,7 @@ def draw_cuml_graph(ctx, proc_tree, chart_bounds, duration, stat_type): below = row - draw_box (ctx, chart_bounds) + draw_box_1 (ctx, chart_bounds) # render labels for l in labels: From 136a4af0cbfd8803d0d7d1776e595444f971e1e4 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:25 -0800 Subject: [PATCH 162/182] hide header details --- pybootchartgui/draw.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 9cfa535..68f1357 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -797,6 +797,7 @@ def draw_header (ctx, headers, duration): ('system.cpu', 'CPU', lambda s: re.sub('model name\s*:\s*', '', s, 1)), ('system.kernel.options', 'kernel options', lambda s: s), ] + toshow = [] cr = ctx.cr header_y = cr.font_extents()[2] + 10 From 20503b94060196994a32bbcfe4ff6a022aa5b9c4 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 12 Dec 2012 13:26:21 -0800 Subject: [PATCH 163/182] big redacting patch --- pybootchartgui/draw.py | 397 ++++++++++++++++++++---------- pybootchartgui/gui.py | 349 +++++++++++++++++++------- pybootchartgui/main.py.in | 118 +++++---- pybootchartgui/parsing.py | 436 +++++++++++++++++++-------------- pybootchartgui/process_tree.py | 18 +- pybootchartgui/samples.py | 157 ++++++++---- pybootchartgui/writer.py | 38 +++ 7 files changed, 1001 insertions(+), 512 deletions(-) create mode 100644 pybootchartgui/writer.py diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 68f1357..6ceb1c4 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -22,6 +22,9 @@ import collections import traceback # debug +from samples import IOStat, EventSample +from . import writer + # Constants: Put the more heavily used, non-derived constants in a named tuple, for immutability. # XX The syntax is awkward, but more elegant alternatives have run-time overhead. # http://stackoverflow.com/questions/4996815/ways-to-make-a-class-immutable-in-python @@ -46,6 +49,7 @@ NOTEPAD_YELLOW = (0.95, 0.95, 0.8, 1.0) PURPLE = (0.6, 0.1, 0.6, 1.0) RED = (1.0, 0.0, 0.0) +MAGENTA = (0.7, 0.0, 0.7, 1.0) # Process tree border color. BORDER_COLOR = (0.63, 0.63, 0.63, 1.0) @@ -74,16 +78,14 @@ # CPU system-mode load chart color. CPU_SYS_COLOR = (0.70, 0.65, 0.40, 1.0) # IO wait chart color. -IO_COLOR = (0.76, 0.48, 0.48, 0.5) +IO_COLOR = (0.88, 0.74, 0.74, 1.0) PROCS_RUNNING_COLOR = (0.0, 1.0, 0.0, 1.0) PROCS_BLOCKED_COLOR = (0.7, 0.0, 0.0, 1.0) -# Disk throughput color. -DISK_TPUT_COLOR = (0.20, 0.71, 0.20, 1.0) -# Disk throughput color. -MAGENTA = (0.7, 0.0, 0.7, 1.0) +DISK_READ_COLOR = (0.20, 0.71, 0.20, 1.0) DISK_WRITE_COLOR = MAGENTA + # Mem cached color MEM_CACHED_COLOR = CPU_COLOR # Mem used color @@ -91,7 +93,7 @@ # Buffers color MEM_BUFFERS_COLOR = (0.4, 0.4, 0.4, 0.3) # Swap color -MEM_SWAP_COLOR = DISK_TPUT_COLOR +MEM_SWAP_COLOR = (0.20, 0.71, 0.20, 1.0) # Process CPU load of children -- including those waited for by the parent, but not captured by any collector sample CPU_CHILD_COLOR = (1.00, 0.70, 0.00, 1.0) @@ -238,6 +240,9 @@ def __init__(self, app_options, trace, cumulative = True, charts = True, kernel_ self.charts = charts self.kernel_only = kernel_only # set iff collector daemon saved output of `dmesg` self.SWEEP_CSEC = None + + self.ps_event_lists_valid = False + self.event_dump_list = None self.trace = trace @@ -245,23 +250,76 @@ def __init__(self, app_options, trace, cumulative = True, charts = True, kernel_ self.time_origin_drawn = None # time of leftmost plotted data, as integer csecs self.SEC_W = None self.time_origin_relative = None # currently used only to locate events - self.highlight_event__match_RE = [] # intra-rendering state self.hide_process_y = None self.unhide_process_y = None self.proc_above_was_hidden = False - def per_render_init(self, cr, time_origin_drawn, SEC_W, sweep_csec): + def _validate_event_state(self, ctx, trace): + def copy_if_enabled(color_list, re_index): + return [ec.color_regex[re_index] for ec in color_list if ec.enable] + + self.event_RE = copy_if_enabled( self.app_options.event_color, 0) + self.event_interval_0_RE = copy_if_enabled( self.app_options.event_interval_color, 0) + self.event_interval_1_RE = copy_if_enabled( self.app_options.event_interval_color, 1) + + if trace.ps_threads_stats: + ps_s = trace.ps_threads_stats + key_fn = lambda ev: ev.tid * 1000 + else: + ps_s = trace.ps_stats + key_fn = lambda ev: ev.pid * 1000 + + # Copy events selected by currently enabled EventSources to per-process lists + for proc in ps_s.process_map.values(): + proc.events = [] + for ep in filter(lambda ep: ep.enable, ctx.app_options.event_source): + for ev in ep.parsed: + key = key_fn(ev) + if key in ps_s.process_map: + ps_s.process_map[key].events.append(ev) + else: + writer.warn("no samples of /proc/%d/task/%d/stat found -- event lost:\n\t%s" % + (ev.pid, ev.tid, ev.raw_log_line)) + + # Strip out from per-process lists any events not selected by a regexp. + for proc in ps_s.process_map.values(): + enabled_events = [] + for ev in proc.events: + # Separate attrs, because they may be drawn differently + ev.event_match_any = None + ev.event_match_0 = None + ev.event_match_1 = None + def _set_attr_on_match(RE_list, event_match_attr): + # Only LAST matching regexp contributes to the label string -- + # this allows user to append overriding regexes to the command line. + m = None + for ev_re in RE_list: + m = re.search(ev_re, ev.raw_log_line) + if m: + setattr(ev, event_match_attr, m) + return getattr(ev, event_match_attr) is not None + _set_attr_on_match(ctx.event_RE + ctx.event_interval_0_RE + ctx.event_interval_1_RE, + "event_match_any") + _set_attr_on_match(ctx.event_interval_0_RE, "event_match_0") + _set_attr_on_match(ctx.event_interval_1_RE, "event_match_1") + enabled_events.append(ev) + enabled_events.sort(key = lambda ev: ev.time_usec) + proc.events = enabled_events + proc.draw |= len(proc.events) > 0 + self.ps_event_lists_valid = True + + def per_render_init(self, cr, ctx, trace, time_origin_drawn, SEC_W, sweep_csec): self.cr = cr self.time_origin_drawn = time_origin_drawn self.SEC_W = SEC_W self.n_WIDTH = cr.text_extents("n")[2] self.M_HEIGHT = cr.text_extents("M")[3] - self.highlight_event__match_RE = [] - for ev_regex in self.app_options.event_regex: - self.highlight_event__match_RE.append( re.compile(ev_regex)) + # merge in enabled event sources + if not self.ps_event_lists_valid: + self._validate_event_state(ctx, trace) self.SWEEP_CSEC = sweep_csec if self.SWEEP_CSEC: @@ -378,13 +436,23 @@ def plot_square(cr, point, x, y): cr.line_to(x, y) # rightward # backward-looking -def plot_segment_positive(cr, point, x, y): +def plot_segment(cr, point, x, y, segment_tick_height): cr.move_to(cr.get_current_point()[0], y) # upward or downward if point[1] <= 0: # zero-Y samples draw nothing cr.move_to(x, y) return - cr.set_line_width(1.0) - cr.line_to(x, y) + #cr.set_line_width(1.0) + cr.line_to(x, y-segment_tick_height/2) + cr.rel_line_to(0, segment_tick_height) + cr.fill() + cr.move_to(x, y) + +SEGMENT_TICK_HEIGHT = 2 +def plot_segment_thin(cr, point, x, y): + plot_segment(cr, point, x, y, SEGMENT_TICK_HEIGHT) + +def plot_segment_fat(cr, point, x, y): + plot_segment(cr, point, x, y, 1.5*SEGMENT_TICK_HEIGHT) def _plot_scatter_positive(cr, point, x, y, w, h): if point[1] <= 0: @@ -472,6 +540,11 @@ def render_charts(ctx, trace, curr_y, w, h): proc_tree = ctx.proc_tree(trace) x_onscreen = max(0, ctx.cr.device_to_user(0, 0)[0]) + max_procs_blocked = \ + max([sample.procs_blocked for sample in trace.cpu_stats]) + max_procs_running = \ + max([sample.procs_running for sample in trace.cpu_stats]) + if ctx.app_options.show_legends: # render bar legend ctx.cr.set_font_size(LEGEND_FONT_SIZE) @@ -480,10 +553,10 @@ def render_charts(ctx, trace, curr_y, w, h): curr_x += 20 + draw_legend_box(ctx.cr, "CPU (user)", CPU_COLOR, curr_x, curr_y, C.leg_s) curr_x += 20 + draw_legend_box(ctx.cr, "CPU (sys)", CPU_SYS_COLOR, curr_x, curr_y, C.leg_s) curr_x += 20 + draw_legend_box(ctx.cr, "I/O (wait)", IO_COLOR, curr_x, curr_y, C.leg_s) - curr_x += draw_legend_diamond(ctx.cr, "Runnable threads", PROCS_RUNNING_COLOR, - curr_x +10, curr_y, C.leg_s, C.leg_s) - curr_x += draw_legend_diamond(ctx.cr, "Blocked threads -- Uninterruptible Syscall", PROCS_BLOCKED_COLOR, - curr_x +70, curr_y, C.leg_s, C.leg_s) + curr_x += draw_legend_diamond(ctx.cr, str(max_procs_running) + " Runnable threads", + PROCS_RUNNING_COLOR, curr_x +10, curr_y, C.leg_s, C.leg_s) + curr_x += draw_legend_diamond(ctx.cr, str(max_procs_blocked) + " Blocked threads -- Uninterruptible Syscall", + PROCS_BLOCKED_COLOR, curr_x +70, curr_y, C.leg_s, C.leg_s) chart_rect = (0, curr_y+10+USER_HALF, w, C.bar_h) draw_box_1 (ctx, chart_rect) @@ -510,35 +583,70 @@ def render_charts(ctx, trace, curr_y, w, h): # instantaneous sample draw_chart (ctx, PROCS_BLOCKED_COLOR, False, chart_rect, [(sample.time, sample.procs_blocked) for sample in trace.cpu_stats], \ - proc_tree, [0, 9], plot_scatter_positive_big) + proc_tree, [0, max(max_procs_blocked, max_procs_running)], plot_scatter_positive_big) # instantaneous sample draw_chart (ctx, PROCS_RUNNING_COLOR, False, chart_rect, [(sample.time, sample.procs_running) for sample in trace.cpu_stats], \ - proc_tree, [0, 9], plot_scatter_positive_small) + proc_tree, [0, max(max_procs_blocked, max_procs_running)], plot_scatter_positive_small) curr_y += 8 + C.bar_h + # XXX Assume single device for now. + # XX Generate an IOStat containing max'es of all stats, instead? + max_whole_device_IOStat = IOStat._make( + getattr( max( trace.disk_stats[0].part_deltas, + key = lambda part_delta: getattr(part_delta.s.iostat, f)).s.iostat, + f) + for f in IOStat._fields) + max_whole_device_util = \ + max( trace.disk_stats[0].part_deltas, + key = lambda part_delta: part_delta.util).util + + if ctx.app_options.show_ops_not_bytes: + read_field = 'nreads' + write_field = 'nwrites' + else: + read_field = 'nsectors_read' + write_field = 'nsectors_write' + + max_whole_device_read_or_write = max(getattr(max_whole_device_IOStat, write_field), + getattr(max_whole_device_IOStat, read_field)) + if ctx.app_options.show_legends: curr_y += 34 + curr_x = C.legend_indent+x_onscreen # render second chart - draw_legend_box(ctx.cr, "Disk utilization -- fraction of sample interval I/O queue was not empty", - IO_COLOR, C.legend_indent+x_onscreen, curr_y, C.leg_s) + draw_legend_box(ctx.cr, + "Disk utilization -- fraction of sample interval I/O queue was not empty", + IO_COLOR, curr_x, curr_y, C.leg_s) + curr_x += 457 + + def draw_RW_legend(x, field, color, plot): + label = str(getattr(max_whole_device_IOStat, field)) + " " + field + cr = ctx.cr + cr.set_source_rgba(*color) + cr.move_to(x, -1) + PLOT_WIDTH = 30 + plot(cr, [0,1], x+PLOT_WIDTH, curr_y-4) + x += PLOT_WIDTH+5 + x += draw_text(cr, label, TEXT_COLOR, x, curr_y) + return x + + curr_x = 10 + draw_RW_legend(curr_x, + read_field, DISK_READ_COLOR, plot_segment_thin) + curr_x = draw_RW_legend(curr_x, + write_field, DISK_WRITE_COLOR, plot_segment_fat) if ctx.app_options.show_ops_not_bytes: - unit = "ops" - else: - unit = "bytes" - draw_legend_line(ctx.cr, "Disk writes -- " + unit + "/sample", - DISK_WRITE_COLOR, C.legend_indent+x_onscreen+470, curr_y, C.leg_s) - draw_legend_line(ctx.cr, "Disk reads+writes -- " + unit + "/sample", - DISK_TPUT_COLOR, C.legend_indent+x_onscreen+470+220, curr_y, C.leg_s) - - # render disk throughput - max_sample = None + curr_x = draw_RW_legend(curr_x, + "nio_in_progress", BLACK, plot_scatter_positive_big) # render I/O utilization + # No correction for non-constant sample.time -- but see sample-coalescing code in parsing.py. for partition in trace.disk_stats: - draw_text(ctx.cr, partition.name, TEXT_COLOR, C.legend_indent+x_onscreen, curr_y+18) + if partition.hide: + continue + draw_text(ctx.cr, partition.label, TEXT_COLOR, C.legend_indent+x_onscreen, curr_y+18) # utilization -- inherently normalized [0,1] chart_rect = (0, curr_y+18+5+USER_HALF, w, C.bar_h) @@ -546,35 +654,28 @@ def render_charts(ctx, trace, curr_y, w, h): draw_annotations (ctx, proc_tree, trace.times, chart_rect) # a backwards delta draw_chart (ctx, IO_COLOR, True, chart_rect, - [(sample.time, sample.util) for sample in partition.samples], - proc_tree, [0, 1], plot_square) + [(sample.s.time, sample.util) for sample in partition.part_deltas], + proc_tree, [0, max_whole_device_util], plot_square) - # render disk throughput - # XXX assume single block device, for now - if not max_sample: - # XXX correction for non-constant sample.time? - max_sample = max (partition.samples, key = lambda s: s.tput) + # write throughput -- a backwards delta + draw_chart (ctx, DISK_WRITE_COLOR, False, chart_rect, + [(sample.s.time, getattr(sample.s.iostat, write_field)) for sample in partition.part_deltas], + proc_tree, [0, max_whole_device_read_or_write], plot_segment_fat) - # a backwards delta - draw_chart (ctx, DISK_TPUT_COLOR, False, chart_rect, - [(sample.time, sample.tput) for sample in partition.samples], - proc_tree, [0, max_sample.tput], plot_segment_positive) + # overlay read throughput -- any overlapping read will be protrude around the edges + draw_chart (ctx, DISK_READ_COLOR, False, chart_rect, + [(sample.s.time, getattr(sample.s.iostat, read_field)) for sample in partition.part_deltas], + proc_tree, [0, max_whole_device_read_or_write], plot_segment_thin) - # overlay write throughput - # a backwards delta - draw_chart (ctx, DISK_WRITE_COLOR, False, chart_rect, - [(sample.time, sample.write) for sample in partition.samples], - proc_tree, [0, max_sample.tput], plot_segment_positive) - - # pos_x = ((max_sample.time - proc_tree.start_time) * w / proc_tree.duration()) - # - # shift_x, shift_y = -20, 20 - # if (pos_x < 245): - # shift_x, shift_y = 5, 40 - # - # DISK_BLOCK_SIZE = 1024 - # label = "%.1fMB/s" % round ((max_sample.tput) / DISK_BLOCK_SIZE) - # draw_text (ctx, label, DISK_TPUT_COLOR, pos_x + shift_x, curr_y + shift_y) + # overlay instantaneous count of number of I/O operations in progress, only if comparable to + # the read/write stats currently shown (ops). + if ctx.app_options.show_ops_not_bytes: + draw_chart (ctx, BLACK, False, chart_rect, + [(sample.s.time, sample.nio_in_progress) for sample in partition.part_deltas], + proc_tree, + [0, max(max_whole_device_read_or_write, + max_whole_device_IOStat.nio_in_progress)], + plot_scatter_positive_small) curr_y += 18+C.bar_h @@ -624,7 +725,7 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): "ctx" is a DrawContext object. ''' #traceback.print_stack() - ctx.per_render_init(cr, _time_origin_drawn(ctx, trace), _sec_w(xscale), sweep_csec) + ctx.per_render_init(cr, ctx, trace, _time_origin_drawn(ctx, trace), _sec_w(xscale), sweep_csec) (w, h) = extents(ctx, xscale, trace) ctx.cr.set_line_width(1.0) @@ -700,15 +801,16 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): ctx.event_dump_list.sort(key = lambda e: e.raw_log_seek) if len(ctx.event_dump_list): event0 = ctx.event_dump_list[0] - event0.raw_log_file.seek(event0.raw_log_seek) - eventN = ctx.event_dump_list[-1] - #for line in event0.raw_log_file.readline(): - while event0.raw_log_file.tell() <= eventN.raw_log_seek: - print event0.raw_log_file.readline(), - else: # dump events only + if event0.raw_log_seek: + event0.raw_log_file.seek(event0.raw_log_seek) + eventN = ctx.event_dump_list[-1] + # for line in event0.raw_log_file.readline(): + while event0.raw_log_file.tell() <= eventN.raw_log_seek: + print event0.raw_log_file.readline().rstrip() + else: # dump only digestible events ctx.event_dump_list.sort(key = lambda ev: ev.time_usec) for ev in ctx.event_dump_list: - print ev.time_usec, ".", ev.raw_log_line(), + print ev.dump_format() ctx.event_dump_list = None @@ -762,6 +864,8 @@ def draw_process_bar_chart_legends(ctx, curr_y): PROC_COLOR_R, curr_x, curr_y, C.leg_s) curr_x += 20 + draw_legend_box (ctx.cr, "Running (sys)", CPU_SYS_COLOR, curr_x, curr_y, C.leg_s) + curr_x += 20 + draw_legend_box (ctx.cr, "I/O wait", + IO_COLOR, curr_x, curr_y, C.leg_s) curr_x += 20 + draw_legend_box (ctx.cr, "Child CPU time lost, charged to parent", CPU_CHILD_COLOR, curr_x, curr_y, C.leg_s) curr_x += 20 + draw_legend_box (ctx.cr, "Sleeping", @@ -972,36 +1076,47 @@ def draw_process_activity_colors(ctx, proc, proc_tree, x, y, w): if cpu_exited_child != 0: print "cpu_exited_child == " + str(cpu_exited_child) - if cpu_self > 1.0: - print "process CPU time overflow: ", proc.tid, sample.time, width, cpu_self - OVERFLOW_BAR_HEIGHT=2 - draw_fill_rect(ctx.cr, PURPLE, (last_time, y+SEP_HALF, width, OVERFLOW_BAR_HEIGHT)) - cpu_self = 1.0 - float(OVERFLOW_BAR_HEIGHT)/C.proc_h - if cpu_self > 0: - height = cpu_self * BAR_HEIGHT - draw_fill_rect(ctx.cr, PROC_COLOR_R, (last_time, y+C.proc_h-SEP_HALF, width, -height)) - - # in unlikely event of no sys time at all, skip setting of color to CPU_SYS_COLOR - if sample.cpu_sample.sys > 0: - height = sample.cpu_sample.sys * BAR_HEIGHT - draw_fill_rect(ctx.cr, CPU_SYS_COLOR, (last_time, y+C.proc_h-SEP_HALF, width, -height)) - if sample.cpu_sample.sys < cpu_self: - # draw a separator between the bar segments, to aid the eye in - # resolving the boundary - ctx.cr.save() - ctx.cr.move_to(last_time, y+C.proc_h-SEP_HALF-height) - ctx.cr.rel_line_to(width,0) - ctx.cr.set_source_rgba(*PROC_COLOR_S) - ctx.cr.set_line_width(DEP_STROKE/2) - ctx.cr.stroke() - ctx.cr.restore() - - # If thread ran at all, draw a "speed bump", in case rect was too short to resolve. + # XX For whole processes, the cpu_sample.io stat is the sum of waits for all threads. + + # Inspection of kernel code shows that tick counters are rounded to nearest, + # so overflow is to be expected. + OVERFLOW_LIMIT=1.0 + # XX What's the upper bound on rounding-induced overflow? + if sample.cpu_sample.io + cpu_self > 0: + height = min(OVERFLOW_LIMIT, (sample.cpu_sample.io + cpu_self)) * BAR_HEIGHT + draw_fill_rect(ctx.cr, IO_COLOR, (last_time, y+C.proc_h-SEP_HALF, width, -height)) + + for (cpu_field, color) in [(sample.cpu_sample.user + sample.cpu_sample.sys, PROC_COLOR_R), + (sample.cpu_sample.sys, CPU_SYS_COLOR)]: + # If this test fails -- no time ticks -- then skip changing of color. + if cpu_field > 0: + height = min(OVERFLOW_LIMIT, cpu_field) * BAR_HEIGHT + draw_fill_rect(ctx.cr, color, (last_time, y+C.proc_h-SEP_HALF, width, -height)) + if cpu_field < cpu_self: + # draw a separator between the bar segments, to aid the eye in + # resolving the boundary + ctx.cr.save() + ctx.cr.move_to(last_time, y+C.proc_h-SEP_HALF-height) + ctx.cr.rel_line_to(width,0) + ctx.cr.set_source_rgba(*PROC_COLOR_S) + ctx.cr.set_line_width(DEP_STROKE/2) + ctx.cr.stroke() + ctx.cr.restore() + + # If thread ran at all, draw a "speed bump", in the last used color, to help the user + # with rects that are too short to resolve. tick_height = C.proc_h/5 ctx.cr.arc((last_time + sample.time)/2, y+C.proc_h-SEP_HALF, tick_height, math.pi, 0.0) ctx.cr.close_path() ctx.cr.fill() + if cpu_self > 1.0: + writer.info("process CPU+I/O time overflow: time {0:5d}, start_time {1:5d}, tid {2:5d}, width {3:2d}, cpu_self {4: >5.2f}".format( + sample.time, proc.start_time, proc.tid/1000, width, cpu_self)) + OVERFLOW_BAR_HEIGHT=2 + draw_fill_rect(ctx.cr, PURPLE, (last_time, y+SEP_HALF, width, OVERFLOW_BAR_HEIGHT)) + cpu_self = 1.0 - float(OVERFLOW_BAR_HEIGHT)/C.proc_h + last_time = sample.time ctx.cr.restore() @@ -1009,8 +1124,11 @@ def usec_to_csec(usec): '''would drop precision without the float() cast''' return float(usec) / 1000 / 10 +def csec_to_usec(csec): + return csec * 1000 * 10 + def draw_event_label(ctx, label, tx, y): - draw_label_at_time(ctx.cr, HIGHLIGHT_EVENT_COLOR, label, y, tx) + return draw_label_at_time(ctx.cr, HIGHLIGHT_EVENT_COLOR, label, y, tx) def format_label_time(ctx, delta): if ctx.SWEEP_CSEC: @@ -1027,23 +1145,21 @@ def format_label_time(ctx, delta): prec=min(3, max(0, int(ctx.SEC_W/100)))) def print_event_times(ctx, y, ev_list): - ctx.cr.set_source_rgba(*DIM_EVENT_COLOR) - width = ctx.cr.text_extents("00")[2] last_x_touched = 0 last_label_str = None for (ev, tx) in ev_list: - for ev_re in ctx.highlight_event__match_RE: - m = re.search(ev_re, ev.match) - if m: - break; - if not m and tx < last_x_touched + width: + if not ctx.app_options.synthesize_sample_start_events and ev.raw_log_line == "pseudo-raw_log_line": continue - delta= float(ev.time_usec)/1000/10 - ctx.time_origin_relative + m_highlight = ev.event_match_any + delta = usec_to_csec(ev.time_usec) - ctx.time_origin_relative label_str = format_label_time(ctx, delta) - if m or label_str != last_label_str: - if m: - # freely step on top of any time labels drawn earlier + white_space = 8 + if tx < last_x_touched + white_space: + continue + + if m_highlight or label_str != last_label_str: + if m_highlight: last_x_touched = tx + draw_label_on_bg( ctx.cr, WHITE, @@ -1056,11 +1172,13 @@ def print_event_times(ctx, y, ev_list): last_label_str = label_str def draw_process_events(ctx, proc, proc_tree, x, y): + ctx.cr.save() + ctx.cr.set_line_width(0) + n_highlighted_events = 0 + last_tx_plus_width_drawn = 0 ev_list = [(ev, csec_to_xscaled(ctx, usec_to_csec(ev.time_usec))) for ev in proc.events] - if not ev_list: - return n_highlighted_events # draw numbers if ctx.app_options.print_event_times: @@ -1068,39 +1186,68 @@ def draw_process_events(ctx, proc, proc_tree, x, y): # draw ticks, maybe add to dump list for (ev, tx) in ev_list: - # FIXME: Optimize by doing re.search() per-regexp change, rather than per-rendering? - for ev_re in ctx.highlight_event__match_RE: - m = re.search(ev_re, ev.match) - if m: - break; - if m: + if not ctx.app_options.synthesize_sample_start_events and ev.raw_log_line == "pseudo-raw_log_line": + continue + last_m = ev.event_match_any + if last_m: ctx.cr.set_source_rgb(*HIGHLIGHT_EVENT_COLOR) - W,H = 2,8 - if m.lastindex: + W = 2 + if last_m.lastindex: groups_concat = "" - for g in m.groups(): + for g in last_m.groups(): groups_concat += str(g) else: - groups_concat = m.group(0) - draw_event_label(ctx, - groups_concat, - tx, y+2*C.proc_h-4) + groups_concat = last_m.group(0) + if last_tx_plus_width_drawn < tx: + # draw bottom half of tick mark + tick_depth = 10 + ctx.cr.move_to(tx, y +C.proc_h +tick_depth) # bottom + + ctx.cr.rel_line_to(-W, -tick_depth -0.5) # top-left + ctx.cr.rel_line_to(2*W, 0) # top-right + ctx.cr.close_path() + ctx.cr.fill() + + clear = 1.0 # clearance between down-tick and string + last_tx_plus_width_drawn = \ + tx + clear + \ + draw_event_label(ctx, + groups_concat, + tx + clear, y+2*C.proc_h-4) n_highlighted_events += 1 else: ctx.cr.set_source_rgb(*DIM_EVENT_COLOR) - W,H = 1,5 - # don't dump synthetic events - if ctx.event_dump_list != None and ctx.SWEEP_CSEC and ev.raw_log_seek: - ev_time_csec = float(ev.time_usec)/1000/10 + W = 1 + + # If an interval bar should start at tx, record the time. + last_m = ev.event_match_0 + if last_m: + proc.event_interval_0_tx = tx + + # Draw interval bar that terminates at tx, if any. + if proc.event_interval_0_tx != None: + last_m = ev.event_match_1 + if last_m: + ctx.cr.rectangle(proc.event_interval_0_tx, y+C.proc_h+0.5, + tx-proc.event_interval_0_tx, 1.5) + ctx.cr.fill() + proc.event_interval_0_tx = None + + if ctx.event_dump_list != None and ctx.SWEEP_CSEC \ + and ev.raw_log_file: # don't dump synthetic events + ev_time_csec = usec_to_csec(ev.time_usec) if ev_time_csec >= ctx.SWEEP_CSEC[0] and ev_time_csec < ctx.SWEEP_CSEC[1]: ctx.event_dump_list.append(ev) - ctx.cr.move_to(tx, y+C.proc_h-6) # top - ctx.cr.rel_line_to(-W,H) # bottom-left + # draw top half of tick mark + ctx.cr.move_to(tx, y+C.proc_h-6.5) # top + + ctx.cr.rel_line_to(-W,6) # bottom-left ctx.cr.rel_line_to(2*W,0) # bottom-right ctx.cr.close_path() ctx.cr.fill() + ctx.cr.restore() return n_highlighted_events def draw_process_state_colors(ctx, proc, proc_tree, x, y, w): diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index 2734a48..0e7ee41 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -161,28 +161,54 @@ def on_zoom_100(self, action): self.zoom_image(1.0) # XX replace with: self.zoom_ratio = 1.0 \ self.x = 0 \ self.y = 0 self.set_xscale(1.0) - def show_legends(self, button): - self.drawctx.app_options.show_legends = button.get_property ('active') + def absolute_uptime_event_times(self, action, current): + self.drawctx.app_options.absolute_uptime_event_times = action.get_current_value() self.queue_draw() - def show_thread_details(self, button): - self.drawctx.app_options.show_all = button.get_property ('active') + def show_IO_ops(self, action, current): + self.drawctx.app_options.show_ops_not_bytes = action.get_current_value() self.queue_draw() - def hide_events(self, button): - self.drawctx.app_options.hide_events = not button.get_property ('active') + def show_thread_details(self, action): + self.drawctx.app_options.show_all = action.get_active() self.queue_draw() - def print_event_times(self, button): - self.drawctx.app_options.print_event_times = button.get_property ('active') + def hide_events(self, action): + self.drawctx.app_options.hide_events = not action.get_active() self.queue_draw() - def absolute_uptime_event_times(self, button): - self.drawctx.app_options.absolute_uptime_event_times = button.get_property ('active') + def print_event_times(self, action): + self.drawctx.app_options.print_event_times = action.get_active() self.queue_draw() - def dump_raw_event_context(self, button): - self.drawctx.app_options.dump_raw_event_context = button.get_property ('active') + def event_source_toggle(self, action): + # turn second char of the string into an int + self.drawctx.app_options.event_source[int(action.get_name()[1])].enable = action.get_active() + self.drawctx.ps_event_lists_valid = False + self.queue_draw() + + def _toggle_EventColor_by_label(self, ecl, action): + for ec in ecl: + if ec.label == action.get_name(): + break + ec.enable = action.get_active() + self.drawctx.ps_event_lists_valid = False + self.queue_draw() + + def event_toggle(self, action): + self._toggle_EventColor_by_label( + self.drawctx.app_options.event_color, action) + + def event_interval_toggle(self, action): + self._toggle_EventColor_by_label( + self.drawctx.app_options.event_interval_color, action) + + def dump_raw_event_context(self, action): + self.drawctx.app_options.dump_raw_event_context = action.get_active() + self.queue_draw() + + def show_legends(self, action): + self.drawctx.app_options.show_legends = action.get_active() self.queue_draw() POS_INCREMENT = 100 @@ -202,7 +228,7 @@ def on_key_press_event(self, widget, event): self.y += self.POS_INCREMENT/self.zoom_ratio else: return False - self.queue_draw() + #self.queue_draw() self.position_changed() return True @@ -219,7 +245,7 @@ def on_area_button_press(self, area, event): elif event.button == 3: if not self.sweep_csec: self.sweep_csec = [self.device_to_csec_user_y(event.x, 0)[0], - self.device_to_csec_user_y(self.trace.ps_stats.end_time, 0)[0]] + self.device_to_csec_user_y(self.trace.end_time, 0)[0]] else: self.sweep_csec = None self.queue_draw() @@ -238,7 +264,7 @@ def on_area_button_release(self, area, event): self.queue_draw() elif event.button == 3: if self.sweep_csec and \ - self.sweep_csec[1] != self.device_to_csec_user_y(self.trace.ps_stats.end_time, 0)[0]: + self.sweep_csec[1] != self.device_to_csec_user_y(self.trace.end_time, 0)[0]: self.drawctx.event_dump_list = [] # XX self.queue_draw() return True @@ -268,13 +294,12 @@ def on_area_motion_notify(self, area, event): # pan the image self.x += (self.prevmousex - x)/self.zoom_ratio self.y += (self.prevmousey - y)/self.zoom_ratio - self.queue_draw() self.prevmousex = x self.prevmousey = y self.position_changed() elif state & gtk.gdk.BUTTON3_MASK and self.sweep_csec: self.sweep_csec[1] = self.device_to_csec_user_y(event.x, 0)[0] - self.queue_draw() + #self.queue_draw() return True def on_set_scroll_adjustments(self, area, hadj, vadj): @@ -340,8 +365,16 @@ def on_position_changed(self, widget, x, y): PyBootchartWidget.set_set_scroll_adjustments_signal('set-scroll-adjustments') class PyBootchartShell(gtk.VBox): - ui = ''' - + def __init__(self, window, trace, drawctx, xscale): + gtk.VBox.__init__(self) + + self.widget = PyBootchartWidget(trace, drawctx, xscale) + + # Create a UIManager instance + uimanager = self.uimanager = gtk.UIManager() + + uimanager.add_ui_from_string(''' + @@ -351,87 +384,209 @@ class PyBootchartShell(gtk.VBox): - - ''' - def __init__(self, window, trace, drawctx, xscale): - gtk.VBox.__init__(self) - - self.widget = PyBootchartWidget(trace, drawctx, xscale) - - # Create a UIManager instance - uimanager = self.uimanager = gtk.UIManager() - - # Add the accelerator group to the toplevel window - accelgroup = uimanager.get_accel_group() - window.add_accel_group(accelgroup) + + + + + + + + + + + + ''') - # Create an ActionGroup actiongroup = gtk.ActionGroup('Actions') - self.actiongroup = actiongroup - # Create actions + # Zooming buttons actiongroup.add_actions(( - ('Expand', gtk.STOCK_ORIENTATION_LANDSCAPE, None, None, None, self.widget.on_expand), - ('Contract', gtk.STOCK_ORIENTATION_PORTRAIT, None, None, None, self.widget.on_contract), - ('ZoomIn', gtk.STOCK_ZOOM_IN, None, None, None, self.widget.on_zoom_in), - ('ZoomOut', gtk.STOCK_ZOOM_OUT, None, None, None, self.widget.on_zoom_out), - ('ZoomFit', gtk.STOCK_ZOOM_FIT, 'Fit Width', None, None, self.widget.on_zoom_fit), - ('Zoom100', gtk.STOCK_ZOOM_100, None, None, None, self.widget.on_zoom_100), + ('Expand', gtk.STOCK_ORIENTATION_LANDSCAPE, None, None, "widen", self.widget.on_expand), + ('Contract', gtk.STOCK_ORIENTATION_PORTRAIT, None, None, "narrow", self.widget.on_contract), + ('ZoomIn', gtk.STOCK_ZOOM_IN, None, None, "zoom in", self.widget.on_zoom_in), + ('ZoomOut', gtk.STOCK_ZOOM_OUT, None, None, "zoom out", self.widget.on_zoom_out), + ('ZoomFit', gtk.STOCK_ZOOM_FIT, 'Fit Width', None, "zoom-to-fit while preserving aspect ratio", self.widget.on_zoom_fit), + ('Zoom100', gtk.STOCK_ZOOM_100, None, None, "zoom to best image quality", self.widget.on_zoom_100), )) - # Add the actiongroup to the uimanager - uimanager.insert_action_group(actiongroup, 0) + # "Time" drop-down menu + actiongroup.add_radio_actions([ + ("bootchart daemon start", None, "bootchart daemon start -- startup of the collector daemon on the target", None, None, False), + ("system boot", None, "system boot i.e. 'uptime'", None, None, True), + ], 1 if drawctx.app_options.absolute_uptime_event_times else 0, self.widget.absolute_uptime_event_times + ) + actiongroup.add_actions(( + ("Time", None, "Time", None, "XXX why does this not show up??? time-origin selection for display"), + )) - # Add a UI description - uimanager.add_ui_from_string(self.ui) + # I/O dropdown + actiongroup.add_radio_actions([ + ("hardware operations", None, "hardware operations", None, None, True), + ("512-byte sectors", None, "512-byte sectors", None, None, False), + ], drawctx.app_options.show_ops_not_bytes, self.widget.show_IO_ops + ) + actiongroup.add_actions(( + ("I/O", None, "I/O", None, ""), + )) - # Scrolled window - scrolled = gtk.ScrolledWindow() - scrolled.add(self.widget) + # Threads dropdown + if not drawctx.kernel_only: + uimanager.add_ui_from_string(''' + + + + + + + + ''') + actiongroup.add_toggle_actions([ + ('details', None, "details", None, None, self.widget.show_thread_details, drawctx.app_options.show_all), + ]) + actiongroup.add_actions([ + ("Threads", None, "Threads", None, ""), + ]) + + + # Events dropdown + ui_Events = ''' + + + + + + + + + ''' + actiongroup.add_toggle_actions([ + ('show', None, "show", None, None, self.widget.hide_events, not drawctx.app_options.hide_events), + ('times', None, "show times", None, None, self.widget.print_event_times, drawctx.app_options.print_event_times), + ]) + uimanager.add_ui_from_string(ui_Events) + actiongroup.add_actions([ + ("Events", None, "Events", None, ""), + ]) + + + # Event Source dropdown + ui_Event_Source = ''' + + + + ''' + def add_es(index, es, callback): + action_name = "p{0:d}".format(index) # callback will extract a list index from this name string + # XX Supports 10 logs, max + actiongroup.add_toggle_actions([ + (action_name, None, + "{0:s} ({1:d})".format(es.label, len(es.parsed)), + None, None, + getattr(self.widget, callback), es.enable), + ]) + return ''' + + '''.format(action_name) + + for index, es in enumerate(drawctx.app_options.event_source): + ui_Event_Source += add_es(index, es, "event_source_toggle") + ui_Event_Source += ''' + + + + ''' + uimanager.add_ui_from_string(ui_Event_Source) + actiongroup.add_actions([ + ("Event_Source", None, "Ev-Sources", None, ""), + ]) + + + # Event Color dropdown + ui_Event_Color = ''' + + + + ''' + def add_re(ec, callback_name): + # XX add_toggle_actions() can take a "user_data" arg -- but how is the value + # retrieved later? + actiongroup.add_toggle_actions([ + (ec.label, None, ec.label, None, None, + getattr(self.widget, callback_name), ec.enable), + ]) + return ''' + + '''.format(ec.label) + for ec in drawctx.app_options.event_color: + ui_Event_Color += add_re(ec, "event_toggle") + ui_Event_Color += '''''' + + for ec in drawctx.app_options.event_interval_color: + ui_Event_Color += add_re(ec, "event_interval_toggle") + ui_Event_Color += ''' + + + + ''' + uimanager.add_ui_from_string(ui_Event_Color) + actiongroup.add_actions([ + ("Event_Color", None, "Ev-Color", None, ""), + ]) + + # Stdout, Help dropdowns + uimanager.add_ui_from_string( ''' + + + + + + + + + + + ''') + # Stdout dropdown + actiongroup.add_toggle_actions([ + ('dump raw', None, "dump raw context lines from log along with events", None, None, + self.widget.dump_raw_event_context, drawctx.app_options.dump_raw_event_context), + ]) + actiongroup.add_actions([ + ("Stdout", None, "Stdout", None, ""), + ]) + + # Stdout dropdown + actiongroup.add_toggle_actions([ + ('show legends', None, "show legends", None, None, + self.widget.show_legends, drawctx.app_options.show_legends), + ]) + actiongroup.add_actions([ + ("Help", None, "Help", None, ""), + ]) + + uimanager.insert_action_group(actiongroup, 0) # toolbar / h-box - hbox = gtk.HBox(False, 8) + hbox = gtk.HBox(False, 0) # Create a Toolbar toolbar = uimanager.get_widget('/ToolBar') - hbox.pack_start(toolbar, True, True) - - def gtk_CheckButton(name): - button = gtk.CheckButton(name) - button.set_focus_on_click(False) - return button + hbox.pack_start(toolbar, True) - button = gtk_CheckButton("show Legends") - button.connect ('toggled', self.widget.show_legends) - hbox.pack_start (button, False) + menubar = uimanager.get_widget("/MenuBar") + hbox.pack_start(menubar, True) - if not drawctx.kernel_only: - # Misc. drawctx - button = gtk_CheckButton("thread details") - button.connect ('toggled', self.widget.show_thread_details) - hbox.pack_start (button, False) - - button = gtk_CheckButton("Events") - button.connect ('toggled', self.widget.hide_events) - button.set_active (True) - hbox.pack_start (button, False) - - button = gtk_CheckButton("event Time Labels") - button.connect ('toggled', self.widget.print_event_times) - button.set_active (drawctx.app_options.print_event_times) - hbox.pack_start (button, False) - - button = gtk_CheckButton("event Times Absolute") - button.connect ('toggled', self.widget.absolute_uptime_event_times) - button.set_active (drawctx.app_options.absolute_uptime_event_times) - hbox.pack_start (button, False) - - button = gtk_CheckButton("dump raw event context") - button.connect ('toggled', self.widget.dump_raw_event_context) - button.set_active (drawctx.app_options.dump_raw_event_context) - hbox.pack_start (button, False) + # force all the real widgets to the left + # XX Why doesn't this force the others all the way to the left? + empty_menubar = gtk.MenuBar() + hbox.pack_start(empty_menubar, True, True) self.pack_start(hbox, False) + + # Scrolled window + scrolled = gtk.ScrolledWindow() + scrolled.add(self.widget) + self.pack_start(scrolled) self.show_all() @@ -450,18 +605,26 @@ def __init__(self, app_options, trace): window.set_default_size(screen.get_width() * 95/100, screen.get_height() * 95/100) - tab_page = gtk.Notebook() - tab_page.show() - window.add(tab_page) - full_drawctx = DrawContext(app_options, trace) - full_tree = PyBootchartShell(window, trace, full_drawctx, 1.0) - tab_page.append_page (full_tree, gtk.Label("Full tree")) + full_tree = PyBootchartShell(window, trace, full_drawctx, + # XX "1.7" is a hack + float(window.get_default_size()[0]) * 1.7 / \ + ((trace.end_time - trace.start_time) + \ + 2 * trace.ps_stats.sample_period)) + # FIXME: Permanently disable top-level tabs? + if True: + window.add(full_tree) + else: + tab_page = gtk.Notebook() + tab_page.show() + window.add(tab_page) + + tab_page.append_page (full_tree, gtk.Label("Full tree")) - if trace.kernel is not None and len (trace.kernel) > 2: - kernel_drawctx = DrawContext(app_options, trace, cumulative = False, charts = False, kernel_only = True) - kernel_tree = PyBootchartShell(window, trace, kernel_drawctx, 5.0) - tab_page.append_page (kernel_tree, gtk.Label("Kernel boot")) + if trace.kernel is not None and len (trace.kernel) > 2: + kernel_drawctx = DrawContext(app_options, trace, cumulative = False, charts = False, kernel_only = True) + kernel_tree = PyBootchartShell(window, trace, kernel_drawctx, 5.0) + tab_page.append_page (kernel_tree, gtk.Label("Kernel boot")) full_tree.grab_focus(self) self.show() diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 2f28618..3980fb9 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -21,12 +21,15 @@ from __future__ import print_function import sys import os import optparse +import string from copy import copy from optparse import Option, OptionValueError from . import parsing from . import batch +from . import writer +from .samples import EventSource, EventColor def _mk_options_parser(): """Make an options parser.""" @@ -37,19 +40,20 @@ def _mk_options_parser(): epilog=None) parser.add_option("--batch", action="store_false", dest="interactive", default=True, help="print a bootchart image, rather than starting the interactive GUI") - parser.add_option("--merge", dest="merge", default=False, - help="Prune the process tree of all activity from dull children: " + - "stats are added to parent, child process is lost in drawing of chart.") - parser.add_option("-H", "--hide-low-CPU", dest="hide_low_CPU", type="int", default=-1, metavar="CSEC", - help="Hide any thread consuming less than CSEC of CPU time." + + parser.add_option("--absolute-uptime-event-times", action="store_true", default=False, + dest="absolute_uptime_event_times", + help="event times shown are relative to system boot, rather than beginning of sampling") + parser.add_option("-H", "--hide-low-CPU", dest="hide_low_CPU", type="int", default=0, metavar="CSEC", + help="Hide any thread consuming less than CSEC of CPU time, unless it otherwise shows itself to be of interest." + " Hiding means the thread is not shown on initial rendering, but can be shown by a mouse click." + - " A value of zero may be specified, meaning the special case of threads that report 0 CSECs" - "consumed, but are known to have executed due to having been born, died, or reported some" - " state other than 'S' when sampled by the collector.") + " A value of 0 may be specified, meaning the special case of showing threads that report 0 CSECs" + + " consumed, but are known to have executed due to having been born, died, or reported some" + + " state other than 'S' when sampled by the collector." + + " A negative value causes all threads to be shown.") parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="suppress informational messages") - parser.add_option("--verbose", action="store_true", dest="verbose", default=False, - help="print all messages") + parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0, + help="Print debugging messages. Issuing twice will print yet more messages.") parser.add_option("--show-legends", action="store_true", dest="show_legends", default=False, help="show legend lines with keys to chart symbols") @@ -68,18 +72,38 @@ def _mk_options_parser(): help="relocate the text within process bars (left, center)") # event plotting - parser.add_option("-e", "--events", action="append", dest="event_regex", metavar="REGEX", default=["^$"], - help="Highlight events matching REGEX." + - " Syntax is similar to grep 'extended' REs." + + parser.set_defaults(event_source = []) + parser.add_option("--event-source", + action="callback", callback=handle_event_source_option, type="string", nargs=3, + help="Tell pybootchartgui which file in the tarball contains event records," + + "and how to extract the four essential fields from it." + + " A 3-tuple of (label,filename,regex) does the job, e.g.:" + + " --event-source usec-format log-file-in-tarball.txt 'usec_bits=(?P[^,]+),\Wpid=(?P[^,]+),\Wtid=(?P[^,]+),\Wcomm=(?P[^,]+)'" + + "N.B.: The '_' character is unusable in labels, as the PyGTK toolkit silently discards it.") + + parser.set_defaults(event_color = []) + parser.add_option("-e", "--events", nargs=2, type="string", + action="callback", callback=handle_event_color, + metavar="REGEX", help="Highlight events matching REGEX." + + " 2-tuple of (label-string, REGEX)." + + " Regex syntax is similar to grep 'extended' REs." + " To match FOO or BAR anywhere on the log line, use 'FOO|BAR'." + - " (file:///usr/share/doc/python2.6/html/library/re.htm)") + " (file:///usr/share/doc/python/html/library/re.htm)") + parser.set_defaults(event_interval_color = []) + parser.add_option("--events-interval", nargs=3, type="string", + action="callback", callback=handle_event_interval_color, + metavar="REGEX", help="3-tuple of (label-string, REGEX_start, REGEX_end)." + + "In addition to highlighting the events matching the two REGEX arguments, " + + " highlight the interval of time between them.") + parser.add_option('-d', "--events-disable", + action="callback", callback=handle_events_disable, type="string", nargs=1, + help="Disable event-parsing regex matching (label) -- its checkbox on the Ev-Color menu will be cleared." + + " Must appear after the option that specified (label).") + parser.add_option("--hide-events", action="store_true", dest="hide_events", default=False, help="hide event ticks (small black triangles)") parser.add_option("--no-print-event-times", action="store_false", default=True, dest="print_event_times", help="suppress printing time of each event, inside the box of the reporting process") - parser.add_option("--absolute-uptime-event-times", action="store_true", default=False, - dest="absolute_uptime_event_times", - help="event times shown are relative to system boot, rather than beginning of sampling") parser.add_option("--dump-raw-event-context", action="store_true", dest="dump_raw_event_context", default=False, help="in addition to log lines for selected events, dump log lines as well," + @@ -87,8 +111,8 @@ def _mk_options_parser(): parser.add_option("--no-synthesize-sample-start-events", action="store_false", default=True, dest="synthesize_sample_start_events", - help="synthesize an event marking each boundary between sample periods -- " + - "helpful in analyzing collector timing issues") + help="disable synthesis of an event marking each boundary between sample periods." + + "These are helpful in analyzing collector timing issues.") pg_Bootchart2 = optparse.OptionGroup(parser,"Bootchart2-specific", "Options effective only for logs coming from the Bootchart2 binary-format collector") @@ -99,6 +123,9 @@ def _mk_options_parser(): pg_Scripting = optparse.OptionGroup(parser,"Scripting support", "Options most useful in scripted processing of tgz batches") + pg_Scripting.add_option("--merge", dest="merge", default=False, + help="Prune the process tree of all activity from dull children: " + + "stats are added to parent, child process is lost in drawing of chart.") pg_Scripting.add_option("--prehistory", action="store_true", dest="prehistory", default=False, help="extend process bars to the recorded start time of each, even if before any samples were collected") pg_Scripting.add_option("-t", "--boot-time", action="store_true", dest="boottime", default=False, @@ -121,31 +148,6 @@ def _mk_options_parser(): parser.add_option_group(pg_Scripting) return parser -class Writer: - def __init__(self, write, options): - self.write = write - self.options = options - - def error(self, msg): - self.write(msg) - - def warn(self, msg): - if not self.options.quiet: - self.write(msg) - - def info(self, msg): - if self.options.verbose: - self.write(msg) - - def status(self, msg): - if not self.options.quiet: - self.write(msg) - -def _mk_writer(options): - def write(s): - sys.stderr.write(s + "\n") - return Writer(write, options) - def _get_filename(paths, options): """Construct a usable filename for outputs based on the paths and options given on the commandline.""" dname = "" @@ -168,6 +170,28 @@ def _get_filename(paths, options): fname = os.path.split(fname)[-1] return os.path.join (dname, fname + "." + options.format) +def handle_event_source_option(option, opt_str, value, parser): + # XX superfluous: note that pdb `print value[1]` will double-print '\' characters + # fix = string.replace(value[1], r"\\", "\x5c") + parser.values.event_source.append( + EventSource(value[0], value[1], value[2])) + +def handle_event_color(option, opt_str, value, parser): + ec = EventColor(value[0], value[1], None, True) + parser.values.event_color.append(ec) + +def handle_event_interval_color(option, opt_str, value, parser): + ec = EventColor(value[0], value[1], value[2], True) + parser.values.event_interval_color.append(ec) + +def handle_events_disable(option, opt_str, value, parser): + for ec in parser.values.event_color + parser.values.event_interval_color: + if ec.label == value: + ec.enable = False + return + raise parsing.ParseError("Event-Color label {1:s} not found\n\t{0:s}".format( + opt_str, value)) + def main(argv=None): try: if argv is None: @@ -175,12 +199,12 @@ def main(argv=None): parser = _mk_options_parser() options, args = parser.parse_args(argv) - writer = _mk_writer(options) + writer.init(options) if len(args) == 0: raise parsing.ParseError("\n\tNo path to a bootchart.tgz found on command line") - res = parsing.Trace(writer, args, options) + res = parsing.Trace(args, options) if options.interactive: from . import gui @@ -208,7 +232,7 @@ def main(argv=None): f.close() filename = _get_filename(args, options) def render(): - batch.render(writer, res, options, filename) + batch.render(res, options, filename) if options.profile: import cProfile import pstats diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 7e3f6ea..c37706f 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -20,24 +20,28 @@ import string import re import tarfile +import struct from time import clock +import collections from collections import defaultdict from functools import reduce from types import * +from .draw import usec_to_csec, csec_to_usec from .samples import * from .process_tree import ProcessTree +from . import writer # Parsing produces as its end result a 'Trace' class Trace: - def __init__(self, writer, paths, options): + def __init__(self, paths, options): self.headers = None self.disk_stats = None self.ps_stats = None + self.ps_threads_stats = None self.taskstats = None - self.cpu_stats = None - self.events = None + self.cpu_stats = None # from /proc/stat self.cmdline = None self.kernel = None self.kernel_tree = None @@ -46,18 +50,23 @@ def __init__(self, writer, paths, options): self.mem_stats = None # Read in all files, parse each into a time-ordered list - parse_paths (writer, self, paths, options) + parse_paths (self, paths, options) + + # support deprecated data sets that contain no proc_ps.log, only proc_ps_threads.log + if not self.ps_stats: + self.ps_stats = self.ps_threads_stats + if not self.valid(): raise ParseError("empty state: '%s' does not contain a valid bootchart" % ", ".join(paths)) # Turn that parsed information into something more useful # link processes into a tree of pointers, calculate statistics - self.adorn_process_map(writer, options) + self.adorn_process_map(options) # Crop the chart to the end of the first idle period after the given # process if options.crop_after: - idle = self.crop (writer, options.crop_after) + idle = self.crop (options.crop_after) else: idle = None @@ -73,22 +82,37 @@ def __init__(self, writer, paths, options): else: self.times.append(None) - self.proc_tree = ProcessTree(writer, self.kernel, self.ps_stats, + self.proc_tree = ProcessTree(self.kernel, self.ps_stats, self.ps_stats.sample_period, self.headers.get("profile.process"), options, idle, self.taskstats, self.parent_map is not None) if self.kernel is not None: - self.kernel_tree = ProcessTree(writer, self.kernel, None, 0, + self.kernel_tree = ProcessTree(self.kernel, None, 0, self.headers.get("profile.process"), options, None, None, True) + self._generate_sample_start_pseudo_events(options) + + def _generate_sample_start_pseudo_events(self, options): + es = EventSource(" ... sample start points", "", "") # empty regex + es.parsed = [] + es.enable = True + init_pid = 1 + for cpu in self.cpu_stats: + # assign to the init process's bar, for lack of any better + ev = EventSample(csec_to_usec(cpu.time), + init_pid, init_pid, + "comm", None, None, "") + es.parsed.append(ev) + options.event_source.append(es) + def valid(self): return self.headers != None and self.disk_stats != None and \ self.ps_stats != None and self.cpu_stats != None - def adorn_process_map(self, writer, options): + def adorn_process_map(self, options): def find_parent_id_for(pid): if pid is 0: @@ -119,28 +143,6 @@ def find_parent_id_for(pid): # else: # print "proc %d '%s' not in cmdline" % (rpid, proc.exe) - if options.synthesize_sample_start_events: - init_pid = 1 - key = init_pid * 1000 - proc = self.ps_stats.process_map[key] - for cpu in self.cpu_stats: - # assign to the init process's bar, for lack of any better - ev = EventSample(cpu.time, cpu.time*10*1000, init_pid, init_pid, - "comm", "match", None, None) - proc.events.append(ev) - - # merge in events - if self.events is not None: - for ev in self.events: - if ev.time > self.ps_stats.end_time: - continue - key = int(ev.tid) * 1000 - if key in self.ps_stats.process_map: - self.ps_stats.process_map[key].events.append(ev) - else: - writer.warn("no samples of /proc/%d/task/%d/proc found -- event lost:\n\t%s" % - (ev.pid, ev.tid, ev.raw_log_line())) - # re-parent any stray orphans if we can if self.parent_map is not None: # requires either "kernel_pacct" or "paternity.log" for process in self.ps_stats.process_map.values(): @@ -157,7 +159,7 @@ def find_parent_id_for(pid): for process in self.ps_stats.process_map.values(): process.calc_stats (self.ps_stats.sample_period) - def crop(self, writer, crop_after): + def crop(self, crop_after): def is_idle_at(util, start, j): k = j + 1 @@ -216,7 +218,7 @@ def is_idle(util, start): and self.disk_stats[-1].time > crop_at: self.disk_stats.pop() - self.ps_stats.end_time = crop_at + self.end_time = crop_at cropped_map = {} for key, value in self.ps_stats.process_map.items(): @@ -277,8 +279,9 @@ def parse(block): # 2. run continuing # 2.1 thread continues (tid in processMap) # 2.2 thread starts -def _handle_sample(processMap, writer, ltime, time, - pid, tid, cmd, state, ppid, userCpu, sysCpu, c_user, c_sys, starttime): +def _handle_sample(processMap, ltime, time, + pid, tid, cmd, state, ppid, userCpu, sysCpu, delayacct_blkio_ticks, c_user, c_sys, starttime, + num_cpus): assert(type(c_user) is IntType) assert(type(c_sys) is IntType) @@ -291,25 +294,32 @@ def _handle_sample(processMap, writer, ltime, time, writer.status("time (%dcs) < starttime (%dcs), diff %d -- TID %d" % (time, starttime, time-starttime, tid/1000)) - process = Process(writer, pid, tid, cmd.strip('()'), ppid, starttime) + process = Process(pid, tid, cmd.strip('()'), ppid, starttime) if ltime: # process is starting during profiling run process.user_cpu_ticks[0] = 0 process.sys_cpu_ticks [0] = 0 + process.delayacct_blkio_ticks [0] = 0 ltime = starttime else: process.user_cpu_ticks[0] = userCpu process.sys_cpu_ticks [0] = sysCpu + process.delayacct_blkio_ticks [0] = delayacct_blkio_ticks ltime = -100000 # XX hacky way of forcing reported load toward zero + process.user_cpu_ticks[-1] = process.user_cpu_ticks[0] + process.sys_cpu_ticks [-1] = process.sys_cpu_ticks [0] + process.delayacct_blkio_ticks[-1] = process.delayacct_blkio_ticks[0] processMap[tid] = process # insert new process into the dict - userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) + userCpuLoad, sysCpuLoad, delayacctBlkioLoad = process.calc_load(userCpu, sysCpu, delayacct_blkio_ticks, + max(1, time - ltime), num_cpus) - cpuSample = ProcessCPUSample('null', userCpuLoad, sysCpuLoad, c_user, c_sys, 0.0, 0.0) + cpuSample = ProcessCPUSample('null', userCpuLoad, sysCpuLoad, c_user, c_sys, delayacctBlkioLoad, 0.0) process.samples.append(ProcessSample(time, state, cpuSample)) # per-tid store for use by a later phase of parsing of samples gathered at this 'time' process.user_cpu_ticks[-1] = userCpu process.sys_cpu_ticks[-1] = sysCpu + process.delayacct_blkio_ticks[-1] = delayacct_blkio_ticks return processMap # Consider this case: a process P and three waited-for children C1, C2, C3. @@ -386,7 +396,17 @@ def total_c_ticks(sample): print "compute_lost_child_times()", time, p_p.pid/1000, \ parent_c_tick_delta, p_p.missing_child_ticks, lost_child_ticks, interval #, p_p.samples[-1].lost_child -def _parse_proc_ps_log(options, writer, file): +def _distribute_belatedly_reported_delayacct_blkio_ticks(processMap): + for p in processMap.itervalues(): + io_acc = 0.0 + for s in p.samples[-1:0:-1]: + s.cpu_sample.io += io_acc + io_acc = 0.0 + if s.cpu_sample.io + s.cpu_sample.user + s.cpu_sample.sys > 1.0: + io_acc = s.cpu_sample.io + s.cpu_sample.user + s.cpu_sample.sys - 1.0 + s.cpu_sample.io -= io_acc + +def _parse_proc_ps_log(options, file, num_cpus): """ * See proc(5) for details. * @@ -409,13 +429,15 @@ def _parse_proc_ps_log(options, writer, file): userCpu, sysCpu = int(tokens[13+offset]), int(tokens[14+offset]), c_user, c_sys = int(tokens[15+offset]), int(tokens[16+offset]) starttime = int(tokens[21+offset]) + delayacct_blkio_ticks = int(tokens[41+offset]) # magic fixed point-ness ... pid *= 1000 ppid *= 1000 - processMap = _handle_sample(processMap, writer, ltime, time, + processMap = _handle_sample(processMap, ltime, time, pid, pid, cmd, state, ppid, - userCpu, sysCpu, c_user, c_sys, starttime) + userCpu, sysCpu, delayacct_blkio_ticks, c_user, c_sys, starttime, + num_cpus) if ltime: accumulate_missing_child_ltime(processMap, ltime) # compute_lost_child_times(processMap, ltime, time) @@ -427,9 +449,11 @@ def _parse_proc_ps_log(options, writer, file): startTime = timed_blocks[0][0] avgSampleLength = (ltime - startTime)/(len (timed_blocks) - 1) - return ProcessStats (writer, processMap, len (timed_blocks), avgSampleLength, startTime, ltime) + _distribute_belatedly_reported_delayacct_blkio_ticks(processMap) -def _parse_proc_ps_threads_log(options, writer, file): + return ProcessStats (processMap, len (timed_blocks), avgSampleLength) + +def _parse_proc_ps_threads_log(options, file): """ * 0* pid -- inserted here from value in /proc/*pid*/task/. Not to be found in /proc/*pid*/task/*tid*/stat. * Not the same as pgrp, session, or tpgid. Refer to collector daemon source code for details. @@ -449,6 +473,7 @@ def _parse_proc_ps_threads_log(options, writer, file): * 14 current_EIP_instruction_pointer * 15 wchan * 16 scheduling_policy + * 17* delayacct_blkio_ticks -- in proc_ps_threads-2.log only, requires CONFIG_TASK_DELAY_ACCT """ processMap = {} ltime = None @@ -465,6 +490,8 @@ def _parse_proc_ps_threads_log(options, writer, file): pid, tid, cmd, state, ppid = int(tokens[0]), int(tokens[1]), ' '.join(tokens[2:3+offset]), tokens[3+offset], int(tokens[4+offset]) userCpu, sysCpu = int(tokens[7+offset]), int(tokens[8+offset]) c_user, c_sys = int(tokens[9+offset]), int(tokens[10+offset]) + delayacct_blkio_ticks = int(tokens[17+offset]) if len(tokens) == 18+offset else 0 + assert(type(c_user) is IntType) assert(type(c_sys) is IntType) starttime = int(tokens[13+offset]) @@ -474,9 +501,10 @@ def _parse_proc_ps_threads_log(options, writer, file): tid *= 1000 ppid *= 1000 - processMap = _handle_sample(processMap, writer, ltime, time, + processMap = _handle_sample(processMap, ltime, time, pid, tid, cmd, state, ppid, - userCpu, sysCpu, c_user, c_sys, starttime) + userCpu, sysCpu, delayacct_blkio_ticks, c_user, c_sys, starttime, + 1) ltime = time if len (timed_blocks) < 2: @@ -485,9 +513,11 @@ def _parse_proc_ps_threads_log(options, writer, file): startTime = timed_blocks[0][0] avgSampleLength = (ltime - startTime)/(len (timed_blocks) - 1) - return ProcessStats (writer, processMap, len (timed_blocks), avgSampleLength, startTime, ltime) + _distribute_belatedly_reported_delayacct_blkio_ticks(processMap) + + return ProcessStats (processMap, len (timed_blocks), avgSampleLength) -def _parse_taskstats_log(writer, file): +def _parse_taskstats_log(file): """ * See bootchart-collector.c for details. * @@ -501,7 +531,7 @@ def _parse_taskstats_log(writer, file): for time, lines in timed_blocks: # we have no 'starttime' from taskstats, so prep 'init' if ltime is None: - process = Process(writer, 1, '[init]', 0, 0) + process = Process(1, '[init]', 0, 0) processMap[1000] = process ltime = time # continue @@ -529,12 +559,12 @@ def _parse_taskstats_log(writer, file): pid += 1 pidRewrites[opid] = pid # print "process mutation ! '%s' vs '%s' pid %s -> pid %s\n" % (process.cmd, cmd, opid, pid) - process = process.split (writer, pid, cmd, ppid, time) + process = process.split (pid, cmd, ppid, time) processMap[pid] = process else: process.cmd = cmd; else: - process = Process(writer, pid, cmd, ppid, time) + process = Process(pid, pid, cmd, ppid, time) processMap[pid] = process delta_cpu_ns = (float) (cpu_ns - process.last_cpu_ns) @@ -570,7 +600,18 @@ def _parse_taskstats_log(writer, file): startTime = timed_blocks[0][0] avgSampleLength = (ltime - startTime)/(len(timed_blocks)-1) - return ProcessStats (writer, processMap, len (timed_blocks), avgSampleLength, startTime, ltime) + return ProcessStats (processMap, len (timed_blocks), avgSampleLength) + +def get_num_cpus(file): + """Get the number of CPUs from the ps_stats file.""" + num_cpus = -1 + for time, lines in _parse_timed_blocks(file): + for l in lines: + if l.split(' ')[0] == "intr": + file.seek(0) + return num_cpus + num_cpus += 1 + assert(False) def _parse_proc_stat_log(file): samples = [] @@ -604,92 +645,65 @@ def _parse_proc_stat_log(file): ltimes = times return samples +# matched not against whole line, but field only +# XX Rework to be not a whitelist but rather a blacklist of uninteresting devices e.g. "loop*" +part_name_re = re.compile('^([hsv]d.|mtdblock\d|mmcblk\d.*|cciss/c\d+d\d+.*)$') + def _parse_proc_disk_stat_log(file, options, numCpu): """ - Parse file for disk stats, summing over all physical storage devices, eg. sda, sdb. - Also parse stats for individual devices or partitions indicated on the command line. - The format of relevant lines should be: - {major minor name rio rmerge rsect ruse wio wmerge wsect wuse running io_ticks aveq} - The file is generated by block/genhd.c - FIXME: for Flash devices, rio/wio may have more usefulness than rsect/wsect. - """ + Input file is organized: + [(time, [(major, minor, partition_name, iostats[])])] + Output form, as required for drawing: + [(partition_name, [(time, iostat_deltas[])])] - def delta_disk_samples(disk_stat_samples, numCpu): - disk_stats = [] + Finally, produce pseudo-partition sample sets containing sums + over each physical storage device, e.g. sda, mmcblk0. - # Very short intervals amplify round-off under division by time delta, so coalesce now. - # XX scaling issue for high-efficiency collector! - disk_stat_samples_coalesced = [(disk_stat_samples[0])] - for sample in disk_stat_samples: - if sample.time - disk_stat_samples_coalesced[-1].time < 5: - continue - disk_stat_samples_coalesced.append(sample) - - for sample1, sample2 in zip(disk_stat_samples_coalesced[:-1], disk_stat_samples_coalesced[1:]): - interval = sample2.time - sample1.time - vector_diff = [ a - b for a, b in zip(sample2.diskdata, sample1.diskdata) ] - readTput = float( vector_diff[0]) / interval - writeTput = float( vector_diff[1]) / interval - util = float( vector_diff[2]) / 10 / interval / numCpu - disk_stats.append(DiskSample(sample2.time, readTput, writeTput, util)) - return disk_stats - - def get_relevant_tokens(lines, regex): - return [ - linetokens - for linetokens in map (lambda x: x.split(),lines) - if len(linetokens) == 14 and regex.match(linetokens[2]) - ] - - def add_tokens_to_sample(sample, tokens): - if options.show_ops_not_bytes: - disk_name, rop, wop, io_ticks = tokens[2], int(tokens[3]), int(tokens[7]), int(tokens[12]) - sample.add_diskdata([rop, wop, io_ticks]) - else: - disk_name, rsect, wsect, io_ticks = tokens[2], int(tokens[3]), int(tokens[7]), int(tokens[12]) - sample.add_diskdata([rsect, wsect, io_ticks]) - return disk_name + The input file was generated by block/genhd.c . + """ - # matched not against whole line, but field only - disk_regex_re = re.compile('^([hsv]d.|mtdblock\d|mmcblk\d|cciss/c\d+d\d+.*)$') + strip_slash_dev_slash = re.compile("/dev/(.*)$") +# this_partition_regex_re = re.compile('^' + part + '.*$') - disk_stat_samples = [] + parts_dict = {} + # for every timed_block, collect per-part lists of samples in 'parts_dict' for time, lines in _parse_timed_blocks(file): - sample = DiskStatSample(time) - relevant_tokens = get_relevant_tokens(lines, disk_regex_re) - - for tokens in relevant_tokens: - add_tokens_to_sample(sample,tokens) - - disk_stat_samples.append(sample) - - partition_samples = [DiskSamples("Sum over all disks", - delta_disk_samples(disk_stat_samples, numCpu))] + for line in lines: + def line_to_tokens(line): + linetokens = line.split() + return linetokens if len(linetokens) == 14 else None - strip_slash_dev_slash = re.compile("/dev/(.*)$") - if options.partitions: - for part in options.partitions: - file.seek(0) - disk_stat_samples = [] - this_partition_regex_re = re.compile('^' + part + '.*$') - disk_name = '' - - # for every timed_block - disk_stat_samples = [] - for time, lines in _parse_timed_blocks(file): - sample = DiskStatSample(time) - relevant_tokens = get_relevant_tokens(lines, this_partition_regex_re) - if relevant_tokens: # XX should exit with usage message - disk_name = add_tokens_to_sample(sample,relevant_tokens[0]) # [0] assumes 'part' matched at most a single line - disk_stat_samples.append(sample) - - if options.partition_labels: - disk_name = options.partition_labels[0] + tokenized_partition = line_to_tokens(line) + if not tokenized_partition: + continue + sample = PartitionSample( time, IOStat_make(tokenized_partition)) + if not part_name_re.match(sample.iostat.name): + continue + if not sample.iostat.name in parts_dict: + parts_dict[sample.iostat.name] = [] + parts_dict[sample.iostat.name].append(sample) + + # take deltas, discard original (cumulative) samples + partitions = [] + WHOLE_DEV = 0 + for partSamples in parts_dict.iteritems(): + partitions.append( PartitionDeltas( + partSamples[1], numCpu, + partSamples[0], + partSamples[0] # possibly to be replaced with partition_labels from command line + )) + partitions.sort(key = lambda p: p.name) + partitions[WHOLE_DEV].hide = False # whole device + + if len(options.partitions) > 0: + for opt_name in options.partitions: + for part in partitions: + if part.name == opt_name: + part.hide = False + part.label = options.partition_labels[0] options.partition_labels = options.partition_labels[1:] - partition_samples.append(DiskSamples(disk_name, - delta_disk_samples(disk_stat_samples, numCpu))) - return partition_samples + return partitions def _parse_proc_meminfo_log(file): """ @@ -727,13 +741,13 @@ def _parse_proc_meminfo_log(file): # ... # [ 0.039993] calling migration_init+0x0/0x6b @ 1 # [ 0.039993] initcall migration_init+0x0/0x6b returned 1 after 0 usecs -def _parse_dmesg(writer, file): +def _parse_dmesg(file): timestamp_re = re.compile ("^\[\s*(\d+\.\d+)\s*]\s+(.*)$") split_re = re.compile ("^(\S+)\s+([\S\+_-]+) (.*)$") processMap = {} idx = 0 inc = 1.0 / 1000000 - kernel = Process(writer, idx, "k-boot", 0, 0.1) + kernel = Process(idx, idx, "k-boot", 0, 0.1) processMap['k-boot'] = kernel base_ts = False max_ts = 0 @@ -780,7 +794,7 @@ def _parse_dmesg(writer, file): # print "match: '%s' ('%g') at '%s'" % (func, ppid, time_ms) name = func.split ('+', 1) [0] idx += inc - processMap[func] = Process(writer, ppid + idx, name, ppid, time_ms / 10) + processMap[func] = Process(ppid + idx, ppid + idx, name, ppid, time_ms / 10) elif type == "initcall": # print "finished: '%s' at '%s'" % (func, time_ms) if func in processMap: @@ -794,39 +808,72 @@ def _parse_dmesg(writer, file): return processMap.values() -def _parse_events_log(writer, tf, file): +def get_boot_relative_usec(state, boot_time_as_usecs_since_epoch, time_usec): + boot_relative_usec = time_usec - boot_time_as_usecs_since_epoch + if boot_relative_usec < csec_to_usec(state.start_time): + return None + if boot_relative_usec > csec_to_usec(state.end_time): + return None + return boot_relative_usec + +def parse_raw_log(state, boot_time_as_usecs_since_epoch, log_file, fields_re): ''' - Parse a generic log format produced by target-specific filters. - Extracting the standard fields from the target-specific raw_file - is the responsibility of target-specific pre-processors. + Parse variously-formatted logs containing recorded events, as guided by a + set of target-specific regexps provided on the command line. Eventual output is per-process lists of events in temporal order. ''' - split_re = re.compile ("^(\S+) +(\S+) +(\S+) +(\S+) +(\S+) +(\S+) +(\S+)$") - timed_blocks = _parse_timed_blocks(file) + fields_re_c = re.compile(fields_re) + samples = [] - for time, lines in timed_blocks: - for line in lines: + for line in log_file: if line is '': continue - m = split_re.match(line) - if m == None or m.lastindex < 7: # XX Ignore bad data from Java events, for now + m = fields_re_c.search(line) + if m == None: continue - time_usec = long(m.group(1)) - pid = int(m.group(2)) - tid = int(m.group(3)) - comm = m.group(4) - match = m.group(5) - raw_log_filename = m.group(6) - raw_log_seek = int(m.group(7)) - samples.append( EventSample(time, time_usec, pid, tid, comm, match, - tf.extractfile(raw_log_filename), raw_log_seek)) + + time_usec = float(m.group('CLOCK_REALTIME_usec')) # See `man 3 clock_gettime` + # tolerate any loss of precision in the timestamp, by rounding down + # FIXME: Don't simply round down -- show the user the (min,max) interval + # corresponding to the low-precision number. + while time_usec < 1300*1000*1000*1000*1000: + time_usec *= 10.0 + while time_usec > 1300*1000*1000*1000*1000 * 2: + time_usec /= 10.0 + + try: + pid = int(m.group('pid')) + except IndexError: + # "inherited" by parent's per-thread/process bar + pid = 1 + + try: + tid = int(m.group('tid')) + except IndexError: + # "inherited" by parent's per-thread/process bar + tid = pid + + try: + comm = m.group('comm') + except IndexError: + comm = "" + + raw_log_seek = log_file.tell() + + boot_relative_usec = get_boot_relative_usec( + state, boot_time_as_usecs_since_epoch, time_usec) + if boot_relative_usec: + samples.append( EventSample( + boot_relative_usec, + pid, tid, comm, + log_file, raw_log_seek, line)) return samples # # Parse binary pacct accounting file output if we have one # cf. /usr/include/linux/acct.h # -def _parse_pacct(writer, file): +def _parse_pacct(file): # read LE int32 def _read_le_int32(file): byts = file.read(4) @@ -850,7 +897,7 @@ def _read_le_int32(file): file.seek (16, 1) # acct_comm return parent_map -def _parse_paternity_log(writer, file): +def _parse_paternity_log(file): parent_map = {} parent_map[0] = 0 for line in file.read().split('\n'): @@ -862,7 +909,7 @@ def _parse_paternity_log(writer, file): print("Odd paternity line '%s'" % (line)) return parent_map -def _parse_cmdline_log(writer, file): +def _parse_cmdline_log(file): cmdLines = {} for block in file.read().split('\n\n'): lines = block.split('\n') @@ -877,62 +924,60 @@ def _parse_cmdline_log(writer, file): cmdLines[pid] = values return cmdLines -def get_num_cpus(headers): - """Get the number of CPUs from the system.cpu header property. As the - CPU utilization graphs are relative, the number of CPUs currently makes - no difference.""" - if headers is None: - return 1 - if headers.get("system.cpu.num"): - return max (int (headers.get("system.cpu.num")), 1) - cpu_model = headers.get("system.cpu") - if cpu_model is None: - return 1 - mat = re.match(".*\\((\\d+)\\)", cpu_model) - if mat is None: - return 1 - return max (int(mat.group(1)), 1) - -def _do_parse(writer, state, tf, name, file, options): +def _do_parse(state, tf, name, file, options): writer.status("parsing '%s'" % name) t1 = clock() if name == "header": state.headers = _parse_headers(file) elif name == "proc_diskstats.log": - state.disk_stats = _parse_proc_disk_stat_log(file, options, get_num_cpus(state.headers)) + state.disk_stats = _parse_proc_disk_stat_log(file, options, state.num_cpus) elif name == "taskstats.log": - state.ps_stats = _parse_taskstats_log(writer, file) + state.ps_stats = _parse_taskstats_log(file) state.taskstats = True elif name == "proc_stat.log": + state.num_cpus = get_num_cpus(file) state.cpu_stats = _parse_proc_stat_log(file) + state.start_time = state.cpu_stats[0].time + state.end_time = state.cpu_stats[-1].time elif name == "proc_meminfo.log": state.mem_stats = _parse_proc_meminfo_log(file) elif name == "dmesg": - state.kernel = _parse_dmesg(writer, file) + state.kernel = _parse_dmesg(file) elif name == "cmdline2.log": - state.cmdline = _parse_cmdline_log(writer, file) + state.cmdline = _parse_cmdline_log(file) elif name == "paternity.log": - state.parent_map = _parse_paternity_log(writer, file) + state.parent_map = _parse_paternity_log(file) elif name == "proc_ps.log": # obsoleted by TASKSTATS - state.ps_stats = _parse_proc_ps_log(options, writer, file) - elif name == "proc_ps_threads.log": - state.ps_stats = _parse_proc_ps_threads_log(options, writer, file) + state.ps_stats = _parse_proc_ps_log(options, file, state.num_cpus) + elif name == "proc_ps_threads.log" or name == "proc_ps_threads-2.log" : + state.ps_threads_stats = _parse_proc_ps_threads_log(options, file) elif name == "kernel_pacct": # obsoleted by PROC_EVENTS - state.parent_map = _parse_pacct(writer, file) - elif name == "events-7.log": # 7 is number of fields -- a crude versioning scheme - state.events = _parse_events_log(writer, tf, file) + state.parent_map = _parse_pacct(file) + elif hasattr(options, "event_source"): + boot_t = state.headers.get("boot_time_as_usecs_since_epoch") + assert boot_t, NotImplementedError + for es in options.event_source: + if name == es.filename: + parser = parse_raw_log + es.parsed = parser(state, long(boot_t), file, es.regex) + es.enable = len(es.parsed) > 0 + file.seek(0) # file will be re-scanned for each regex + writer.info("parsed {0:5d} events from {1:16s} using {2:s}".format( + len(es.parsed), file.name, es.regex)) + else: + pass # unknown file in tarball t2 = clock() writer.info(" %s seconds" % str(t2-t1)) return state -def parse_file(writer, state, filename, options): +def parse_file(state, filename, options): if state.filename is None: state.filename = filename basename = os.path.basename(filename) with open(filename, "rb") as file: - return _do_parse(writer, state, None, basename, file, options) + return _do_parse(state, None, basename, file, options) -def parse_paths(writer, state, paths, options): +def parse_paths(state, paths, options): for path in paths: root, extension = os.path.splitext(path) if not(os.path.exists(path)): @@ -941,7 +986,7 @@ def parse_paths(writer, state, paths, options): if os.path.isdir(path): files = [ f for f in [os.path.join(path, f) for f in os.listdir(path)] if os.path.isfile(f) ] files.sort() - state = parse_paths(writer, state, files, options) + state = parse_paths(state, files, options) elif extension in [".tar", ".tgz", ".gz"]: if extension == ".gz": root, extension = os.path.splitext(root) @@ -950,12 +995,27 @@ def parse_paths(writer, state, paths, options): continue state.tf = None try: - writer.status("parsing '%s'" % path) state.tf = tarfile.open(path, 'r:*') - for name in state.tf.getnames(): - state = _do_parse(writer, state, state.tf, name, state.tf.extractfile(name), options) + + # parsing of other files depends on these + early_opens = ["header", "proc_stat.log"] + def not_in_early_opens(name): + return len(filter(lambda n: n==name, early_opens)) == 0 + + for name in early_opens + filter(not_in_early_opens, state.tf.getnames()): + # Extracted file should be seekable, presumably a decompressed copy. + # file:///usr/share/doc/python2.6/html/library/tarfile.html?highlight=extractfile#tarfile.TarFile.extractfile + # XX Python 2.6 extractfile() assumes file contains lines of text, not binary :-( + extracted_file = state.tf.extractfile(name) + if not extracted_file: + continue + state = _do_parse(state, state.tf, name, extracted_file, options) except tarfile.ReadError as error: raise ParseError("error: could not read tarfile '%s': %s." % (path, error)) else: - state = parse_file(writer, state, path, options) + state = parse_file(state, path, options) + + for es in options.event_source: + if es.parsed == None: + raise ParseError("\n\tevents file found on command line but not in tarball: {0}\n".format(es.filename)) return state diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index 4db71f2..98b9930 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -15,6 +15,8 @@ MAX_PID=100000 +from . import writer + def sort_func(proc): return long(proc.pid) / 1000 * MAX_PID + proc.tid / 1000 @@ -42,18 +44,17 @@ class ProcessTree: LOGGER_PROC = 'bootchart-colle' EXPLODER_PROCESSES = set(['hwup']) - def __init__(self, writer, kernel, psstats, sample_period, + def __init__(self, kernel, ps_stats, sample_period, monitoredApp, options, idle, taskstats, accurate_parentage, for_testing = False): - self.writer = writer self.process_tree = [] self.taskstats = taskstats - if psstats is None: + if ps_stats is None: process_list = kernel elif kernel is None: - process_list = psstats.process_map.values() + process_list = ps_stats.process_map.values() else: - process_list = kernel + psstats.process_map.values() + process_list = kernel + ps_stats.process_map.values() self.process_list = sorted(process_list, key = sort_func) self.sample_period = sample_period @@ -190,10 +191,9 @@ def update_ppids_for_daemons(self, process_list): def is_active_process(self, p): # (self.options.hide_low_CPU == 0) is a special case, documented in the usage message. - return (self.options.hide_low_CPU == 0 and \ - (p.activeCount > 0 or len(p.samples) != len(self.process_list[0].samples))) or \ - p.cpu_tick_count_during_run() > self.options.hide_low_CPU or \ - len(p.events) > 0 # any event counts as activity + return p.cpu_tick_count_during_run() > self.options.hide_low_CPU or \ + (self.options.hide_low_CPU == 0 and \ + p.sleepingCount != len(p.samples)) # born, died, or did not sleep for the duration def is_inactive_process(self, p): return not self.is_active_process(p) diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index c4db9a3..4eeb0cd 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -14,35 +14,108 @@ # along with pybootchartgui. If not, see . from types import * +import collections +import re + +from . import writer + +class EventSource: + """ Extract (EventSample)s from some disjoint subset of the available log entries """ + def __init__(self, label, filename, regex): + self.label = label # descriptive name for GUI + self.filename = filename + self.regex = regex + self.parsed = None # list of EventSamples parsed from filename + self.enable = None # initially set to True iff at least one sample was parsed; maintained by gui.py thereafter class EventSample: - def __init__(self, time, time_usec, pid, tid, comm, match, raw_log_file, raw_log_seek): - self.time = time + def dump_format(self): + return "{0:10f} {1:5d} {2:5d} {3} {4}".format( \ + float(self.time_usec)/1000/1000, self.pid, self.tid, self.comm, self.raw_log_line.rstrip()) + + def __init__(self, time_usec, pid, tid, comm, raw_log_file, raw_log_seek, raw_log_line): self.time_usec = time_usec self.pid = pid self.tid = tid self.comm = comm - self.match = match - self.raw_log_file = raw_log_file # a File object + self.raw_log_file = raw_log_file self.raw_log_seek = raw_log_seek + self.raw_log_line = raw_log_line + if pid != 1: + writer.debug(self.dump_format()) + +class EventColor: + def __init__(self, label, regex0, regex1, enable): + self.label = label + self.color_regex = [] + self.color_regex.append(re.compile(regex0)) + if regex1 is not None: + self.color_regex.append(re.compile(regex1)) + self.enable = enable + +# See Documentation/iostats.txt. +IOStat_field_names = ['major', 'minor', 'name', + 'nreads', 'nreads_merged', 'nsectors_read', 'nread_time_msec', + 'nwrites', 'nwrites_merged', 'nsectors_write', 'nwrite_time_msec', + 'nio_in_progress', # not an accumulator + 'io_msec', 'io_weighted_msec'] +IOStat = collections.namedtuple('typename_IOStat', IOStat_field_names) + +# wrapper necessary to produce desired 'int' rather than 'string' types +def IOStat_make(fields_as_list): + return IOStat._make(fields_as_list[:3] + [int(a) for a in fields_as_list[3:]]) + +def IOStat_op(sa, sb, f): + la = list(sa.iostat) + lb = list(sb.iostat) + preamble = [sa.iostat.major, sa.iostat.minor, sa.iostat.name] + return IOStat._make(preamble + + [f(int(a),int(b)) for a, b in zip(la[3:], lb[3:])]) + +def IOStat_diff(sa, sb): + return IOStat_op(sa, sb, lambda a,b: a - b) +def IOStat_sum(sa, sb): + return IOStat_op(sa, sb, lambda a,b: a + b) +def IOStat_max2(sa, sb): + return IOStat_op(sa, sb, lambda a,b: max(a, b)) + +class PartitionSample: + def __init__(self, time, IOStat): + self.time = time + self.iostat = IOStat - def raw_log_line(self): - def _readline(file, raw_log_seek): - if not file: - return - file.seek(raw_log_seek) - line = file.readline() - return line - return _readline(self.raw_log_file, self.raw_log_seek) - +class PartitionDelta: + def __init__(self, time, IOStat, util, nio_in_progress): + self.util = util # computed, not a simple delta + self.nio_in_progress = int(nio_in_progress) # an instantaneous count, not a delta + self.s = PartitionSample(time, IOStat) +class PartitionDeltas: + def __init__(self, partSamples, numCpu, name, label): + assert( type(partSamples) is list) -class DiskStatSample: - def __init__(self, time): - self.time = time - self.diskdata = [0, 0, 0] - def add_diskdata(self, new_diskdata): - self.diskdata = [ a + b for a, b in zip(self.diskdata, new_diskdata) ] + self.name = name + self.label = label # to be drawn for this PartitionSamples object, in a label on the chart + self.numCpu = numCpu + self.hide = True + self.part_deltas = [] + + COALESCE_THRESHOLD = 1 # XX needs synchronization with other graphs + partSamples_coalesced = [(partSamples[0])] + for sample in partSamples: + if sample.time - partSamples_coalesced[-1].time < COALESCE_THRESHOLD: + continue + partSamples_coalesced.append(sample) + + for sample1, sample2 in zip(partSamples_coalesced[:-1], partSamples_coalesced[1:]): + interval = sample2.time - sample1.time + diff = IOStat_diff(sample2, sample1) + util = float(diff.io_msec) / 10 / interval / numCpu + self.part_deltas.append( PartitionDelta(sample2.time, diff, + util, sample2.iostat.nio_in_progress)) + + # Very short intervals amplify round-off under division by time delta, so coalesce now. + # XX scaling issue for high-efficiency collector! class SystemCPUSample: def __init__(self, time, user, sys, io, procs_running, procs_blocked): @@ -97,18 +170,15 @@ def add_value(self, name, value): class ProcessStats: """stats over the collection of all processes, all samples""" - def __init__(self, writer, process_map, sample_count, sample_period, start_time, end_time): + def __init__(self, process_map, sample_count, sample_period): self.process_map = process_map self.sample_count = sample_count self.sample_period = sample_period - self.start_time = start_time # time at which the first sample was collected - self.end_time = end_time writer.info ("%d samples, avg. sample length %f" % (self.sample_count, self.sample_period)) writer.info ("process list size: %d" % len (self.process_map.values())) class Process: - def __init__(self, writer, pid, tid, cmd, ppid, start_time): - self.writer = writer + def __init__(self, pid, tid, cmd, ppid, start_time): self.pid = pid self.tid = tid self.cmd = cmd @@ -119,11 +189,13 @@ def __init__(self, writer, pid, tid, cmd, ppid, start_time): self.duration = 0 self.samples = [] # list of ProcessCPUSample self.events = [] # time-ordered list of EventSample + self.event_interval_0_tx = None self.parent = None self.child_list = [] self.user_cpu_ticks = [None, 0] # [first, last] self.sys_cpu_ticks = [None, 0] + self.delayacct_blkio_ticks = [None, 0] # For transient use as an accumulator during early parsing -- when # concurrent samples of all threads can be accessed O(1). @@ -143,8 +215,8 @@ def cpu_tick_count_during_run(self): # split this process' run - triggered by a name change # XX called only if taskstats.log is provided (bootchart2 daemon) - def split(self, writer, pid, cmd, ppid, start_time): - split = Process (writer, pid, cmd, ppid, start_time) + def split(self, pid, cmd, ppid, start_time): + split = Process (pid, cmd, ppid, start_time) split.last_cpu_ns = self.last_cpu_ns split.last_blkio_delay_ns = self.last_blkio_delay_ns @@ -163,39 +235,24 @@ def calc_stats(self, samplePeriod): # self.duration is the _minimum_ known duration of the thread self.duration = lastSample.time - self.start_time - # XX add in page faults, including "minor" - self.activeCount = sum( [1 for sample in self.samples if sample.state != 'S']) + self.sleepingCount = sum([1 for sample in self.samples if sample.state == 'S']) - def calc_load(self, userCpu, sysCpu, interval): + def calc_load(self, userCpu, sysCpu, delayacct_blkio_ticks, interval, num_cpus): + downscale = interval * num_cpus # all args in units of clock ticks - userCpuLoad = float(userCpu - self.user_cpu_ticks[-1]) / interval - sysCpuLoad = float(sysCpu - self.sys_cpu_ticks[-1]) / interval - return (userCpuLoad, sysCpuLoad) + userCpuLoad = float(userCpu - self.user_cpu_ticks[-1]) / downscale + sysCpuLoad = float(sysCpu - self.sys_cpu_ticks[-1]) / downscale + delayacctBlkioLoad = float(delayacct_blkio_ticks - self.delayacct_blkio_ticks[-1]) / downscale + return (userCpuLoad, sysCpuLoad, delayacctBlkioLoad) def set_parent(self, processMap): if self.ppid != None: self.parent = processMap.get (self.ppid) if self.parent == None and self.pid / 1000 > 1 and \ not (self.ppid == 2000 or self.pid == 2000): # kernel threads: ppid=2 - self.writer.warn("Missing CONFIG_PROC_EVENTS: no parent for pid '%i' ('%s') with ppid '%i'" \ - % (self.pid,self.cmd,self.ppid)) + writer.warn("Missing CONFIG_PROC_EVENTS: no parent for pid '%i' ('%s') with ppid '%i'" \ + % (self.pid,self.cmd,self.ppid)) def get_end_time(self): return self.start_time + self.duration -# To understand 'io_ticks', see the kernel's part_round_stats_single() and part_round_stats() -class DiskSample: - def __init__(self, time, read, write, io_ticks): - self.time = time - self.read = read # sectors, a delta relative to the preceding time - self.write = write # ~ - self.util = io_ticks # a delta, units of msec - self.tput = read + write - -class DiskSamples: - def __init__(self, name, samples): - self.name = name - self.samples = samples -# -# def __str__(self): -# return "\t".join([str(self.time), str(self.read), str(self.write), str(self.util)]) diff --git a/pybootchartgui/writer.py b/pybootchartgui/writer.py new file mode 100644 index 0000000..3d9b8d0 --- /dev/null +++ b/pybootchartgui/writer.py @@ -0,0 +1,38 @@ +import sys + +_writer = None + +class Writer: + def __init__(self, options): + self.options = options + + def _write(self, s): + sys.stderr.write(s + "\n") + +def init(options): + global _writer + _writer = Writer(options) + +def fatal(msg): + _writer._write(msg) + exit(1) + +def error(msg): + _writer._write(msg) + +def warn(msg): + if not _writer.options.quiet: + _writer._write(msg) + +def info(msg): + if _writer.options.verbose > 0: + _writer._write(msg) + +def debug(msg): + if _writer.options.verbose > 1: + _writer._write(msg) + +def status(msg): + if not _writer.options.quiet: + _writer._write(msg) + From b6ee77d0ea7a90918a6755561013c0bc0d2d21b7 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Mon, 3 Dec 2012 23:03:46 -0800 Subject: [PATCH 164/182] draw -- thread or process cmd field show basename only -- not full pathname --- pybootchartgui/parsing.py | 4 ++-- pybootchartgui/samples.py | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index c37706f..7dc1d16 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -287,14 +287,14 @@ def _handle_sample(processMap, ltime, time, if tid in processMap: process = processMap[tid] - process.cmd = cmd.strip('()') # XX loses name changes prior to the final sample + process.set_cmd(cmd) else: if time < starttime: # large values signify a collector problem, e.g. resource starvation writer.status("time (%dcs) < starttime (%dcs), diff %d -- TID %d" % (time, starttime, time-starttime, tid/1000)) - process = Process(pid, tid, cmd.strip('()'), ppid, starttime) + process = Process(pid, tid, cmd, ppid, starttime) if ltime: # process is starting during profiling run process.user_cpu_ticks[0] = 0 process.sys_cpu_ticks [0] = 0 diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 4eeb0cd..e7fcfec 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -19,6 +19,9 @@ from . import writer +# To understand this, see comment "Fedora hack" +PID_SCALE = 1000 + class EventSource: """ Extract (EventSample)s from some disjoint subset of the available log entries """ def __init__(self, label, filename, regex): @@ -181,10 +184,10 @@ class Process: def __init__(self, pid, tid, cmd, ppid, start_time): self.pid = pid self.tid = tid - self.cmd = cmd - self.exe = cmd + self.exe = cmd # may be overwritten, later self.args = [] self.ppid = ppid + self.set_cmd(cmd) # XX depends on self.ppid self.start_time = start_time self.duration = 0 self.samples = [] # list of ProcessCPUSample @@ -256,3 +259,11 @@ def set_parent(self, processMap): def get_end_time(self): return self.start_time + self.duration + def set_cmd(self, cmd): + cmd = cmd.strip('()') + # In case the collector writes the executable's pathname into the 'comm' field + # of /proc/[pid]/stat, strip off all but the basename, to preserve screen space + # -- but some kernel threads are named like so 'ksoftirqd/0'. Hack around + # this by carving out an exception for all children of 'kthreadd' + kthreadd_pid = 2 * PID_SCALE + self.cmd = cmd.split('/')[-1] if self.ppid != kthreadd_pid else cmd From c1803cb2dd9b7e7b935ef015d680d460b011ef9a Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Mon, 26 Nov 2012 22:19:47 -0800 Subject: [PATCH 165/182] gui -- add Statusbar --- pybootchartgui/gui.py | 44 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index 0e7ee41..7c494c0 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -32,6 +32,9 @@ class PyBootchartWidget(gtk.DrawingArea): def __init__(self, trace, drawctx, xscale): gtk.DrawingArea.__init__(self) + # large values cause toolbar to morph into a dropdown + self.MAX_STATUS_LABEL_CHARS = 50 # XX gross hack + self.trace = trace self.drawctx = drawctx @@ -44,6 +47,7 @@ def __init__(self, trace, drawctx, xscale): self.connect("motion-notify-event", self.on_area_motion_notify) self.connect("scroll-event", self.on_area_scroll_event) self.connect('key-press-event', self.on_key_press_event) + self.connect('key-release-event', self.on_key_release_event) self.connect('set-scroll-adjustments', self.on_set_scroll_adjustments) self.connect("size-allocate", self.on_allocation_size_changed) @@ -213,11 +217,15 @@ def show_legends(self, action): POS_INCREMENT = 100 + def _set_italic_label(self, text): + self.status_label.set_markup("{0:s}".format(text)) + # file:///usr/share/doc/libgtk2.0-doc/gtk/GtkWidget.html says: # Returns : # TRUE to stop other handlers from being invoked for the event. # FALSE to propagate the event further def on_key_press_event(self, widget, event): + # print str(event) if event.keyval == gtk.keysyms.Left: self.x -= self.POS_INCREMENT/self.zoom_ratio elif event.keyval == gtk.keysyms.Right: @@ -226,27 +234,39 @@ def on_key_press_event(self, widget, event): self.y -= self.POS_INCREMENT/self.zoom_ratio elif event.keyval == gtk.keysyms.Down: self.y += self.POS_INCREMENT/self.zoom_ratio + elif event.keyval == gtk.keysyms.Control_L or event.keyval == gtk.keysyms.Control_R: + self._set_italic_label("time dilation w/ mouse wheel") + elif event.keyval == gtk.keysyms.Alt_L or event.keyval == gtk.keysyms.Alt_R: + self._set_italic_label("zoom w/ mouse wheel") else: return False #self.queue_draw() self.position_changed() return True + def on_key_release_event(self, widget, event): + self._set_italic_label("") + return True + def on_area_button_press(self, area, event): # cancel any pending action based on an earlier button pressed and now held down self.hide_process_y = [] if event.button == 1: + self._set_italic_label("pan across chart") area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR)) self.prevmousex = event.x self.prevmousey = event.y elif event.button == 2 and len(self.hide_process_y) == 0: + self._set_italic_label("hide/unhide threads/processes") self.hide_process_y.append( self.device_to_csec_user_y(event.x, event.y)[1]) elif event.button == 3: if not self.sweep_csec: + self._set_italic_label("dump events within time range to stdout") self.sweep_csec = [self.device_to_csec_user_y(event.x, 0)[0], self.device_to_csec_user_y(self.trace.end_time, 0)[0]] else: + self._set_italic_label("") self.sweep_csec = None self.queue_draw() if event.type not in (gtk.gdk.BUTTON_PRESS, gtk.gdk.BUTTON_RELEASE): @@ -254,6 +274,7 @@ def on_area_button_press(self, area, event): return True def on_area_button_release(self, area, event): + self.status_label.set_markup("") if event.button == 1: area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW)) self.prevmousex = None @@ -566,20 +587,27 @@ def add_re(ec, callback_name): uimanager.insert_action_group(actiongroup, 0) + # toolbar / h-box hbox = gtk.HBox(False, 0) + hbox.set_homogeneous(False) - # Create a Toolbar toolbar = uimanager.get_widget('/ToolBar') - hbox.pack_start(toolbar, True) + toolbar.set_style(gtk.TOOLBAR_ICONS) # !? no effect + toolbar.set_icon_size(gtk.ICON_SIZE_SMALL_TOOLBAR) # !? no effect + toolbar.set_orientation(gtk.ORIENTATION_HORIZONTAL) + #toolbar.set_alignment(0.0, 0.5) + hbox.pack_start(toolbar, expand=True, fill=True) # if expand== False, a dropdown menu appears menubar = uimanager.get_widget("/MenuBar") - hbox.pack_start(menubar, True) - - # force all the real widgets to the left - # XX Why doesn't this force the others all the way to the left? - empty_menubar = gtk.MenuBar() - hbox.pack_start(empty_menubar, True, True) + #menubar.set_alignment(0.0, 0.5) + hbox.pack_start(menubar, expand=False) + + self.widget.status_label = gtk.Label("") + self.widget.status_label.set_width_chars(self.widget.MAX_STATUS_LABEL_CHARS) + self.widget.status_label.set_single_line_mode(True) + self.widget.status_label.set_alignment(0.98, 0.5) + hbox.pack_end(self.widget.status_label, expand=False, fill=True, padding=2) self.pack_start(hbox, False) From b111b4834daceb2608129db3fe9b06d6ef437fdc Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Sat, 8 Dec 2012 11:49:19 -0800 Subject: [PATCH 166/182] events -- make options.event_source a dict --- pybootchartgui/draw.py | 2 +- pybootchartgui/gui.py | 10 +++++----- pybootchartgui/main.py.in | 6 +++--- pybootchartgui/parsing.py | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 6ceb1c4..b4b8982 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -274,7 +274,7 @@ def copy_if_enabled(color_list, re_index): # Copy events selected by currently enabled EventSources to per-process lists for proc in ps_s.process_map.values(): proc.events = [] - for ep in filter(lambda ep: ep.enable, ctx.app_options.event_source): + for ep in filter(lambda ep: ep.enable, ctx.app_options.event_source.itervalues()): for ev in ep.parsed: key = key_fn(ev) if key in ps_s.process_map: diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index 7c494c0..a903dba 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -187,7 +187,7 @@ def print_event_times(self, action): def event_source_toggle(self, action): # turn second char of the string into an int - self.drawctx.app_options.event_source[int(action.get_name()[1])].enable = action.get_active() + self.drawctx.app_options.event_source[action.get_name()].enable = action.get_active() self.drawctx.ps_event_lists_valid = False self.queue_draw() @@ -496,8 +496,8 @@ def __init__(self, window, trace, drawctx, xscale): ''' - def add_es(index, es, callback): - action_name = "p{0:d}".format(index) # callback will extract a list index from this name string + def add_es(es, callback): + action_name = es.label #"p{0:d}".format(index) # callback will extract a list index from this name string # XX Supports 10 logs, max actiongroup.add_toggle_actions([ (action_name, None, @@ -509,8 +509,8 @@ def add_es(index, es, callback): '''.format(action_name) - for index, es in enumerate(drawctx.app_options.event_source): - ui_Event_Source += add_es(index, es, "event_source_toggle") + for es in drawctx.app_options.event_source.itervalues(): + ui_Event_Source += add_es(es, "event_source_toggle") ui_Event_Source += ''' diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index 3980fb9..b7ca7d7 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -72,7 +72,7 @@ def _mk_options_parser(): help="relocate the text within process bars (left, center)") # event plotting - parser.set_defaults(event_source = []) + parser.set_defaults(event_source = {}) parser.add_option("--event-source", action="callback", callback=handle_event_source_option, type="string", nargs=3, help="Tell pybootchartgui which file in the tarball contains event records," + @@ -173,8 +173,8 @@ def _get_filename(paths, options): def handle_event_source_option(option, opt_str, value, parser): # XX superfluous: note that pdb `print value[1]` will double-print '\' characters # fix = string.replace(value[1], r"\\", "\x5c") - parser.values.event_source.append( - EventSource(value[0], value[1], value[2])) + es = EventSource(value[0], value[1], value[2]) + parser.values.event_source[es.label] = es def handle_event_color(option, opt_str, value, parser): ec = EventColor(value[0], value[1], None, True) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 7dc1d16..7f60821 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -106,7 +106,7 @@ def _generate_sample_start_pseudo_events(self, options): init_pid, init_pid, "comm", None, None, "") es.parsed.append(ev) - options.event_source.append(es) + options.event_source[es.label] = es def valid(self): return self.headers != None and self.disk_stats != None and \ @@ -956,7 +956,7 @@ def _do_parse(state, tf, name, file, options): elif hasattr(options, "event_source"): boot_t = state.headers.get("boot_time_as_usecs_since_epoch") assert boot_t, NotImplementedError - for es in options.event_source: + for es in options.event_source.itervalues(): if name == es.filename: parser = parse_raw_log es.parsed = parser(state, long(boot_t), file, es.regex) @@ -1015,7 +1015,7 @@ def not_in_early_opens(name): else: state = parse_file(state, path, options) - for es in options.event_source: + for es in options.event_source.itervalues(): if es.parsed == None: raise ParseError("\n\tevents file found on command line but not in tarball: {0}\n".format(es.filename)) return state From e37ba9ba1babf82e48504e5fc32514878a2e5330 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 12 Dec 2012 12:42:48 -0800 Subject: [PATCH 167/182] commandline -- rework help text --- pybootchartgui/main.py.in | 105 ++++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 49 deletions(-) diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index b7ca7d7..ae1aab2 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -38,8 +38,6 @@ def _mk_options_parser(): parser = optparse.OptionParser(usage=usage, version=version, description="PATH must point to a bootchart*.tgz", epilog=None) - parser.add_option("--batch", action="store_false", dest="interactive", default=True, - help="print a bootchart image, rather than starting the interactive GUI") parser.add_option("--absolute-uptime-event-times", action="store_true", default=False, dest="absolute_uptime_event_times", help="event times shown are relative to system boot, rather than beginning of sampling") @@ -55,74 +53,81 @@ def _mk_options_parser(): parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0, help="Print debugging messages. Issuing twice will print yet more messages.") parser.add_option("--show-legends", action="store_true", dest="show_legends", default=False, - help="show legend lines with keys to chart symbols") + help="show legend text offering keys to chart symbols") + parser.add_option("--show-pid", action="store_true", dest="show_pid", default=False, + help="show process ids in the bootchart as 'processname [pid]'") + parser.add_option("--show-all", action="store_true", dest="show_all", default=False, + help="show all process information in the bootchart as '/process/path/exe [pid] [args]'") + parser.add_option("-j", "--justify", dest="justify", default="left", choices=["left", "center"], + help="relocate the text within process bars (left, center)") # disk stats - parser.add_option("--partition", action="append", dest="partitions", type="string", default=[], + pg_IO = optparse.OptionGroup(parser,"I/O-specific", + "Options specific to graphing of I/O activity, as gathered from /proc/diskstats") + pg_IO.add_option("--partition", action="append", dest="partitions", type="string", default=[], + metavar="PARTITION", help="draw a disk stat chart for any block device partition whose basename matches PARTITION") - parser.add_option("--relabel-partition", action="append", dest="partition_labels", default=[], + pg_IO.add_option("--relabel-partition", action="append", dest="partition_labels", default=[], help="list of per-partition strings, each replacing the raw per-partition device name" + "in the corresponding position") - parser.add_option("--show-ops-not-bytes", action="store_true", dest="show_ops_not_bytes", default=False, + pg_IO.add_option("--show-ops-not-bytes", action="store_true", dest="show_ops_not_bytes", default=False, help="chart number of I/O operations handed to driver, rather than bytes transferred per sample") - parser.add_option("--show-pid", action="store_true", dest="show_pid", default=False, - help="show process ids in the bootchart as 'processname [pid]'") - parser.add_option("-j", "--justify", dest="justify", default="left", choices=["left", "center"], - help="relocate the text within process bars (left, center)") + parser.add_option_group(pg_IO) # event plotting - parser.set_defaults(event_source = {}) - parser.add_option("--event-source", - action="callback", callback=handle_event_source_option, type="string", nargs=3, - help="Tell pybootchartgui which file in the tarball contains event records," + - "and how to extract the four essential fields from it." + - " A 3-tuple of (label,filename,regex) does the job, e.g.:" + - " --event-source usec-format log-file-in-tarball.txt 'usec_bits=(?P[^,]+),\Wpid=(?P[^,]+),\Wtid=(?P[^,]+),\Wcomm=(?P[^,]+)'" + - "N.B.: The '_' character is unusable in labels, as the PyGTK toolkit silently discards it.") - - parser.set_defaults(event_color = []) - parser.add_option("-e", "--events", nargs=2, type="string", - action="callback", callback=handle_event_color, - metavar="REGEX", help="Highlight events matching REGEX." + - " 2-tuple of (label-string, REGEX)." + - " Regex syntax is similar to grep 'extended' REs." + - " To match FOO or BAR anywhere on the log line, use 'FOO|BAR'." + - " (file:///usr/share/doc/python/html/library/re.htm)") - parser.set_defaults(event_interval_color = []) - parser.add_option("--events-interval", nargs=3, type="string", - action="callback", callback=handle_event_interval_color, - metavar="REGEX", help="3-tuple of (label-string, REGEX_start, REGEX_end)." + - "In addition to highlighting the events matching the two REGEX arguments, " + - " highlight the interval of time between them.") - parser.add_option('-d', "--events-disable", - action="callback", callback=handle_events_disable, type="string", nargs=1, - help="Disable event-parsing regex matching (label) -- its checkbox on the Ev-Color menu will be cleared." + - " Must appear after the option that specified (label).") - - parser.add_option("--hide-events", action="store_true", dest="hide_events", default=False, - help="hide event ticks (small black triangles)") - parser.add_option("--no-print-event-times", action="store_false", default=True, dest="print_event_times", + pg_Events = optparse.OptionGroup(parser, "Event-specific", + "Options specific to plotting of instantaneous, asynchronous events, as gathered from" + + " target-specific logs." + + " A few internally generated pseudo-events are also available." + + " N.B.: The '_' character is unusable in label strings, as the PyGTK toolkit silently discards it.") + pg_Events.add_option("--hide-events", action="store_true", dest="hide_events", default=False, + help="hide all event-related info, even the small black triangles of unhighlighted events.") + pg_Events.add_option("--no-print-event-times", action="store_false", default=True, dest="print_event_times", help="suppress printing time of each event, inside the box of the reporting process") - parser.add_option("--dump-raw-event-context", action="store_true", dest="dump_raw_event_context", default=False, + parser.set_defaults(event_source = {}) + pg_Events.add_option("--event-source", metavar="label filename regex", + action="callback", callback=handle_event_source_option, type="string", nargs=3, + help="Filename should contain event records, one per line of text." + + " Regex should contain assigments using (?P) syntax to initialize variables" + + " CLOCK_REALTIME_usec, pid, tid, and comm.") + pg_Events.add_option("--dump-raw-event-context", action="store_true", dest="dump_raw_event_context", default=False, help="in addition to log lines for selected events, dump log lines as well," + " retaining original log sort order, which may not be temporally correct.") - parser.add_option("--no-synthesize-sample-start-events", action="store_false", default=True, + pg_Events.add_option("--no-synthesize-sample-start-events", action="store_false", default=True, dest="synthesize_sample_start_events", help="disable synthesis of an event marking each boundary between sample periods." + "These are helpful in analyzing collector timing issues.") + parser.add_option_group(pg_Events) - pg_Bootchart2 = optparse.OptionGroup(parser,"Bootchart2-specific", - "Options effective only for logs coming from the Bootchart2 binary-format collector") - pg_Bootchart2.add_option("--show-all", action="store_true", dest="show_all", default=False, - help="show all process information in the bootchart as '/process/path/exe [pid] [args]'") - parser.add_option_group(pg_Bootchart2) + pg_EventHighlighting = optparse.OptionGroup(parser, "Event Highlighting", + "Select events to be highlighted in color, and annotated" + + " with text specific to the event, space permitting.") + parser.set_defaults(event_color = []) + pg_EventHighlighting.add_option("-e", "--events", metavar="label regex", nargs=2, type="string", + action="callback", callback=handle_event_color, + help="Highlight events matching regex." + + " Regex syntax is similar to grep 'extended' REs." + + " To match FOO or BAR anywhere on the log line, use 'FOO|BAR'." + + " (file:///usr/share/doc/python/html/library/re.htm)") + parser.set_defaults(event_interval_color = []) + pg_EventHighlighting.add_option("--events-interval", metavar="label regex_start regex_end", nargs=3, type="string", + action="callback", callback=handle_event_interval_color, + help="In addition to highlighting the events matching the two regex arguments," + + " highlight the interval of time between them.") + pg_EventHighlighting.add_option('-d', "--events-disable", metavar="label", + action="callback", callback=handle_events_disable, type="string", nargs=1, + help="Disable event-parsing regex matching (label) -- its checkbox on the Ev-Color menu will be cleared." + + " Must appear after the option that specified (label).") + parser.add_option_group(pg_EventHighlighting) pg_Scripting = optparse.OptionGroup(parser,"Scripting support", "Options most useful in scripted processing of tgz batches") + pg_Scripting.add_option("--batch", action="store_false", dest="interactive", default=True, + help="print a bootchart image, rather than starting the interactive GUI") pg_Scripting.add_option("--merge", dest="merge", default=False, help="Prune the process tree of all activity from dull children: " + "stats are added to parent, child process is lost in drawing of chart.") @@ -146,6 +151,7 @@ def _mk_options_parser(): pg_Scripting.add_option("--annotate-file", dest="annotate_file", metavar="FILENAME", default=None, help="filename to write annotation points to") parser.add_option_group(pg_Scripting) + return parser def _get_filename(paths, options): @@ -198,7 +204,8 @@ def main(argv=None): argv = sys.argv[1:] parser = _mk_options_parser() - options, args = parser.parse_args(argv) + options, args = parser.parse_args( + ['--events', 'PC samples', '\A(0x[0-9a-f]+|\w+\+0x[0-9a-f]+)\Z'] + argv) writer.init(options) if len(args) == 0: From b2fdc004025a61951d69200514d3372292e19b54 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Sat, 8 Dec 2012 11:34:42 -0800 Subject: [PATCH 168/182] main -- argparse replaces optparse --- pybootchartgui/main.py.in | 278 +++++++++++++++++++------------------- pybootchartgui/parsing.py | 4 +- 2 files changed, 144 insertions(+), 138 deletions(-) diff --git a/pybootchartgui/main.py.in b/pybootchartgui/main.py.in index ae1aab2..b063c55 100644 --- a/pybootchartgui/main.py.in +++ b/pybootchartgui/main.py.in @@ -20,11 +20,10 @@ from __future__ import print_function import sys import os -import optparse +import argparse import string from copy import copy -from optparse import Option, OptionValueError from . import parsing from . import batch @@ -32,125 +31,129 @@ from . import writer from .samples import EventSource, EventColor def _mk_options_parser(): - """Make an options parser.""" - usage = "%prog [options] PATH" - version = "%prog v@VER@" - parser = optparse.OptionParser(usage=usage, version=version, - description="PATH must point to a bootchart*.tgz", - epilog=None) - parser.add_option("--absolute-uptime-event-times", action="store_true", default=False, - dest="absolute_uptime_event_times", - help="event times shown are relative to system boot, rather than beginning of sampling") - parser.add_option("-H", "--hide-low-CPU", dest="hide_low_CPU", type="int", default=0, metavar="CSEC", - help="Hide any thread consuming less than CSEC of CPU time, unless it otherwise shows itself to be of interest." + - " Hiding means the thread is not shown on initial rendering, but can be shown by a mouse click." + - " A value of 0 may be specified, meaning the special case of showing threads that report 0 CSECs" + - " consumed, but are known to have executed due to having been born, died, or reported some" + - " state other than 'S' when sampled by the collector." + - " A negative value causes all threads to be shown.") - parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, - help="suppress informational messages") - parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0, - help="Print debugging messages. Issuing twice will print yet more messages.") - parser.add_option("--show-legends", action="store_true", dest="show_legends", default=False, - help="show legend text offering keys to chart symbols") - parser.add_option("--show-pid", action="store_true", dest="show_pid", default=False, - help="show process ids in the bootchart as 'processname [pid]'") - parser.add_option("--show-all", action="store_true", dest="show_all", default=False, - help="show all process information in the bootchart as '/process/path/exe [pid] [args]'") - parser.add_option("-j", "--justify", dest="justify", default="left", choices=["left", "center"], - help="relocate the text within process bars (left, center)") + usage = "%(prog)s [options] PATH" + parser = argparse.ArgumentParser(usage=usage, + description="PATH must point to a bootchart*.tgz", + epilog=None) + + parser.add_argument('--version', action='version', version="%(prog)s v@VER@") + parser.add_argument("--absolute-uptime-event-times", action="store_true", default=False, + dest="absolute_uptime_event_times", + help="event times shown are relative to system boot, rather than beginning of sampling") + parser.add_argument("-H", "--hide-low-CPU", dest="hide_low_CPU", type=int, default=0, metavar="CSEC", + help="Hide any thread consuming less than CSEC of CPU time," + + " unless it otherwise shows itself to be of interest." + + " Hiding means the thread is not shown on initial rendering, but can be shown by a mouse click." + + " A value of 0 may be specified, meaning the special case of showing threads that report 0 CSECs" + + " consumed, but are known to have executed due to having been born, died, or reported some" + + " state other than 'S' when sampled by the collector." + + " A negative value causes all threads to be shown.") + parser.add_argument("-q", "--quiet", action="store_true", dest="quiet", default=False, + help="suppress informational messages") + parser.add_argument("-v", "--verbose", action="count", dest="verbose", default=0, + help="Print debugging messages. Issuing twice will print yet more messages.") + parser.add_argument("--show-legends", action="store_true", dest="show_legends", default=False, + help="show legend text offering keys to chart symbols") + parser.add_argument("--show-pid", action="store_true", dest="show_pid", default=False, + help="show process ids in the bootchart as 'processname [pid]'") + parser.add_argument("--show-all", action="store_true", dest="show_all", default=False, + help="show all process information in the bootchart as '/process/path/exe [pid] [args]'") + parser.add_argument("-j", "--justify", dest="justify", default="left", choices=["left", "center"], + help="relocate the text within process bars (left, center)") # disk stats - pg_IO = optparse.OptionGroup(parser,"I/O-specific", - "Options specific to graphing of I/O activity, as gathered from /proc/diskstats") - pg_IO.add_option("--partition", action="append", dest="partitions", type="string", default=[], - metavar="PARTITION", - help="draw a disk stat chart for any block device partition whose basename matches PARTITION") - pg_IO.add_option("--relabel-partition", action="append", dest="partition_labels", default=[], - help="list of per-partition strings, each replacing the raw per-partition device name" + - "in the corresponding position") - pg_IO.add_option("--show-ops-not-bytes", action="store_true", dest="show_ops_not_bytes", default=False, - help="chart number of I/O operations handed to driver, rather than bytes transferred per sample") - - parser.add_option_group(pg_IO) + pg_IO = parser.add_argument_group("I/O-specific", + "Options specific to graphing of I/O activity, as gathered from /proc/diskstats") + pg_IO.add_argument("--partition", action="append", dest="partitions", default=[], + metavar="PARTITION", + help="draw a disk stat chart for any block device partition whose basename matches PARTITION") + pg_IO.add_argument("--relabel-partition", action="append", dest="partition_labels", default=[], + help="list of per-partition strings, each replacing the raw per-partition device name" + + "in the corresponding position") + pg_IO.add_argument("--show-ops-not-bytes", action="store_true", dest="show_ops_not_bytes", default=False, + help="chart number of I/O operations handed to driver, rather than bytes transferred per sample") - # event plotting - pg_Events = optparse.OptionGroup(parser, "Event-specific", - "Options specific to plotting of instantaneous, asynchronous events, as gathered from" + - " target-specific logs." + - " A few internally generated pseudo-events are also available." + - " N.B.: The '_' character is unusable in label strings, as the PyGTK toolkit silently discards it.") - pg_Events.add_option("--hide-events", action="store_true", dest="hide_events", default=False, - help="hide all event-related info, even the small black triangles of unhighlighted events.") - pg_Events.add_option("--no-print-event-times", action="store_false", default=True, dest="print_event_times", - help="suppress printing time of each event, inside the box of the reporting process") + parser.add_argument_group(pg_IO) + # event plotting parser.set_defaults(event_source = {}) - pg_Events.add_option("--event-source", metavar="label filename regex", - action="callback", callback=handle_event_source_option, type="string", nargs=3, - help="Filename should contain event records, one per line of text." + - " Regex should contain assigments using (?P) syntax to initialize variables" + - " CLOCK_REALTIME_usec, pid, tid, and comm.") - pg_Events.add_option("--dump-raw-event-context", action="store_true", dest="dump_raw_event_context", default=False, - help="in addition to log lines for selected events, dump log lines as well," + - " retaining original log sort order, which may not be temporally correct.") - - pg_Events.add_option("--no-synthesize-sample-start-events", action="store_false", default=True, - dest="synthesize_sample_start_events", - help="disable synthesis of an event marking each boundary between sample periods." + - "These are helpful in analyzing collector timing issues.") - parser.add_option_group(pg_Events) - - pg_EventHighlighting = optparse.OptionGroup(parser, "Event Highlighting", + pg_Events = parser.add_argument_group( "Event-specific", + "Options specific to plotting of instantaneous, asynchronous events, as gathered from" + + " target-specific logs." + + " A few internally generated pseudo-events are also available." + + " N.B.: The '_' character is unusable in label strings, as the PyGTK toolkit silently discards it.") + pg_Events.add_argument("--hide-events", action="store_true", dest="hide_events", default=False, + help="hide all event-related info, even the small black triangles of unhighlighted events.") + pg_Events.add_argument("--no-print-event-times", action="store_false", default=True, dest="print_event_times", + help="suppress printing time of each event, inside the box of the reporting process") + pg_Events.add_argument("--event-source", metavar=("label", "filename", "regex"), + action=handle_event_source_option, nargs=3, + help="Filename should contain event records, one per line of text." + + " Regex should contain assigments using (?P) syntax to initialize variables" + + " CLOCK_REALTIME_usec, pid, tid, and comm.") + pg_Events.add_argument("--dump-raw-event-context", action="store_true", dest="dump_raw_event_context", default=False, + help="in addition to log lines for selected events, dump log lines as well," + + " retaining original log sort order, which may not be temporally correct.") + + pg_Events.add_argument("--no-synthesize-sample-start-events", action="store_false", default=True, + dest="synthesize_sample_start_events", + help="disable synthesis of an event marking each boundary between sample periods." + + "These are helpful in analyzing collector timing issues.") + parser.add_argument_group(pg_Events) + + pg_EventHighlighting = parser.add_argument_group( "Event Highlighting", "Select events to be highlighted in color, and annotated" + " with text specific to the event, space permitting.") parser.set_defaults(event_color = []) - pg_EventHighlighting.add_option("-e", "--events", metavar="label regex", nargs=2, type="string", - action="callback", callback=handle_event_color, - help="Highlight events matching regex." + - " Regex syntax is similar to grep 'extended' REs." + - " To match FOO or BAR anywhere on the log line, use 'FOO|BAR'." + - " (file:///usr/share/doc/python/html/library/re.htm)") + pg_EventHighlighting.add_argument("-e", "--events", metavar=("label", "regex"), nargs=2, + action=handle_event_color, + help="Highlight events matching regex." + + " Regex syntax is similar to grep 'extended' REs." + + " To match FOO or BAR anywhere on the log line, use 'FOO|BAR'." + + " (file:///usr/share/doc/python/html/library/re.htm)") parser.set_defaults(event_interval_color = []) - pg_EventHighlighting.add_option("--events-interval", metavar="label regex_start regex_end", nargs=3, type="string", - action="callback", callback=handle_event_interval_color, - help="In addition to highlighting the events matching the two regex arguments," + - " highlight the interval of time between them.") - pg_EventHighlighting.add_option('-d', "--events-disable", metavar="label", - action="callback", callback=handle_events_disable, type="string", nargs=1, - help="Disable event-parsing regex matching (label) -- its checkbox on the Ev-Color menu will be cleared." + - " Must appear after the option that specified (label).") - parser.add_option_group(pg_EventHighlighting) - - pg_Scripting = optparse.OptionGroup(parser,"Scripting support", - "Options most useful in scripted processing of tgz batches") - - pg_Scripting.add_option("--batch", action="store_false", dest="interactive", default=True, - help="print a bootchart image, rather than starting the interactive GUI") - pg_Scripting.add_option("--merge", dest="merge", default=False, - help="Prune the process tree of all activity from dull children: " + - "stats are added to parent, child process is lost in drawing of chart.") - pg_Scripting.add_option("--prehistory", action="store_true", dest="prehistory", default=False, - help="extend process bars to the recorded start time of each, even if before any samples were collected") - pg_Scripting.add_option("-t", "--boot-time", action="store_true", dest="boottime", default=False, - help="only display the boot time of the boot in text format (stdout)") - pg_Scripting.add_option("-f", "--format", dest="format", default="png", choices=["png", "svg", "pdf"], - help="image format (png, svg, pdf); default format png") - pg_Scripting.add_option("--very-quiet", action="store_true", dest="veryquiet", default=False, - help="suppress all messages except errors") - pg_Scripting.add_option("-o", "--output", dest="output", metavar="PATH", default=None, - help="output path (file or directory) where charts are stored") - pg_Scripting.add_option("--profile", action="store_true", dest="profile", default=False, - help="profile rendering of chart (only useful when in batch mode indicated by -f)") - pg_Scripting.add_option("--crop-after", dest="crop_after", metavar="PROCESS", default=None, - help="crop chart when idle after PROCESS is started") - pg_Scripting.add_option("--annotate", action="append", dest="annotate", metavar="PROCESS", default=None, - help="annotate position where PROCESS is started; can be specified multiple times. " + - "To create a single annotation when any one of a set of processes is started, use commas to separate the names") - pg_Scripting.add_option("--annotate-file", dest="annotate_file", metavar="FILENAME", default=None, - help="filename to write annotation points to") - parser.add_option_group(pg_Scripting) + pg_EventHighlighting.add_argument("--events-interval", metavar=("label", "regex_start", "regex_end"), nargs=3, + action=handle_event_interval_color, + help="In addition to highlighting the events matching the two regex arguments," + + " highlight the interval of time between them.") + pg_EventHighlighting.add_argument('-d', "--events-disable", metavar=("label"), + action=handle_events_disable, nargs=1, + help="Disable event-parsing regex matching (label) -- its checkbox on the Ev-Color menu will be cleared." + + " Must appear after the option that specified (label).") + parser.add_argument_group(pg_EventHighlighting) + + pg_Scripting = parser.add_argument_group("Scripting support", + "Options most useful in scripted processing of tgz batches") + + pg_Scripting.add_argument("--batch", action="store_false", dest="interactive", default=True, + help="print a bootchart image, rather than starting the interactive GUI") + pg_Scripting.add_argument("--merge", dest="merge", default=False, + help="Prune the process tree of all activity from dull children: " + + "stats are added to parent, child process is lost in drawing of chart.") + pg_Scripting.add_argument("--prehistory", action="store_true", dest="prehistory", default=False, + help="extend process bars to the recorded start time of each, even if before any samples were collected") + pg_Scripting.add_argument("-t", "--boot-time", action="store_true", dest="boottime", default=False, + help="only display the boot time of the boot in text format (stdout)") + pg_Scripting.add_argument("-f", "--format", dest="format", default="png", choices=["png", "svg", "pdf"], + help="image format (png, svg, pdf); default format png") + pg_Scripting.add_argument("--very-quiet", action="store_true", dest="veryquiet", default=False, + help="suppress all messages except errors") + pg_Scripting.add_argument("-o", "--output", dest="output", metavar=("PATH"), default=None, + help="output path (file or directory) where charts are stored") + pg_Scripting.add_argument("--profile", action="store_true", dest="profile", default=False, + help="profile rendering of chart (only useful when in batch mode indicated by -f)") + pg_Scripting.add_argument("--crop-after", dest="crop_after", metavar=("PROCESS"), default=None, + help="crop chart when idle after PROCESS is started") + pg_Scripting.add_argument("--annotate", action="append", dest="annotate", metavar=("PROCESS"), default=None, + help="annotate position where PROCESS is started; can be specified multiple times. " + + "To create a single annotation when any one of a set of processes is started," + + " use commas to separate the names") + pg_Scripting.add_argument("--annotate-file", dest="annotate_file", metavar=("FILENAME"), default=None, + help="filename to write annotation points to") + parser.add_argument_group(pg_Scripting) + + parser.add_argument(dest="paths", action="append", + help="bootchart.tgz") return parser @@ -176,27 +179,31 @@ def _get_filename(paths, options): fname = os.path.split(fname)[-1] return os.path.join (dname, fname + "." + options.format) -def handle_event_source_option(option, opt_str, value, parser): - # XX superfluous: note that pdb `print value[1]` will double-print '\' characters - # fix = string.replace(value[1], r"\\", "\x5c") - es = EventSource(value[0], value[1], value[2]) - parser.values.event_source[es.label] = es +class handle_event_source_option(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + # XX superfluous: note that pdb `print values[1]` will double-print '\' characters + # fix = string.replace(values[1], r"\\", "\x5c") + es = EventSource(values[0], values[1], values[2]) + namespace.event_source[es.label] = es -def handle_event_color(option, opt_str, value, parser): - ec = EventColor(value[0], value[1], None, True) - parser.values.event_color.append(ec) +class handle_event_color(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + ec = EventColor(values[0], values[1], None, True) + namespace.event_color.append(ec) -def handle_event_interval_color(option, opt_str, value, parser): - ec = EventColor(value[0], value[1], value[2], True) - parser.values.event_interval_color.append(ec) +class handle_event_interval_color(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + ec = EventColor(values[0], values[1], values[2], True) + namespace.event_interval_color.append(ec) -def handle_events_disable(option, opt_str, value, parser): - for ec in parser.values.event_color + parser.values.event_interval_color: - if ec.label == value: - ec.enable = False - return - raise parsing.ParseError("Event-Color label {1:s} not found\n\t{0:s}".format( - opt_str, value)) +class handle_events_disable(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + for ec in namespace.event_color + namespace.event_interval_color: + if ec.label == values[0]: + ec.enable = False + return + raise parsing.ParseError("Event-Color label {1:s} not found\n\t{0:s}".format( + opt_str, values[0])) def main(argv=None): try: @@ -204,14 +211,13 @@ def main(argv=None): argv = sys.argv[1:] parser = _mk_options_parser() - options, args = parser.parse_args( + + # XX 'argparse' documentation prefers to name the variable 'args' not 'options' + options = parser.parse_args( ['--events', 'PC samples', '\A(0x[0-9a-f]+|\w+\+0x[0-9a-f]+)\Z'] + argv) writer.init(options) - if len(args) == 0: - raise parsing.ParseError("\n\tNo path to a bootchart.tgz found on command line") - - res = parsing.Trace(args, options) + res = parsing.Trace(options) if options.interactive: from . import gui @@ -237,7 +243,7 @@ def main(argv=None): print(file=f) finally: f.close() - filename = _get_filename(args, options) + filename = options.filename def render(): batch.render(res, options, filename) if options.profile: diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 7f60821..8c80146 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -35,7 +35,7 @@ # Parsing produces as its end result a 'Trace' class Trace: - def __init__(self, paths, options): + def __init__(self, options): self.headers = None self.disk_stats = None self.ps_stats = None @@ -50,7 +50,7 @@ def __init__(self, paths, options): self.mem_stats = None # Read in all files, parse each into a time-ordered list - parse_paths (self, paths, options) + parse_paths (self, options.paths, options) # support deprecated data sets that contain no proc_ps.log, only proc_ps_threads.log if not self.ps_stats: From bd57299391f34f12d28eafd8ce01aa63b95c836a Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Thu, 29 Nov 2012 22:30:53 -0800 Subject: [PATCH 169/182] draw TID for procPIDtaskTIDstat threads only --- pybootchartgui/draw.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index b4b8982..be25205 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -992,7 +992,10 @@ def draw_process(ctx, proc, proc_tree, x, y, w): prefix = " [" if ctx.app_options.show_all: prefix += str(proc.ppid / 1000) + ":" - prefix += str(proc.pid / 1000) + ":" + str(proc.tid / 1000) + "]" + prefix += str(proc.pid / 1000) + if ctx.trace.ps_threads_stats: + prefix += ":" + str(proc.tid / 1000) + prefix += "]" cmdString = prefix + cmdString if ctx.app_options.show_all and proc.args: cmdString += " '" + "' '".join(proc.args) + "'" From ac1b001f767cd91552eb0a87a2eb3edac1c34e3c Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 21 Nov 2012 14:51:36 -0800 Subject: [PATCH 170/182] gui sort EvSource pulldown entries --- pybootchartgui/gui.py | 9 ++++++--- pybootchartgui/parsing.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index a903dba..5b24bfb 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -186,7 +186,6 @@ def print_event_times(self, action): self.queue_draw() def event_source_toggle(self, action): - # turn second char of the string into an int self.drawctx.app_options.event_source[action.get_name()].enable = action.get_active() self.drawctx.ps_event_lists_valid = False self.queue_draw() @@ -509,8 +508,12 @@ def add_es(es, callback): '''.format(action_name) - for es in drawctx.app_options.event_source.itervalues(): - ui_Event_Source += add_es(es, "event_source_toggle") + es_dict = drawctx.app_options.event_source + # Appearance note: menu entries will appear lexicographically sorted + es_keys_sorted = es_dict.keys() + es_keys_sorted.sort() + for ek in es_keys_sorted: + ui_Event_Source += add_es(es_dict[ek], "event_source_toggle") ui_Event_Source += ''' diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 8c80146..1482a2e 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -96,7 +96,7 @@ def __init__(self, options): self._generate_sample_start_pseudo_events(options) def _generate_sample_start_pseudo_events(self, options): - es = EventSource(" ... sample start points", "", "") # empty regex + es = EventSource(" ~ bootchartd sample start points", "", "") # empty regex es.parsed = [] es.enable = True init_pid = 1 From f4b0de63408e20ad3aa82db67ed7acf00b748082 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 12 Dec 2012 13:14:25 -0800 Subject: [PATCH 171/182] events PC sample pseudo EvSource refactor --- pybootchartgui/parsing.py | 57 ++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 1482a2e..209dc0d 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -21,6 +21,7 @@ import re import tarfile import struct +import bisect from time import clock import collections from collections import defaultdict @@ -272,6 +273,10 @@ def parse(block): blocks = file.read().split('\n\n') return [parse(block) for block in blocks if block.strip() and not block.endswith(' not running\n')] +def _save_PC_sample(trace, es, time, pid, tid, comm, addr): + ev = EventSample(csec_to_usec(time), pid, tid, comm, None, None, "0x{0:08x}".format(addr)) + es.parsed.append(ev) + # Cases to handle: # 1. run starting (ltime==None) # 1.1 thread started in preceding sample_period @@ -279,8 +284,9 @@ def parse(block): # 2. run continuing # 2.1 thread continues (tid in processMap) # 2.2 thread starts -def _handle_sample(processMap, ltime, time, - pid, tid, cmd, state, ppid, userCpu, sysCpu, delayacct_blkio_ticks, c_user, c_sys, starttime, +def _handle_sample(options, trace, processMap, ltime, time, + pid, tid, cmd, state, ppid, userCpu, sysCpu, + kstkeip, wchan, delayacct_blkio_ticks, c_user, c_sys, starttime, num_cpus): assert(type(c_user) is IntType) assert(type(c_sys) is IntType) @@ -320,6 +326,16 @@ def _handle_sample(processMap, ltime, time, process.user_cpu_ticks[-1] = userCpu process.sys_cpu_ticks[-1] = sysCpu process.delayacct_blkio_ticks[-1] = delayacct_blkio_ticks + + if state == "R" and kstkeip != 0 and kstkeip != 0xffffffff: + _save_PC_sample(trace, options.event_source[" ~ current PC, if thread Runnable"], + time, pid/1000, tid/1000, cmd, kstkeip) + if state == "D" and kstkeip != 0 and kstkeip != 0xffffffff: + _save_PC_sample(trace, options.event_source[" ~ current PC, if thread in non-interruptible D-wait"], + time, pid/1000, tid/1000, cmd, kstkeip) + if state == "D" and wchan != 0: + _save_PC_sample(trace, options.event_source[" ~ kernel function wchan, if thread in non-interruptible D-wait"], + time, pid/1000, tid/1000, cmd, wchan) return processMap # Consider this case: a process P and three waited-for children C1, C2, C3. @@ -406,7 +422,20 @@ def _distribute_belatedly_reported_delayacct_blkio_ticks(processMap): io_acc = s.cpu_sample.io + s.cpu_sample.user + s.cpu_sample.sys - 1.0 s.cpu_sample.io -= io_acc -def _parse_proc_ps_log(options, file, num_cpus): +def _init_pseudo_EventSource_for_PC_samples(name): + es = EventSource(name, "", "") # empty regex + es.enable = False + es.parsed = [] + return es + +def _init_pseudo_EventSources_for_PC_samples(options): + for label in [" ~ current PC, if thread Runnable", + " ~ current PC, if thread in non-interruptible D-wait", + " ~ kernel function wchan, if thread in non-interruptible D-wait"]: + es = _init_pseudo_EventSource_for_PC_samples(label) + options.event_source[label] = es # XX options.event_source must be used because the Trace object is not yet instantiated + +def _parse_proc_ps_log(options, trace, file, num_cpus): """ * See proc(5) for details. * @@ -414,6 +443,8 @@ def _parse_proc_ps_log(options, file, num_cpus): * cutime, cstime, priority, nice, 0, itrealvalue, starttime, vsize, rss, rlim, startcode, endcode, startstack, * kstkesp, kstkeip} """ + _init_pseudo_EventSources_for_PC_samples(options) + processMap = {} ltime = None timed_blocks = _parse_timed_blocks(file) @@ -429,14 +460,16 @@ def _parse_proc_ps_log(options, file, num_cpus): userCpu, sysCpu = int(tokens[13+offset]), int(tokens[14+offset]), c_user, c_sys = int(tokens[15+offset]), int(tokens[16+offset]) starttime = int(tokens[21+offset]) + kstkeip = int(tokens[29+offset]) + wchan = int(tokens[34+offset]) delayacct_blkio_ticks = int(tokens[41+offset]) # magic fixed point-ness ... pid *= 1000 ppid *= 1000 - processMap = _handle_sample(processMap, ltime, time, + processMap = _handle_sample(options, trace, processMap, ltime, time, pid, pid, cmd, state, ppid, - userCpu, sysCpu, delayacct_blkio_ticks, c_user, c_sys, starttime, + userCpu, sysCpu, kstkeip, wchan, delayacct_blkio_ticks, c_user, c_sys, starttime, num_cpus) if ltime: accumulate_missing_child_ltime(processMap, ltime) @@ -453,7 +486,7 @@ def _parse_proc_ps_log(options, file, num_cpus): return ProcessStats (processMap, len (timed_blocks), avgSampleLength) -def _parse_proc_ps_threads_log(options, file): +def _parse_proc_ps_threads_log(options, trace, file): """ * 0* pid -- inserted here from value in /proc/*pid*/task/. Not to be found in /proc/*pid*/task/*tid*/stat. * Not the same as pgrp, session, or tpgid. Refer to collector daemon source code for details. @@ -475,6 +508,8 @@ def _parse_proc_ps_threads_log(options, file): * 16 scheduling_policy * 17* delayacct_blkio_ticks -- in proc_ps_threads-2.log only, requires CONFIG_TASK_DELAY_ACCT """ + _init_pseudo_EventSources_for_PC_samples(options) + processMap = {} ltime = None timed_blocks = _parse_timed_blocks(file) @@ -490,6 +525,8 @@ def _parse_proc_ps_threads_log(options, file): pid, tid, cmd, state, ppid = int(tokens[0]), int(tokens[1]), ' '.join(tokens[2:3+offset]), tokens[3+offset], int(tokens[4+offset]) userCpu, sysCpu = int(tokens[7+offset]), int(tokens[8+offset]) c_user, c_sys = int(tokens[9+offset]), int(tokens[10+offset]) + kstkeip = int(tokens[14+offset]) + wchan = int(tokens[15+offset]) delayacct_blkio_ticks = int(tokens[17+offset]) if len(tokens) == 18+offset else 0 assert(type(c_user) is IntType) @@ -501,9 +538,9 @@ def _parse_proc_ps_threads_log(options, file): tid *= 1000 ppid *= 1000 - processMap = _handle_sample(processMap, ltime, time, + processMap = _handle_sample(options, trace, processMap, ltime, time, pid, tid, cmd, state, ppid, - userCpu, sysCpu, delayacct_blkio_ticks, c_user, c_sys, starttime, + userCpu, sysCpu, kstkeip, wchan, delayacct_blkio_ticks, c_user, c_sys, starttime, 1) ltime = time @@ -948,9 +985,9 @@ def _do_parse(state, tf, name, file, options): elif name == "paternity.log": state.parent_map = _parse_paternity_log(file) elif name == "proc_ps.log": # obsoleted by TASKSTATS - state.ps_stats = _parse_proc_ps_log(options, file, state.num_cpus) + state.ps_stats = _parse_proc_ps_log(options, state, file, state.num_cpus) elif name == "proc_ps_threads.log" or name == "proc_ps_threads-2.log" : - state.ps_threads_stats = _parse_proc_ps_threads_log(options, file) + state.ps_threads_stats = _parse_proc_ps_threads_log(options, state, file) elif name == "kernel_pacct": # obsoleted by PROC_EVENTS state.parent_map = _parse_pacct(file) elif hasattr(options, "event_source"): From 12c07246d08710ab3ad31a699d687ae61218c88d Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Tue, 4 Dec 2012 09:14:19 -0800 Subject: [PATCH 172/182] variable rename -- parsing.py --- pybootchartgui/parsing.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 209dc0d..71805f6 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -292,40 +292,40 @@ def _handle_sample(options, trace, processMap, ltime, time, assert(type(c_sys) is IntType) if tid in processMap: - process = processMap[tid] - process.set_cmd(cmd) + proc = processMap[tid] + proc.set_cmd(cmd) else: if time < starttime: # large values signify a collector problem, e.g. resource starvation writer.status("time (%dcs) < starttime (%dcs), diff %d -- TID %d" % (time, starttime, time-starttime, tid/1000)) - process = Process(pid, tid, cmd, ppid, starttime) + proc = Process(pid, tid, cmd, ppid, starttime) if ltime: # process is starting during profiling run - process.user_cpu_ticks[0] = 0 - process.sys_cpu_ticks [0] = 0 - process.delayacct_blkio_ticks [0] = 0 + proc.user_cpu_ticks[0] = 0 + proc.sys_cpu_ticks [0] = 0 + proc.delayacct_blkio_ticks [0] = 0 ltime = starttime else: - process.user_cpu_ticks[0] = userCpu - process.sys_cpu_ticks [0] = sysCpu - process.delayacct_blkio_ticks [0] = delayacct_blkio_ticks + proc.user_cpu_ticks[0] = userCpu + proc.sys_cpu_ticks [0] = sysCpu + proc.delayacct_blkio_ticks [0] = delayacct_blkio_ticks ltime = -100000 # XX hacky way of forcing reported load toward zero - process.user_cpu_ticks[-1] = process.user_cpu_ticks[0] - process.sys_cpu_ticks [-1] = process.sys_cpu_ticks [0] - process.delayacct_blkio_ticks[-1] = process.delayacct_blkio_ticks[0] - processMap[tid] = process # insert new process into the dict + proc.user_cpu_ticks[-1] = proc.user_cpu_ticks[0] + proc.sys_cpu_ticks [-1] = proc.sys_cpu_ticks [0] + proc.delayacct_blkio_ticks[-1] = proc.delayacct_blkio_ticks[0] + processMap[tid] = proc # insert new process into the dict - userCpuLoad, sysCpuLoad, delayacctBlkioLoad = process.calc_load(userCpu, sysCpu, delayacct_blkio_ticks, + userCpuLoad, sysCpuLoad, delayacctBlkioLoad = proc.calc_load(userCpu, sysCpu, delayacct_blkio_ticks, max(1, time - ltime), num_cpus) cpuSample = ProcessCPUSample('null', userCpuLoad, sysCpuLoad, c_user, c_sys, delayacctBlkioLoad, 0.0) - process.samples.append(ProcessSample(time, state, cpuSample)) + proc.samples.append(ProcessSample(time, state, cpuSample)) # per-tid store for use by a later phase of parsing of samples gathered at this 'time' - process.user_cpu_ticks[-1] = userCpu - process.sys_cpu_ticks[-1] = sysCpu - process.delayacct_blkio_ticks[-1] = delayacct_blkio_ticks + proc.user_cpu_ticks[-1] = userCpu + proc.sys_cpu_ticks[-1] = sysCpu + proc.delayacct_blkio_ticks[-1] = delayacct_blkio_ticks if state == "R" and kstkeip != 0 and kstkeip != 0xffffffff: _save_PC_sample(trace, options.event_source[" ~ current PC, if thread Runnable"], From 96b2a64e768cca9ba57ab89b0b88a0436a7517d6 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 5 Dec 2012 09:29:24 -0800 Subject: [PATCH 173/182] Process carries LWP boolean flag --- pybootchartgui/draw.py | 98 ++++++++++++++++++++-------------- pybootchartgui/parsing.py | 30 ++++++----- pybootchartgui/process_tree.py | 9 ++++ pybootchartgui/samples.py | 14 ++++- 4 files changed, 95 insertions(+), 56 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index be25205..c10843a 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -22,7 +22,7 @@ import collections import traceback # debug -from samples import IOStat, EventSample +from samples import IOStat, EventSample, PID_SCALE, LWP_OFFSET from . import writer # Constants: Put the more heavily used, non-derived constants in a named tuple, for immutability. @@ -47,6 +47,7 @@ BLACK = (0.0, 0.0, 0.0, 1.0) DARK_GREY = (0.1, 0.1, 0.1) NOTEPAD_YELLOW = (0.95, 0.95, 0.8, 1.0) +NOTEPAD_PINK = (1.0, 0.90, 1.0, 1.0) PURPLE = (0.6, 0.1, 0.6, 1.0) RED = (1.0, 0.0, 0.0) MAGENTA = (0.7, 0.0, 0.7, 1.0) @@ -265,26 +266,26 @@ def copy_if_enabled(color_list, re_index): self.event_interval_1_RE = copy_if_enabled( self.app_options.event_interval_color, 1) if trace.ps_threads_stats: - ps_s = trace.ps_threads_stats - key_fn = lambda ev: ev.tid * 1000 + key_fn = lambda ev: ev.tid * PID_SCALE + LWP_OFFSET else: - ps_s = trace.ps_stats - key_fn = lambda ev: ev.pid * 1000 + key_fn = lambda ev: ev.pid * PID_SCALE + + p_map = trace.ps_stats.process_map # Copy events selected by currently enabled EventSources to per-process lists - for proc in ps_s.process_map.values(): + for proc in p_map.values(): proc.events = [] for ep in filter(lambda ep: ep.enable, ctx.app_options.event_source.itervalues()): for ev in ep.parsed: key = key_fn(ev) - if key in ps_s.process_map: - ps_s.process_map[key].events.append(ev) + if key in p_map: + p_map[key].events.append(ev) else: writer.warn("no samples of /proc/%d/task/%d/stat found -- event lost:\n\t%s" % (ev.pid, ev.tid, ev.raw_log_line)) # Strip out from per-process lists any events not selected by a regexp. - for proc in ps_s.process_map.values(): + for proc in p_map.values(): enabled_events = [] for ev in proc.events: # Separate attrs, because they may be drawn differently @@ -334,7 +335,7 @@ def per_render_init(self, cr, ctx, trace, time_origin_drawn, SEC_W, sweep_csec): def proc_tree (self, trace): return trace.kernel_tree if self.kernel_only else trace.proc_tree - def draw_process_label_in_box(self, color, label, + def draw_process_label_in_box(self, color, bg_color, label, x, y, w, minx, maxx): # hack in a pair of left and right margins extents = self.cr.text_extents("j" + label + "k") # XX "j", "k" were found by tedious trial-and-error @@ -353,7 +354,7 @@ def draw_process_label_in_box(self, color, label, if label_x < minx: label_x = minx # XX ugly magic constants, tuned by trial-and-error - draw_fill_rect(self.cr, NOTEPAD_YELLOW, (label_x, y-1, label_w, -(C.proc_h-2))) + draw_fill_rect(self.cr, bg_color, (label_x, y-1, label_w, -(C.proc_h-2))) draw_text(self.cr, label, color, label_x, y-4) # XX Should be "_csec_to_user" @@ -888,7 +889,8 @@ def draw_process_bar_chart(ctx, proc_tree, times, curr_y, w, h): curr_y += 15 for root in proc_tree.process_tree: - curr_y = draw_processes_recursively(ctx, root, proc_tree, curr_y)[1] + if not root.lwp(): + curr_y = draw_processes_recursively(ctx, root, proc_tree, curr_y)[1] if ctx.proc_above_was_hidden: draw_hidden_process_separator(ctx, curr_y) ctx.proc_above_was_hidden = False @@ -976,7 +978,8 @@ def draw_process(ctx, proc, proc_tree, x, y, w): # Do not draw right-hand vertical border -- process exit never exactly known draw_visible_process_separator(ctx, proc, x, y, w) - draw_process_state_colors(ctx, proc, proc_tree, x, y, w) + if proc.lwp() or not ctx.trace.ps_threads_stats: + draw_process_state_colors(ctx, proc, proc_tree, x, y, w) # Event ticks step on the rectangle painted by draw_process_state_colors(), # e.g. for non-interruptible wait. @@ -993,52 +996,65 @@ def draw_process(ctx, proc, proc_tree, x, y, w): if ctx.app_options.show_all: prefix += str(proc.ppid / 1000) + ":" prefix += str(proc.pid / 1000) - if ctx.trace.ps_threads_stats: + if proc.lwp(): prefix += ":" + str(proc.tid / 1000) prefix += "]" cmdString = prefix + cmdString if ctx.app_options.show_all and proc.args: cmdString += " '" + "' '".join(proc.args) + "'" - ctx.draw_process_label_in_box( PROC_TEXT_COLOR, cmdString, - csec_to_xscaled(ctx, max(proc.start_time,ctx.time_origin_drawn)), - y, - w, - ctx.cr.device_to_user(0, 0)[0], - ctx.cr.clip_extents()[2]) + ctx.draw_process_label_in_box( PROC_TEXT_COLOR, + NOTEPAD_YELLOW if proc.lwp() else NOTEPAD_PINK, + cmdString, + csec_to_xscaled(ctx, max(proc.start_time,ctx.time_origin_drawn)), + y, + w, + ctx.cr.device_to_user(0, 0)[0], + ctx.cr.clip_extents()[2]) return n_highlighted_events def draw_processes_recursively(ctx, proc, proc_tree, y): xmin = ctx.cr.device_to_user(0, 0)[0] # work around numeric overflow at high xscale factors xmin = max(xmin, 0) - x = max(xmin, csec_to_xscaled(ctx, proc.start_time)) - w = max(xmin, csec_to_xscaled(ctx, proc.start_time + proc.duration)) - x - if ctx.hide_process_y and y+C.proc_h > ctx.hide_process_y[0] and proc.draw: - proc.draw = False - ctx.hide_process_y[1] -= C.proc_h - if y > (ctx.hide_process_y[1]) / C.proc_h * C.proc_h: - ctx.hide_process_y = None + def draw_process_and_separator(ctx, proc, proc_tree, y): + x = max(xmin, csec_to_xscaled(ctx, proc.start_time)) + w = max(xmin, csec_to_xscaled(ctx, proc.start_time + proc.duration)) - x + if ctx.hide_process_y and y+C.proc_h > ctx.hide_process_y[0] and proc.draw: + proc.draw = False + ctx.hide_process_y[1] -= C.proc_h + if y > (ctx.hide_process_y[1]) / C.proc_h * C.proc_h: + ctx.hide_process_y = None + + elif ctx.unhide_process_y and y+C.proc_h*3/4 > ctx.unhide_process_y: + if proc.draw: # found end of run of hidden processes + ctx.unhide_process_y = None + else: + proc.draw = True + ctx.unhide_process_y += C.proc_h - elif ctx.unhide_process_y and y+C.proc_h*3/4 > ctx.unhide_process_y: - if proc.draw: # found end of run of hidden processes - ctx.unhide_process_y = None + if not proc.draw: + ctx.proc_above_was_hidden = True + return x, y else: - proc.draw = True - ctx.unhide_process_y += C.proc_h + n_highlighted_events = draw_process(ctx, proc, proc_tree, x, y+USER_HALF, w) + if ctx.proc_above_was_hidden: + draw_hidden_process_separator(ctx, y+USER_HALF) + ctx.proc_above_was_hidden = False + return x, y + C.proc_h*(1 if n_highlighted_events <= 0 else 2) - if not proc.draw: - ctx.proc_above_was_hidden = True - child_y = y - else: - n_highlighted_events = draw_process(ctx, proc, proc_tree, x, y+USER_HALF, w) - if ctx.proc_above_was_hidden: - draw_hidden_process_separator(ctx, y+USER_HALF) - ctx.proc_above_was_hidden = False - child_y = y + C.proc_h*(1 if n_highlighted_events <= 0 else 2) + x, child_y = draw_process_and_separator(ctx, proc, proc_tree, y) + + if proc.lwp_list is not None: + for lwp in proc.lwp_list: + x, child_y = draw_process_and_separator(ctx, lwp, proc_tree, child_y) elder_sibling_y = None for child in proc.child_list: + # Processes draw their "own" LWPs, contrary to formal parentage relationship + if child.lwp_list is None: + continue + child_x, next_y = draw_processes_recursively(ctx, child, proc_tree, child_y) if proc.draw and child.draw: # draw upward from child to elder sibling or parent (proc) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 71805f6..00e9cc3 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -50,12 +50,16 @@ def __init__(self, options): self.parent_map = None self.mem_stats = None - # Read in all files, parse each into a time-ordered list + # Read in all files, parse each into a time-ordered list, init many of the attributes above parse_paths (self, options.paths, options) - # support deprecated data sets that contain no proc_ps.log, only proc_ps_threads.log + # FIXME: support deprecated data sets that contain no proc_ps.log, only proc_ps_threads.log if not self.ps_stats: self.ps_stats = self.ps_threads_stats + elif self.ps_threads_stats: + for (k,v) in self.ps_threads_stats.process_map.iteritems(): + self.ps_stats.process_map[k] = v + self.ps_threads_stats = True if not self.valid(): raise ParseError("empty state: '%s' does not contain a valid bootchart" % ", ".join(paths)) @@ -285,7 +289,7 @@ def _save_PC_sample(trace, es, time, pid, tid, comm, addr): # 2.1 thread continues (tid in processMap) # 2.2 thread starts def _handle_sample(options, trace, processMap, ltime, time, - pid, tid, cmd, state, ppid, userCpu, sysCpu, + pid, tid, lwp, cmd, state, ppid, userCpu, sysCpu, kstkeip, wchan, delayacct_blkio_ticks, c_user, c_sys, starttime, num_cpus): assert(type(c_user) is IntType) @@ -300,7 +304,7 @@ def _handle_sample(options, trace, processMap, ltime, time, writer.status("time (%dcs) < starttime (%dcs), diff %d -- TID %d" % (time, starttime, time-starttime, tid/1000)) - proc = Process(pid, tid, cmd, ppid, starttime) + proc = Process(pid, tid, lwp, cmd, ppid, starttime) if ltime: # process is starting during profiling run proc.user_cpu_ticks[0] = 0 proc.sys_cpu_ticks [0] = 0 @@ -468,7 +472,7 @@ def _parse_proc_ps_log(options, trace, file, num_cpus): pid *= 1000 ppid *= 1000 processMap = _handle_sample(options, trace, processMap, ltime, time, - pid, pid, cmd, state, ppid, + pid, pid, False, cmd, state, ppid, userCpu, sysCpu, kstkeip, wchan, delayacct_blkio_ticks, c_user, c_sys, starttime, num_cpus) if ltime: @@ -533,13 +537,13 @@ def _parse_proc_ps_threads_log(options, trace, file): assert(type(c_sys) is IntType) starttime = int(tokens[13+offset]) - # magic fixed point-ness ... - pid *= 1000 - tid *= 1000 + # force sorting later than whole-process records from proc_ps.log + pid = pid * PID_SCALE + LWP_OFFSET + tid = tid * PID_SCALE + LWP_OFFSET ppid *= 1000 processMap = _handle_sample(options, trace, processMap, ltime, time, - pid, tid, cmd, state, ppid, + pid, tid, True, cmd, state, ppid, userCpu, sysCpu, kstkeip, wchan, delayacct_blkio_ticks, c_user, c_sys, starttime, 1) ltime = time @@ -568,7 +572,7 @@ def _parse_taskstats_log(file): for time, lines in timed_blocks: # we have no 'starttime' from taskstats, so prep 'init' if ltime is None: - process = Process(1, '[init]', 0, 0) + process = Process(1, '[init]', False, 0, 0) processMap[1000] = process ltime = time # continue @@ -601,7 +605,7 @@ def _parse_taskstats_log(file): else: process.cmd = cmd; else: - process = Process(pid, pid, cmd, ppid, time) + process = Process(pid, pid, False, cmd, ppid, time) processMap[pid] = process delta_cpu_ns = (float) (cpu_ns - process.last_cpu_ns) @@ -784,7 +788,7 @@ def _parse_dmesg(file): processMap = {} idx = 0 inc = 1.0 / 1000000 - kernel = Process(idx, idx, "k-boot", 0, 0.1) + kernel = Process(idx, idx, False, "k-boot", 0, 0.1) processMap['k-boot'] = kernel base_ts = False max_ts = 0 @@ -831,7 +835,7 @@ def _parse_dmesg(file): # print "match: '%s' ('%g') at '%s'" % (func, ppid, time_ms) name = func.split ('+', 1) [0] idx += inc - processMap[func] = Process(ppid + idx, ppid + idx, name, ppid, time_ms / 10) + processMap[func] = Process(ppid + idx, ppid + idx, False, name, ppid, time_ms / 10) elif type == "initcall": # print "finished: '%s' at '%s'" % (func, time_ms) if func in processMap: diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index 98b9930..d872a9a 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -16,6 +16,7 @@ MAX_PID=100000 from . import writer +from samples import PID_SCALE, LWP_OFFSET def sort_func(proc): return long(proc.pid) / 1000 * MAX_PID + proc.tid / 1000 @@ -58,6 +59,14 @@ def __init__(self, kernel, ps_stats, sample_period, self.process_list = sorted(process_list, key = sort_func) self.sample_period = sample_period + # LWPs get appended to a list in their ps_stats Process + for proc in self.process_list: + if proc.lwp(): + ps_stats.process_map[proc.pid / PID_SCALE * PID_SCALE].lwp_list.append(proc) + for proc in self.process_list: + if not proc.lwp(): + proc.lwp_list.sort(key = sort_func) + self.build() if not accurate_parentage: self.update_ppids_for_daemons(self.process_list) diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index e7fcfec..c638463 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -21,6 +21,7 @@ # To understand this, see comment "Fedora hack" PID_SCALE = 1000 +LWP_OFFSET = 1 class EventSource: """ Extract (EventSample)s from some disjoint subset of the available log entries """ @@ -181,9 +182,11 @@ def __init__(self, process_map, sample_count, sample_period): writer.info ("process list size: %d" % len (self.process_map.values())) class Process: - def __init__(self, pid, tid, cmd, ppid, start_time): + def __init__(self, pid, tid, lwp, cmd, ppid, start_time): self.pid = pid self.tid = tid + assert(type(lwp) is BooleanType) + self.lwp_list = None if lwp else [] self.exe = cmd # may be overwritten, later self.args = [] self.ppid = ppid @@ -211,6 +214,10 @@ def __init__(self, pid, tid, cmd, ppid, start_time): # dynamic, view-dependent per-process state boolean self.draw = True + # Is this an LWP a/k/a pthread? + def lwp(self): + return self.lwp_list == None + def cpu_tick_count_during_run(self): ''' total CPU clock ticks reported for this process during the profiling run''' return self.user_cpu_ticks[-1] + self.sys_cpu_ticks[-1] \ @@ -251,8 +258,11 @@ def calc_load(self, userCpu, sysCpu, delayacct_blkio_ticks, interval, num_cpus): def set_parent(self, processMap): if self.ppid != None: self.parent = processMap.get (self.ppid) + # FIXME: support deprecated data sets that contain no proc_ps.log, only proc_ps_threads.log + if self.parent == None: + self.parent = processMap.get (self.ppid + LWP_OFFSET) if self.parent == None and self.pid / 1000 > 1 and \ - not (self.ppid == 2000 or self.pid == 2000): # kernel threads: ppid=2 + not (self.ppid/PID_SCALE == 2 or self.pid/PID_SCALE == 2): # kernel threads: ppid=2 writer.warn("Missing CONFIG_PROC_EVENTS: no parent for pid '%i' ('%s') with ppid '%i'" \ % (self.pid,self.cmd,self.ppid)) From 3a170148f5e413742e9aefeba8703613c6fc1b50 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Thu, 6 Dec 2012 15:23:47 -0800 Subject: [PATCH 174/182] warning messages demoted to info --- pybootchartgui/draw.py | 2 +- pybootchartgui/parsing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index c10843a..1c5acbe 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -281,7 +281,7 @@ def copy_if_enabled(color_list, re_index): if key in p_map: p_map[key].events.append(ev) else: - writer.warn("no samples of /proc/%d/task/%d/stat found -- event lost:\n\t%s" % + writer.info("no samples of /proc/%d/task/%d/stat found -- event lost:\n\t%s" % (ev.pid, ev.tid, ev.raw_log_line)) # Strip out from per-process lists any events not selected by a regexp. diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 00e9cc3..f4ea605 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -301,7 +301,7 @@ def _handle_sample(options, trace, processMap, ltime, time, else: if time < starttime: # large values signify a collector problem, e.g. resource starvation - writer.status("time (%dcs) < starttime (%dcs), diff %d -- TID %d" % + writer.info("time (%dcs) < starttime (%dcs), diff %d -- TID %d" % (time, starttime, time-starttime, tid/1000)) proc = Process(pid, tid, lwp, cmd, ppid, starttime) From 6d48370c40bea064a6a0627c04a847618625878e Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Tue, 4 Dec 2012 22:32:16 -0800 Subject: [PATCH 175/182] tweak defaults --- pybootchartgui/parsing.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index f4ea605..cc8f09c 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -103,7 +103,7 @@ def __init__(self, options): def _generate_sample_start_pseudo_events(self, options): es = EventSource(" ~ bootchartd sample start points", "", "") # empty regex es.parsed = [] - es.enable = True + es.enable = False init_pid = 1 for cpu in self.cpu_stats: # assign to the init process's bar, for lack of any better @@ -426,17 +426,14 @@ def _distribute_belatedly_reported_delayacct_blkio_ticks(processMap): io_acc = s.cpu_sample.io + s.cpu_sample.user + s.cpu_sample.sys - 1.0 s.cpu_sample.io -= io_acc -def _init_pseudo_EventSource_for_PC_samples(name): - es = EventSource(name, "", "") # empty regex - es.enable = False - es.parsed = [] - return es - def _init_pseudo_EventSources_for_PC_samples(options): - for label in [" ~ current PC, if thread Runnable", - " ~ current PC, if thread in non-interruptible D-wait", - " ~ kernel function wchan, if thread in non-interruptible D-wait"]: - es = _init_pseudo_EventSource_for_PC_samples(label) + for label, enable in [ + (" ~ current PC, if thread Runnable", False), + (" ~ current PC, if thread in non-interruptible D-wait", False), + (" ~ kernel function wchan, if thread in non-interruptible D-wait", True)]: + es = EventSource(label, "", "") # empty regex + es.enable = enable + es.parsed = [] options.event_source[label] = es # XX options.event_source must be used because the Trace object is not yet instantiated def _parse_proc_ps_log(options, trace, file, num_cpus): From 230e4af657cae14403e7e56adca2f1bdec581a0f Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Tue, 4 Dec 2012 23:34:41 -0800 Subject: [PATCH 176/182] FIX show lwps only for kernel threads --- pybootchartgui/draw.py | 9 +++++++-- pybootchartgui/samples.py | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 1c5acbe..4999dbe 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -22,7 +22,7 @@ import collections import traceback # debug -from samples import IOStat, EventSample, PID_SCALE, LWP_OFFSET +from samples import IOStat, EventSample, PID_SCALE, LWP_OFFSET, KTHREADD_PID from . import writer # Constants: Put the more heavily used, non-derived constants in a named tuple, for immutability. @@ -1043,7 +1043,12 @@ def draw_process_and_separator(ctx, proc, proc_tree, y): ctx.proc_above_was_hidden = False return x, y + C.proc_h*(1 if n_highlighted_events <= 0 else 2) - x, child_y = draw_process_and_separator(ctx, proc, proc_tree, y) + # Hide proc_ps_stat.log "processes" for the kernel-daemon children of kthreadd, provided that + # corresponding lwp entries are available from proc_ps_thread.log + if proc.ppid / PID_SCALE != KTHREADD_PID or not proc.lwp_list: + x, child_y = draw_process_and_separator(ctx, proc, proc_tree, y) + else: + x, child_y = 0, y if proc.lwp_list is not None: for lwp in proc.lwp_list: diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index c638463..182f97d 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -23,6 +23,8 @@ PID_SCALE = 1000 LWP_OFFSET = 1 +KTHREADD_PID = 2 + class EventSource: """ Extract (EventSample)s from some disjoint subset of the available log entries """ def __init__(self, label, filename, regex): From ecc9b266611ebe8a9ac1948cb24ea2c7a1c809b5 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 5 Dec 2012 12:36:02 -0800 Subject: [PATCH 177/182] draw -- show niceness --- pybootchartgui/draw.py | 1 + pybootchartgui/parsing.py | 12 ++++++++---- pybootchartgui/samples.py | 3 +++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 4999dbe..b873632 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -998,6 +998,7 @@ def draw_process(ctx, proc, proc_tree, x, y, w): prefix += str(proc.pid / 1000) if proc.lwp(): prefix += ":" + str(proc.tid / 1000) + prefix += ":" + str(proc.nice) prefix += "]" cmdString = prefix + cmdString if ctx.app_options.show_all and proc.args: diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index cc8f09c..cba3fc9 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -289,7 +289,7 @@ def _save_PC_sample(trace, es, time, pid, tid, comm, addr): # 2.1 thread continues (tid in processMap) # 2.2 thread starts def _handle_sample(options, trace, processMap, ltime, time, - pid, tid, lwp, cmd, state, ppid, userCpu, sysCpu, + pid, tid, lwp, nice, cmd, state, ppid, userCpu, sysCpu, kstkeip, wchan, delayacct_blkio_ticks, c_user, c_sys, starttime, num_cpus): assert(type(c_user) is IntType) @@ -297,7 +297,6 @@ def _handle_sample(options, trace, processMap, ltime, time, if tid in processMap: proc = processMap[tid] - proc.set_cmd(cmd) else: if time < starttime: # large values signify a collector problem, e.g. resource starvation @@ -320,6 +319,9 @@ def _handle_sample(options, trace, processMap, ltime, time, proc.delayacct_blkio_ticks[-1] = proc.delayacct_blkio_ticks[0] processMap[tid] = proc # insert new process into the dict + proc.set_cmd(cmd) # over-write values from earlier samples + proc.set_nice(nice) # over-write values from earlier samples + userCpuLoad, sysCpuLoad, delayacctBlkioLoad = proc.calc_load(userCpu, sysCpu, delayacct_blkio_ticks, max(1, time - ltime), num_cpus) @@ -460,6 +462,7 @@ def _parse_proc_ps_log(options, trace, file, num_cpus): pid, cmd, state, ppid = int(tokens[0]), ' '.join(tokens[1:2+offset]), tokens[2+offset], int(tokens[3+offset]) userCpu, sysCpu = int(tokens[13+offset]), int(tokens[14+offset]), c_user, c_sys = int(tokens[15+offset]), int(tokens[16+offset]) + nice = int(tokens[18+offset]) starttime = int(tokens[21+offset]) kstkeip = int(tokens[29+offset]) wchan = int(tokens[34+offset]) @@ -469,7 +472,7 @@ def _parse_proc_ps_log(options, trace, file, num_cpus): pid *= 1000 ppid *= 1000 processMap = _handle_sample(options, trace, processMap, ltime, time, - pid, pid, False, cmd, state, ppid, + pid, pid, False, nice, cmd, state, ppid, userCpu, sysCpu, kstkeip, wchan, delayacct_blkio_ticks, c_user, c_sys, starttime, num_cpus) if ltime: @@ -526,6 +529,7 @@ def _parse_proc_ps_threads_log(options, trace, file): pid, tid, cmd, state, ppid = int(tokens[0]), int(tokens[1]), ' '.join(tokens[2:3+offset]), tokens[3+offset], int(tokens[4+offset]) userCpu, sysCpu = int(tokens[7+offset]), int(tokens[8+offset]) c_user, c_sys = int(tokens[9+offset]), int(tokens[10+offset]) + nice = int(tokens[12+offset]) kstkeip = int(tokens[14+offset]) wchan = int(tokens[15+offset]) delayacct_blkio_ticks = int(tokens[17+offset]) if len(tokens) == 18+offset else 0 @@ -540,7 +544,7 @@ def _parse_proc_ps_threads_log(options, trace, file): ppid *= 1000 processMap = _handle_sample(options, trace, processMap, ltime, time, - pid, tid, True, cmd, state, ppid, + pid, tid, True, nice, cmd, state, ppid, userCpu, sysCpu, kstkeip, wchan, delayacct_blkio_ticks, c_user, c_sys, starttime, 1) ltime = time diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 182f97d..81da346 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -279,3 +279,6 @@ def set_cmd(self, cmd): # this by carving out an exception for all children of 'kthreadd' kthreadd_pid = 2 * PID_SCALE self.cmd = cmd.split('/')[-1] if self.ppid != kthreadd_pid else cmd + + def set_nice(self, nice): + self.nice = nice From bfc3da5aea735d8e6bf413f3a68e022dfbee0c43 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 12 Dec 2012 13:30:15 -0800 Subject: [PATCH 178/182] draw legend for process and thread labels --- pybootchartgui/draw.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index b873632..51b564a 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -356,6 +356,7 @@ def draw_process_label_in_box(self, color, bg_color, label, # XX ugly magic constants, tuned by trial-and-error draw_fill_rect(self.cr, bg_color, (label_x, y-1, label_w, -(C.proc_h-2))) draw_text(self.cr, label, color, label_x, y-4) + return label_x+label_w # XX Should be "_csec_to_user" # Assume off_x translation is already applied, as it will be in drawing functions. @@ -855,8 +856,26 @@ def draw_vertical(ctx, time, x): x_itime[1] + ctx.n_WIDTH/2) def draw_process_bar_chart_legends(ctx, curr_y): - curr_y += 30 - curr_x = 10 + C.legend_indent + max(0, ctx.cr.device_to_user(0, 0)[0]) + x_onscreen = max(0, ctx.cr.device_to_user(0, 0)[0]) + + def draw_process_or_thread_legend(color, label, x, y): + label_w = ctx.cr.text_extents(label)[2] + return ctx.draw_process_label_in_box( + PROC_TEXT_COLOR, + color, + label, + x, y, + -1, + ctx.cr.device_to_user(0, 0)[0], + ctx.cr.clip_extents()[2]) + + curr_y += 10 + curr_x = draw_process_or_thread_legend(NOTEPAD_PINK, "[PPID:PID]", x_onscreen, curr_y) + curr_y += 16 + curr_x = draw_process_or_thread_legend(NOTEPAD_YELLOW, "[PPID:PID:TID:NICE]", x_onscreen, curr_y) + + curr_y += 15 + curr_x += C.legend_indent curr_x += 30 + draw_legend_diamond (ctx.cr, "Runnable", PROCS_RUNNING_COLOR, curr_x, curr_y, C.leg_s*3/4, C.proc_h) curr_x += 30 + draw_legend_diamond (ctx.cr, "Uninterruptible Syscall", From 065e497c404f5b89ce4a320ae35e4ec4444c9cfe Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 5 Dec 2012 20:58:11 -0800 Subject: [PATCH 179/182] deprecate datasets containing LWP stats only -- proc_ps_threads --- pybootchartgui/parsing.py | 6 ++---- pybootchartgui/samples.py | 3 --- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index cba3fc9..cb50af0 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -53,10 +53,8 @@ def __init__(self, options): # Read in all files, parse each into a time-ordered list, init many of the attributes above parse_paths (self, options.paths, options) - # FIXME: support deprecated data sets that contain no proc_ps.log, only proc_ps_threads.log - if not self.ps_stats: - self.ps_stats = self.ps_threads_stats - elif self.ps_threads_stats: + assert self.ps_stats, "data sets that contain no proc_ps.log, only proc_ps_threads.log are now deprecated" + if self.ps_threads_stats: for (k,v) in self.ps_threads_stats.process_map.iteritems(): self.ps_stats.process_map[k] = v self.ps_threads_stats = True diff --git a/pybootchartgui/samples.py b/pybootchartgui/samples.py index 81da346..cd71216 100644 --- a/pybootchartgui/samples.py +++ b/pybootchartgui/samples.py @@ -260,9 +260,6 @@ def calc_load(self, userCpu, sysCpu, delayacct_blkio_ticks, interval, num_cpus): def set_parent(self, processMap): if self.ppid != None: self.parent = processMap.get (self.ppid) - # FIXME: support deprecated data sets that contain no proc_ps.log, only proc_ps_threads.log - if self.parent == None: - self.parent = processMap.get (self.ppid + LWP_OFFSET) if self.parent == None and self.pid / 1000 > 1 and \ not (self.ppid/PID_SCALE == 2 or self.pid/PID_SCALE == 2): # kernel threads: ppid=2 writer.warn("Missing CONFIG_PROC_EVENTS: no parent for pid '%i' ('%s') with ppid '%i'" \ From 325f2ddb7443a60a954e36f5cd0575e71195b832 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Wed, 12 Dec 2012 13:30:21 -0800 Subject: [PATCH 180/182] support proc_ps_compact.log --- pybootchartgui/parsing.py | 18 ++++++++++++------ pybootchartgui/process_tree.py | 6 +++++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index cb50af0..9e6ebe2 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -488,7 +488,7 @@ def _parse_proc_ps_log(options, trace, file, num_cpus): return ProcessStats (processMap, len (timed_blocks), avgSampleLength) -def _parse_proc_ps_threads_log(options, trace, file): +def _parse_proc_ps_threads_log(options, trace, file, lwp): """ * 0* pid -- inserted here from value in /proc/*pid*/task/. Not to be found in /proc/*pid*/task/*tid*/stat. * Not the same as pgrp, session, or tpgid. Refer to collector daemon source code for details. @@ -536,13 +536,17 @@ def _parse_proc_ps_threads_log(options, trace, file): assert(type(c_sys) is IntType) starttime = int(tokens[13+offset]) - # force sorting later than whole-process records from proc_ps.log - pid = pid * PID_SCALE + LWP_OFFSET - tid = tid * PID_SCALE + LWP_OFFSET + pid *= PID_SCALE + tid *= PID_SCALE + if lwp: + # force sorting later than whole-process records from /proc/PID/stat + pid += LWP_OFFSET + tid += LWP_OFFSET + ppid *= 1000 processMap = _handle_sample(options, trace, processMap, ltime, time, - pid, tid, True, nice, cmd, state, ppid, + pid, tid, lwp, nice, cmd, state, ppid, userCpu, sysCpu, kstkeip, wchan, delayacct_blkio_ticks, c_user, c_sys, starttime, 1) ltime = time @@ -989,8 +993,10 @@ def _do_parse(state, tf, name, file, options): state.parent_map = _parse_paternity_log(file) elif name == "proc_ps.log": # obsoleted by TASKSTATS state.ps_stats = _parse_proc_ps_log(options, state, file, state.num_cpus) + elif name == "proc_ps_compact.log": + state.ps_stats = _parse_proc_ps_threads_log(options, state, file, False) elif name == "proc_ps_threads.log" or name == "proc_ps_threads-2.log" : - state.ps_threads_stats = _parse_proc_ps_threads_log(options, state, file) + state.ps_threads_stats = _parse_proc_ps_threads_log(options, state, file, True) elif name == "kernel_pacct": # obsoleted by PROC_EVENTS state.parent_map = _parse_pacct(file) elif hasattr(options, "event_source"): diff --git a/pybootchartgui/process_tree.py b/pybootchartgui/process_tree.py index d872a9a..f5c3622 100644 --- a/pybootchartgui/process_tree.py +++ b/pybootchartgui/process_tree.py @@ -62,7 +62,11 @@ def __init__(self, kernel, ps_stats, sample_period, # LWPs get appended to a list in their ps_stats Process for proc in self.process_list: if proc.lwp(): - ps_stats.process_map[proc.pid / PID_SCALE * PID_SCALE].lwp_list.append(proc) + try: + ps_stats.process_map[proc.pid / PID_SCALE * PID_SCALE].lwp_list.append(proc) + except KeyError: + writer.info("LWP (thread) {0:d} dropped - no matching process found\n".format(proc.tid)) + self.process_list.remove(proc) for proc in self.process_list: if not proc.lwp(): proc.lwp_list.sort(key = sort_func) From c62b369d8c8f04db71bccd4c3dd0aa557c0d6830 Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Sat, 8 Dec 2012 11:15:11 -0800 Subject: [PATCH 181/182] FIX -- output was off by one line -- in dump raw context lines --- pybootchartgui/gui.py | 4 ++-- pybootchartgui/parsing.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index 5b24bfb..ea30ea9 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -572,14 +572,14 @@ def add_re(ec, callback_name): ''') # Stdout dropdown actiongroup.add_toggle_actions([ - ('dump raw', None, "dump raw context lines from log along with events", None, None, + ('dump raw', None, "dump raw log lines, with context", None, None, self.widget.dump_raw_event_context, drawctx.app_options.dump_raw_event_context), ]) actiongroup.add_actions([ ("Stdout", None, "Stdout", None, ""), ]) - # Stdout dropdown + # Help dropdown actiongroup.add_toggle_actions([ ('show legends', None, "show legends", None, None, self.widget.show_legends, drawctx.app_options.show_legends), diff --git a/pybootchartgui/parsing.py b/pybootchartgui/parsing.py index 9e6ebe2..7f47fe7 100644 --- a/pybootchartgui/parsing.py +++ b/pybootchartgui/parsing.py @@ -902,7 +902,7 @@ def parse_raw_log(state, boot_time_as_usecs_since_epoch, log_file, fields_re): except IndexError: comm = "" - raw_log_seek = log_file.tell() + raw_log_seek = log_file.tell() - len(line) boot_relative_usec = get_boot_relative_usec( state, boot_time_as_usecs_since_epoch, time_usec) From 3bf22c9809184769ccdeb3d0721f2ed96dd831bc Mon Sep 17 00:00:00 2001 From: Don Mullis Date: Sat, 8 Dec 2012 12:24:48 -0800 Subject: [PATCH 182/182] DISABLE raw log dump --- pybootchartgui/draw.py | 13 ++++++++++--- pybootchartgui/gui.py | 35 +++++++++++++++++++++-------------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/pybootchartgui/draw.py b/pybootchartgui/draw.py index 51b564a..1cd685d 100644 --- a/pybootchartgui/draw.py +++ b/pybootchartgui/draw.py @@ -797,13 +797,20 @@ def render(cr, ctx, xscale, trace, sweep_csec = None, hide_process_y = None): return print # blank, separator line - if ctx.app_options.dump_raw_event_context: + # FIXME: Support of raw log dump of multiple log files is hard -- disable for now. + if ctx.app_options.dump_raw_event_context and False: # dump all raw log lines between events, where "between" means merely textually, # irrespective of time. ctx.event_dump_list.sort(key = lambda e: e.raw_log_seek) if len(ctx.event_dump_list): - event0 = ctx.event_dump_list[0] - if event0.raw_log_seek: + # find first and last events of the list that contain a valid raw_log_seek + for event0 in ctx.event_dump_list: + if event0.raw_log_seek: + break + for eventN in reversed(ctx.event_dump_list): + if eventN.raw_log_seek: + break + if event0.raw_log_seek and eventN.raw_log_seek: event0.raw_log_file.seek(event0.raw_log_seek) eventN = ctx.event_dump_list[-1] # for line in event0.raw_log_file.readline(): diff --git a/pybootchartgui/gui.py b/pybootchartgui/gui.py index ea30ea9..4051164 100644 --- a/pybootchartgui/gui.py +++ b/pybootchartgui/gui.py @@ -557,29 +557,36 @@ def add_re(ec, callback_name): ("Event_Color", None, "Ev-Color", None, ""), ]) - # Stdout, Help dropdowns + # Stdout dropdown + # FIXME: Support of raw log dump of multiple log files is hard -- disable for now. + if False: + uimanager.add_ui_from_string( ''' + + + + + + + + ''') + actiongroup.add_toggle_actions([ + ('dump raw', None, "dump raw log lines, with context", None, None, + self.widget.dump_raw_event_context, drawctx.app_options.dump_raw_event_context), + ]) + actiongroup.add_actions([ + ("Stdout", None, "Stdout", None, ""), + ]) + + # Help dropdown uimanager.add_ui_from_string( ''' - - - ''') - # Stdout dropdown - actiongroup.add_toggle_actions([ - ('dump raw', None, "dump raw log lines, with context", None, None, - self.widget.dump_raw_event_context, drawctx.app_options.dump_raw_event_context), - ]) - actiongroup.add_actions([ - ("Stdout", None, "Stdout", None, ""), - ]) - - # Help dropdown actiongroup.add_toggle_actions([ ('show legends', None, "show legends", None, None, self.widget.show_legends, drawctx.app_options.show_legends),