#!/usr/bin/env python3 """ performance-log-viewer.py -- GIMP performance log viewer Copyright (C) 2018 Ell This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Usage: performance-log-viewer.py < infile """ import builtins, sys, os, math, statistics, bisect, functools, enum, re, \ subprocess from collections import namedtuple from xml.etree import ElementTree import gi gi.require_version ("Gdk", "3.0") gi.require_version ("Gtk", "3.0") from gi.repository import GLib, GObject, Gio, Gdk, Gtk, Pango def compose (head = None, *tail): return ( lambda *args, **kwargs: head (compose (*tail) (*args, **kwargs)) ) if tail else head or (lambda x: x) def div (x, y): return x / y if y else \ +math.inf if x > 0 else \ -math.inf if x < 0 else \ None def format_float (x): return "%g" % (round (100 * x) / 100) def format_percentage (x, digits = 0): return "%%.%df%%%%" % digits % (100 * x) def format_size (size): return GLib.format_size_full (size, GLib.FormatSizeFlags.IEC_UNITS) def format_duration (t): return "%02d:%02d:%02d.%02d" % (int (t / 3600), int (t / 60) % 60, int (t % 60), round (100 * t) % 100) def format_color (color): return "#%02x%02x%02x" % tuple ( map (lambda x: min (max (round (255 * x), 0), 255), color) ) def is_bright_color (color): return max (tuple (color)[0:3]) > 0.5 def blend_colors (color1, color2, amount): color1 = tuple (color1) color2 = tuple (color2) a1 = color1[-1] a2 = color2[-1] a = (1 - amount) * a1 + amount * a2 return tuple (a and ((1 - amount) * a1 * c1 + amount * a2 * c2) / a for c1, c2 in zip (color1[:-1], color2[:-1])) + (a,) def rounded_rectangle (cr, x, y, width, height, radius): radius = min (radius, width / 2, height / 2) cr.arc (x + radius, y + radius, radius, -math.pi, -math.pi / 2) cr.rel_line_to (width - 2 * radius, 0) cr.arc (x + width - radius, y + radius, radius, -math.pi / 2, 0) cr.rel_line_to (0, height - 2 * radius) cr.arc (x + width - radius, y + height - radius, radius, 0, math.pi / 2) cr.rel_line_to (-(width - 2 * radius), 0) cr.arc (x + radius, y + height - radius, radius, math.pi / 2, math.pi) cr.rel_line_to (0, -(height - 2 * radius)) cr.close_path () def get_basename (path): match = re.fullmatch (".*[\\\\/](.+?)[\\\\/]?", path) return match[1] if match else path search_path = list (filter ( bool, os.environ.get ("PERFORMANCE_LOG_VIEWER_PATH", ".").split (":") )) editor_command = os.environ.get ("PERFORMANCE_LOG_VIEWER_EDITOR", "xdg-open {file}") editor_command += " &" def find_file (filename): def lookup (filename): filename = re.sub ("[\\\\/]", GLib.DIR_SEPARATOR_S, filename) if GLib.path_is_absolute (filename): file = Gio.File.new_for_path (filename) if file.query_exists (): return file for path in search_path: rest = filename while rest: file = Gio.File.new_for_path (GLib.build_filenamev ((path, rest))) if file.query_exists (): return file sep = rest.find (GLib.DIR_SEPARATOR_S) rest = rest[sep + 1:] if sep >= 0 else "" return None if filename not in find_file.cache: find_file.cache[filename] = lookup (filename) return find_file.cache[filename] find_file.cache = {} def run_editor (file, line): subprocess.call (editor_command.format (file = "\"%s\"" % file.get_path (), line = line), shell = True) VariableType = namedtuple ("VariableType", ("parse", "format", "format_numeric")) var_types = { "boolean": VariableType ( parse = int, format = compose (str, bool), format_numeric = format_float ), "integer": VariableType ( parse = int, format = format_float, format_numeric = None ), "size": VariableType ( parse = int, format = format_size, format_numeric = None ), "size-ratio": VariableType ( parse = lambda x: div (*map (int, x.split ("/"))), format = format_percentage, format_numeric = None ), "int-ratio": VariableType ( parse = lambda x: div (*map (int, x.split (":"))), format = lambda x: "%g:%g" % ( (0, 0) if math.isnan (x) else (1, 0) if x == math.inf else (-1, 0) if x == -math.inf else (0, 1) if x == 0 else (round (100 * x) / 100, 1) if abs (x) > 1 else (1, round (100 / x) / 100) ), format_numeric = None ), "percentage": VariableType ( parse = float, format = format_percentage, format_numeric = None ), "duration": VariableType ( parse = float, format = format_duration, format_numeric = None ), "rate-of-change": VariableType ( parse = float, format = lambda x: "%s/s" % format_size (x), format_numeric = None ) } var_types = { type: VariableType ( parse = parse, format = lambda x, f = format: \ f (x) if x is not None else "N/A", format_numeric = lambda x, f = format_numeric or format: f (x) if x is not None else "N/A" ) for type, (parse, format, format_numeric) in var_types.items () } # Read performance log from STDIN log = ElementTree.fromstring (sys.stdin.buffer.read ()) Variable = namedtuple ("Variable", ("type", "desc", "color")) Value = namedtuple ("Value", ("value", "raw")) var_colors = [ (0.8, 0.4, 0.4), (0.8, 0.6, 0.4), (0.4, 0.8, 0.4), (0.8, 0.8, 0.4), (0.4, 0.4, 0.8), (0.4, 0.8, 0.8), (0.8, 0.4, 0.8), (0.8, 0.8, 0.8) ] var_defs = {} for var in log.find ("var-defs"): color = var_colors[len (var_defs) % len (var_colors)] var_defs[var.get ("name")] = Variable (var.get ("type"), var.get ("desc"), color) AddressInfo = namedtuple ("AddressInfo", ("id", "name", "object", "symbol", "offset", "source", "line")) address_map = {} if log.find ("address-map"): for address in log.find ("address-map").iterfind ("address"): value = int (address.get ("value"), 0) object = address.find ("object").text symbol = address.find ("symbol").text base = address.find ("base").text source = address.find ("source").text line = address.find ("line").text address_map[value] = AddressInfo ( id = int (base, 0) if base else value, name = symbol or base or hex (value), object = object, symbol = symbol, offset = value - int (base, 0) if base else None, source = source, line = int (line) if line else None ) class ThreadState (enum.Enum): SUSPENDED = enum.auto () RUNNING = enum.auto () def __str__ (self): return { ThreadState.SUSPENDED: "S", ThreadState.RUNNING: "R" }[self] Thread = namedtuple ("Thread", ("id", "name", "state", "frames")) Frame = namedtuple ("Frame", ("id", "address", "info")) Sample = namedtuple ("Sample", ("t", "vars", "markers", "backtrace")) Marker = namedtuple ("Marker", ("id", "t", "description")) samples = [] markers = [] last_marker = 0 for element in log.find ("samples"): if element.tag == "sample": sample = Sample ( t = int (element.get ("t")), vars = {}, markers = markers[last_marker:], backtrace = [] ) for var in element.find ("vars"): sample.vars[var.tag] = Value ( value = var_types[var_defs[var.tag].type].parse (var.text) \ if var.text else None, raw = var.text.strip () if var.text else None ) if element.find ("backtrace"): for thread in element.find ("backtrace").iterfind ("thread"): id = thread.get ("id") name = thread.get ("name") running = thread.get ("running") t = Thread ( id = int (id), name = name, state = ThreadState.RUNNING if running and int (running) \ else ThreadState.SUSPENDED, frames = [] ) for frame in thread.iterfind ("frame"): address = int (frame.get ("address"), 0) info = address_map.get (address, None) if not info: info = AddressInfo ( id = address, name = hex (address), object = None, symbol = None, offset = None, source = None, line = None ) t.frames.append (Frame ( id = len (t.frames), address = address, info = info )) sample.backtrace.append (t) samples.append (sample) last_marker = len (markers) elif element.tag == "marker": marker = Marker ( id = int (element.get ("id")), t = int (element.get ("t")), description = element.text.strip () if element.text else None ) markers.append (marker) if samples: samples[-1].markers.extend (markers[last_marker:]) DELTA_SAME = __builtins__.object () def delta_encode (dest, src): if type (dest) == type (src): if dest == src: return DELTA_SAME elif type (dest) == tuple: return tuple (delta_encode (d, s) for d, s in zip (dest, src)) + \ dest[len (src):] return dest def delta_decode (dest, src): if dest == DELTA_SAME: return src elif type (dest) == type (src): if type (dest) == tuple: return tuple (delta_decode (d, s) for d, s in zip (dest, src)) + \ dest[len (src):] return dest class History (GObject.GObject): Source = namedtuple ("HistorySource", ("get", "set")) def __init__ (self): GObject.GObject.__init__ (self) self.sources = [] self.state = None self.undo_stack = [] self.redo_stack = [] self.blocked = 0 self.n_groups = 0 self.pending_record = False @GObject.Property (type = bool, default = False) def can_undo (self): return bool (self.undo_stack) @GObject.Property (type = bool, default = False) def can_redo (self): return bool (self.redo_stack) def add_source (self, get, set): self.sources.append (self.Source (get, set)) def block (self): self.blocked += 1 def unblock (self): self.blocked -= 1 def is_blocked (self): return self.blocked > 0 def start_group (self): self.n_groups += 1 def end_group (self): self.n_groups -= 1 if self.n_groups == 0 and self.pending_record: self.record () def record (self): if self.is_blocked (): return if self.n_groups == 0: state = tuple (source.get () for source in self.sources) if self.state is None: self.state = state else: self.pending_record = False delta = delta_encode (self.state, state) if delta == DELTA_SAME: return self.undo_stack.append (delta_encode (self.state, state)) self.redo_stack = [] self.state = state self.notify ("can-undo") self.notify ("can-redo") else: self.pending_record = True def update (self): if self.is_blocked (): return if self.n_groups == 0: state = tuple (source.get () for source in self.sources) for stack in self.undo_stack, self.redo_stack: if stack: stack[-1] = delta_encode (delta_decode (stack[-1], self.state), state) self.state = state else: self.pending_record = True def move (self, src, dest): self.block () state = src.pop () for source, substate, prev_substate in \ zip (self.sources, self.state, state): if prev_substate != DELTA_SAME: source.set (delta_decode (prev_substate, substate)) state = delta_decode (state, self.state) dest.append (delta_encode (self.state, state)) self.state = state self.notify ("can-undo") self.notify ("can-redo") self.unblock () def undo (self): self.move (self.undo_stack, self.redo_stack) def redo (self): self.move (self.redo_stack, self.undo_stack) history = History () class SelectionOp (enum.Enum): REPLACE = enum.auto () ADD = enum.auto () SUBTRACT = enum.auto () INTERSECT = enum.auto () XOR = enum.auto () class Selection (GObject.GObject): __gsignals__ = { "changed": (GObject.SignalFlags.RUN_FIRST, None, ()), "change-complete": (GObject.SignalFlags.RUN_FIRST, None, ()), "highlight-changed": (GObject.SignalFlags.RUN_FIRST, None, ()) } def __init__ (self, iter = ()): GObject.GObject.__init__ (self) self.selection = set (iter) self.highlight = None self.cursor = None self.cursor_dir = 0 self.pending_change_completion = False def __eq__ (self, other): return type (self) == type (other) and \ self.selection == other.selection and \ self.cursor == other.cursor and \ self.cursor_dir == other.cursor_dir def __str__ (self): n_sel = len (self.selection) if n_sel == 0 or n_sel == len (samples): return "All Samples" elif n_sel == 1: i, = self.selection return "Sample %d" % i else: sel = list (self.selection) sel.sort () if all (sel[i] + 1 == sel[i + 1] for i in range (n_sel - 1)): return "Samples %d–%d" % (sel[0], sel[-1]) else: return "%d Samples" % n_sel def copy (self): selection = Selection () selection.highlight = self.highlight selection.cursor = self.cursor selection.cursor_dir = self.cursor_dir selection.selection = self.selection.copy () return selection def get_effective_selection (self): if self.selection: return self.selection else: return set (range (len (samples))) def select (self, selection, op = SelectionOp.REPLACE): if op == SelectionOp.REPLACE: self.selection = selection.copy () elif op == SelectionOp.ADD: self.selection |= selection elif op == SelectionOp.SUBTRACT: self.selection -= selection elif op == SelectionOp.INTERSECT: self.selection &= selection elif op == SelectionOp.XOR: self.selection.symmetric_difference_update (selection) if len (self.selection) == 1: (self.cursor,) = self.selection else: self.cursor = None self.cursor_dir = 0 self.pending_change_completion = True self.emit ("changed") def select_range (self, first, last, op = SelectionOp.REPLACE): if first > last: temp = first first = last last = temp first = max (first, 0) last = min (last, len (samples) - 1) if first <= last: self.select (set (range (first, last + 1)), op) else: self.select (set (), op) def clear (self): self.select (set ()) def invert (self): self.select_range (0, len (samples), SelectionOp.XOR) def change_complete (self): if self.pending_change_completion: self.pending_change_completion = False history.start_group () history.record () self.emit ("change-complete") history.end_group () def set_highlight (self, highlight): self.highlight = highlight self.emit ("highlight-changed") def source_get (self): return self.copy () def source_set (self, selection): self.cursor = selection.cursor self.cursor_dir = selection.cursor_dir self.selection = selection.selection.copy () self.emit ("changed") self.emit ("change-complete") def add_history_source (self): history.add_source (self.source_get, self.source_set) selection = Selection () selection.add_history_source () class FindSamplesPopover (Gtk.Popover): def __init__ (self, *args, **kwargs): Gtk.Popover.__init__ (self, *args, **kwargs) vbox = Gtk.Box (orientation = Gtk.Orientation.VERTICAL, border_width = 20, spacing = 8) self.add (vbox) vbox.show () entry = Gtk.Entry (width_chars = 40, placeholder_text = "Python expression") self.entry = entry vbox.pack_start (entry, False, False, 0) entry.show () entry.connect ("activate", self.find_samples) entry.get_buffer ().connect ( "notify::text", lambda *args: self.entry.get_style_context ().remove_class ("error") ) frame = Gtk.Frame (label = "Selection", shadow_type = Gtk.ShadowType.NONE) vbox.pack_start (frame, False, False, 8) frame.get_label_widget ().get_style_context ().add_class ("dim-label") frame.show () vbox2 = Gtk.Box (orientation = Gtk.Orientation.VERTICAL, border_width = 8, spacing = 8) frame.add (vbox2) vbox2.show () self.radios = [] radio = Gtk.RadioButton.new_with_mnemonic (None, "_Replace") self.radios.append ((radio, SelectionOp.REPLACE)) vbox2.pack_start (radio, False, False, 0) radio.show () radio = Gtk.RadioButton.new_with_mnemonic_from_widget (radio, "_Add") self.radios.append ((radio, SelectionOp.ADD)) vbox2.pack_start (radio, False, False, 0) radio.show () radio = Gtk.RadioButton.new_with_mnemonic_from_widget (radio, "_Subtract") self.radios.append ((radio, SelectionOp.SUBTRACT)) vbox2.pack_start (radio, False, False, 0) radio.show () radio = Gtk.RadioButton.new_with_mnemonic_from_widget (radio, "_Intersect") self.radios.append ((radio, SelectionOp.INTERSECT)) vbox2.pack_start (radio, False, False, 0) radio.show () button = Gtk.Button.new_with_mnemonic ("_Find") vbox.pack_start (button, False, False, 0) button.set_halign (Gtk.Align.CENTER) button.show () button.connect ("clicked", self.find_samples) def do_hide (self): self.entry.set_text ("") self.entry.get_style_context ().remove_class ("error") Gtk.Popover.do_hide (self) def find_samples (self, *args): def var_name (var): return var.replace ("-", "_") try: f = eval ("lambda thread, function, %s: %s" % ( ", ".join (map (var_name, var_defs)), self.entry.get_text ())) except: self.entry.get_style_context ().add_class ("error") return sel = set () for i in range (len (samples)): try: def match_thread (thread, id, state = None): return (id is None or \ (type (id) == int and \ id == thread.id) or \ (type (id) == str and \ thread.name and \ re.fullmatch (id, thread.name))) and \ (state is None or \ re.fullmatch (state, str (thread.state))) def thread (id, state = None): return any (match_thread (thread, id, state) for thread in samples[i].backtrace or []) def function (name, id = None, state = None): for thread in samples[i].backtrace or []: if match_thread (thread, id, state): for frame in thread.frames: if re.fullmatch (name, frame.info.name): return True return False if f (thread, function, **{ var_name (var): value.value for var, value in samples[i].vars.items () }): sel.add (i) except: pass op = [op for radio, op in self.radios if radio.get_active ()][0] selection.select (sel, op) selection.change_complete () self.hide () class CellRendererColorToggle (Gtk.CellRendererToggle): padding = 3 color = GObject.Property (type = Gdk.RGBA, default = Gdk.RGBA (0, 0, 0)) def do_render (self, cr, widget, background_area, cell_area, flags): state = widget.get_state_flags () style = widget.get_style_context () fg_color = style.get_color (state) active = self.get_property ("active") size = max (min (cell_area.width, cell_area.height) - 2 * self.padding, 0) (r, g, b, a) = self.color if is_bright_color (fg_color): bg = (0.75 * r, 0.75 * g, 0.75 * b) fg = (r, g, b) else: bg = (r, g, b) fg = (0.75 * r, 0.75 * g, 0.75 * b) x = cell_area.x + (cell_area.width - size) // 2 y = cell_area.y + (cell_area.height - size) // 2 if active: cr.rectangle (x, y, size, size) cr.set_source_rgba (*bg) cr.fill () else: style.save () style.set_state (Gtk.StateFlags (state & ~Gtk.StateFlags.SELECTED)) Gtk.render_background (style, cr, x, y, size, size) style.restore () cr.rectangle (x, y, size, size) cr.set_source_rgb (*fg) cr.set_line_width (2) cr.stroke () class VariableSet (Gtk.TreeView): class Store (Gtk.ListStore): NAME = 0 DESC = 1 COLOR = 2 ACTIVE = 3 def __init__ (self): Gtk.ListStore.__init__ (self, str, str, Gdk.RGBA, bool) for var, var_def in var_defs.items (): i = self.append ((var, var_def.desc, Gdk.RGBA (*var_def.color), False)) def __init__ (self, *args, **kwargs): Gtk.TreeView.__init__ (self, *args, headers_visible = False, **kwargs) store = self.Store () self.store = store self.set_model (store) self.set_tooltip_column (store.DESC) col = Gtk.TreeViewColumn () self.append_column (col) cell = CellRendererColorToggle () col.pack_start (cell, False) col.add_attribute (cell, "active", store.ACTIVE) col.add_attribute (cell, "color", store.COLOR) cell.connect ("toggled", self.var_toggled) cell = Gtk.CellRendererText () col.pack_start (cell, True) col.add_attribute (cell, "text", store.NAME) def var_toggled (self, cell, path): self.store[path][self.store.ACTIVE] = not cell.get_property ("active") class SampleGraph (Gtk.DrawingArea): def __init__ (self, model = None, *args, **kwargs): Gtk.DrawingArea.__init__ (self, *args, can_focus = True, **kwargs) self.style_widget = Gtk.Entry () self.model = model if model: model.connect ("row-changed", lambda *args: self.update ()) self.update () self.selection = None self.sel = None selection.connect ("changed", self.selection_changed) selection.connect ("highlight-changed", lambda selection: self.queue_draw ()) self.add_events (Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK) self.selection_changed (selection) def sample_to_x (self, i): if not samples: return None width = self.get_allocated_width () n_samples = max (len (samples), 2) return 1 + (width - 3) * i / (n_samples - 1) def sample_to_range (self, i): if not samples: return None width = self.get_allocated_width () n_samples = max (len (samples), 2) return (1 + math.floor ((width - 3) * (i - 0.5) / (n_samples - 1)), 1 + math.ceil ((width - 3) * (i + 0.5) / (n_samples - 1))) def x_to_sample (self, x): if not samples: return None width = max (self.get_allocated_width (), 4) n_samples = len (samples) return round ((n_samples - 1) * (x - 1) / (width - 3)) def update (self): if not samples or not self.model: return self.max_value = 1 for row in self.model: var_name = row[self.model.NAME] var_active = row[self.model.ACTIVE] if var_active: values = (sample.vars[var_name].value for sample in samples) values = filter (lambda x: x is not None, values) values = filter (math.isfinite, values) try: self.max_value = max (self.max_value, max (values)) except: pass self.queue_draw () def selection_changed (self, selection): if selection.selection: self.sel = list (selection.selection) self.sel.sort () else: self.sel = None self.queue_draw () def do_get_preferred_width (self): return (300, 300) def do_get_preferred_height (self): if self.model: return (32, 256) else: return (16, 16) def update_selection (self): sel = self.selection.copy () i0 = self.selection_i0 i1 = self.selection_i1 if self.selection_range: swap = i0 > i1 if swap: temp = i0 i0 = i1 i1 = temp n_samples = len (samples) while i0 > 0 and not samples[i0 - 1].markers: i0 -= 1 while i1 < n_samples - 1 and not samples[i1 + 1].markers: i1 += 1 if swap: temp = i0 i0 = i1 i1 = temp sel.select_range (i0, i1, self.selection_op) selection.select (sel.selection) selection.cursor = i1 selection.cursor_dir = i1 - i0 def do_button_press_event (self, event): state = event.state & Gdk.ModifierType.MODIFIER_MASK self.grab_focus () if event.button == 1: i = self.x_to_sample (event.x) if i is None: return False self.selection = selection.copy () self.selection_i0 = i self.selection_i1 = i self.selection_op = SelectionOp.REPLACE self.selection_range = event.type != Gdk.EventType.BUTTON_PRESS if state == Gdk.ModifierType.SHIFT_MASK: self.selection_op = SelectionOp.ADD elif state == Gdk.ModifierType.CONTROL_MASK: self.selection_op = SelectionOp.SUBTRACT elif state == (Gdk.ModifierType.SHIFT_MASK | Gdk.ModifierType.CONTROL_MASK): self.selection_op = SelectionOp.INTERSECT self.update_selection () self.grab_add () elif event.button == 3: if state == 0: selection.clear () elif state == Gdk.ModifierType.CONTROL_MASK: selection.invert () self.grab_add () return True def do_button_release_event (self, event): if event.button == 1 or event.button == 3: self.selection = None selection.change_complete () self.grab_remove () return True return False def do_motion_notify_event (self, event): i = self.x_to_sample (event.x) selection.set_highlight (i) if self.selection and i is not None: self.selection_i1 = i self.update_selection () return True return False def do_leave_notify_event (self, event): selection.set_highlight (None) return False def do_key_press_event (self, event): if event.keyval == Gdk.KEY_Left or \ event.keyval == Gdk.KEY_Right or \ event.keyval == Gdk.KEY_Home or \ event.keyval == Gdk.KEY_KP_Home or \ event.keyval == Gdk.KEY_End or \ event.keyval == Gdk.KEY_KP_End: n_samples = len (samples) state = event.state & Gdk.ModifierType.MODIFIER_MASK op = SelectionOp.REPLACE if state == Gdk.ModifierType.SHIFT_MASK: op = SelectionOp.XOR cursor = selection.cursor cursor_dir = selection.cursor_dir if event.keyval == Gdk.KEY_Left or \ event.keyval == Gdk.KEY_Home or \ event.keyval == Gdk.KEY_KP_Home: if selection.cursor is not None: if cursor_dir <= 0 or op == SelectionOp.REPLACE: cursor -= 1 else: cursor = n_samples - 1 cursor_dir = -1 elif event.keyval == Gdk.KEY_Right or \ event.keyval == Gdk.KEY_End or \ event.keyval == Gdk.KEY_KP_End: if cursor is not None: if cursor_dir >= 0 or op == SelectionOp.REPLACE: cursor += 1 else: cursor = 0 cursor_dir = +1 if cursor < 0 or cursor >= n_samples: cursor = min (max (cursor, 0), n_samples - 1) selection.cursor = cursor selection.cursor_dir = cursor_dir if op != SelectionOp.REPLACE: return True i0 = cursor if event.keyval == Gdk.KEY_Home or \ event.keyval == Gdk.KEY_KP_Home: cursor = 0 elif event.keyval == Gdk.KEY_End or \ event.keyval == Gdk.KEY_KP_End: cursor = n_samples - 1 if op == SelectionOp.REPLACE: i0 = cursor selection.select_range (i0, cursor, op) if len (selection.selection) > 1: selection.cursor = cursor selection.cursor_dir = cursor_dir return True elif event.keyval == Gdk.KEY_Escape: selection.select (set ()) return True return False def do_key_release_event (self, event): selection.change_complete () return False def do_draw (self, cr): state = self.get_state_flags () style = (self.style_widget if self.model else self).get_style_context () (width, height) = (self.get_allocated_width (), self.get_allocated_height ()) fg_color = tuple (style.get_color (state)) grid_color = (*fg_color[:3], 0.25 * fg_color[3]) highlight_color = grid_color selection_color = (*fg_color[:3], 0.15 * fg_color[3]) Gtk.render_background (style, cr, 0, 0, width, height) if self.model: max_value = self.max_value vscale = (height - 4) / max_value cr.save () cr.translate (0, height - 2) cr.scale (1, -1) first_sample = True has_infinite = False for row in self.model: var_name = row[self.model.NAME] var_active = row[self.model.ACTIVE] if var_active: is_boolean = var_defs[var_name].type == "boolean" is_continuous = not is_boolean for i in range (len (samples)): value = samples[i].vars[var_name].value if value is not None: first_sample = False if math.isinf (value): first_sample = True has_infinite = True value = max_value elif is_boolean: value *= max_value y = value * vscale if is_continuous: x = self.sample_to_x (i) if first_sample: cr.move_to (x, y) else: cr.line_to (x, y) else: (x0, x1) = self.sample_to_range (i) if first_sample: cr.move_to (x0, y) else: cr.line_to (x0, y) cr.line_to (x1, y) else: first_sample = True (r, g, b) = var_defs[var_name].color cr.set_source_rgb (r, g, b) cr.set_line_width (2) cr.stroke () if has_infinite: cr.save () for i in range (len (samples)): value = samples[i].vars[var_name].value if value is not None and math.isinf (value): first_sample = False y = max_value * vscale if is_continuous: x = self.sample_to_x (i) if first_sample: cr.move_to (x, y) else: cr.line_to (x, y) else: (x0, x1) = self.sample_to_range (i) if first_sample: cr.move_to (x0, y) else: cr.line_to (x0, y) cr.line_to (x1, y) else: first_sample = True cr.set_dash ([6, 6], 0) cr.stroke () cr.restore () cr.restore () cr.set_line_width (1) cr.set_source_rgba (*grid_color) n_hgrid_lines = 4 n_vgrid_lines = 1 for i in range (n_hgrid_lines + 1): cr.move_to (0, round (i * (height - 1) / n_hgrid_lines) + 0.5) cr.rel_line_to (width, 0) cr.stroke () for i in range (n_vgrid_lines + 1): cr.move_to (round (i * (width - 1) / n_vgrid_lines) + 0.5, 0) cr.rel_line_to (0, height) cr.stroke () else: for i in range (len (samples)): if samples[i].markers: (x0, x1) = self.sample_to_range (i) cr.rectangle (x0, 0, x1 - x0, height) cr.set_source_rgba (*fg_color) cr.fill () if selection.highlight is not None: (x0, x1) = self.sample_to_range (selection.highlight) cr.rectangle (x0, 0, x1 - x0, height) (r, g, b, a) = style.get_color (state) cr.set_source_rgba (*highlight_color) cr.fill () if self.sel: def draw_selection (): x0 = self.sample_to_range (i0)[0] x1 = self.sample_to_range (i1)[1] cr.rectangle (x0, 0, x1 - x0, height) (r, g, b, a) = style.get_color (state) cr.set_source_rgba (*selection_color) cr.fill () i0 = None for i in self.sel: if i0 is None: i0 = i i1 = i elif i == i1 + 1: i1 = i else: draw_selection () i0 = i i1 = i if i0 is not None: draw_selection () class SampleGraphList (Gtk.Box): Item = namedtuple ( "SampleGraphListGraph", ("widget", "model", "remove_button", "move_up_button", "move_down_button") ) def __init__ (self, *args, **kwargs): Gtk.Box.__init__ (self, *args, orientation = Gtk.Orientation.VERTICAL, **kwargs) self.items = [] self.vset_size_group = Gtk.SizeGroup ( mode = Gtk.SizeGroupMode.HORIZONTAL ) hbox = Gtk.Box (orientation = Gtk.Orientation.HORIZONTAL) self.pack_start (hbox, False, False, 0) hbox.show () empty = Gtk.DrawingArea () hbox.pack_start (empty, False, True, 0) self.vset_size_group.add_widget (empty) empty.show () graph = SampleGraph (has_tooltip = True) hbox.pack_start (graph, True, True, 0) graph.show () graph.connect ("query-tooltip", self.graph_query_tooltip) separator = Gtk.Separator (orientation = Gtk.Orientation.HORIZONTAL) self.pack_start (separator, False, False, 0) separator.show () vbox = Gtk.Box (orientation = Gtk.Orientation.VERTICAL) self.items_vbox = vbox self.pack_start (vbox, False, False, 0) vbox.show () self.add_item (0) def update_items (self): for widget in self.items_vbox.get_children (): self.items_vbox.remove (widget) i = 0 for item in self.items: if i > 0: separator = Gtk.Separator ( orientation = Gtk.Orientation.HORIZONTAL ) self.items_vbox.pack_start (separator, False, False, 0) separator.show () self.items_vbox.pack_start (item.widget, False, False, 0) item.remove_button.set_sensitive (len (self.items) > 1) item.move_up_button.set_sensitive (i > 0) item.move_down_button.set_sensitive (i < len (self.items) - 1) i += 1 def add_item (self, i): hbox = Gtk.Box (orientation = Gtk.Orientation.HORIZONTAL) hbox.show () vbox = Gtk.Box (orientation = Gtk.Orientation.VERTICAL) hbox.pack_start (vbox, False, True, 0) self.vset_size_group.add_widget (vbox) vbox.show () scroll = Gtk.ScrolledWindow ( hscrollbar_policy = Gtk.PolicyType.NEVER, vscrollbar_policy = Gtk.PolicyType.AUTOMATIC ) vbox.pack_start (scroll, True, True, 0) scroll.show () vset = VariableSet () scroll.add (vset) vset.show () buttons = Gtk.ButtonBox (orientation = Gtk.Orientation.HORIZONTAL) vbox.pack_start (buttons, False, False, 0) buttons.set_layout (Gtk.ButtonBoxStyle.EXPAND) buttons.show () button = Gtk.Button.new_from_icon_name ("list-add-symbolic", Gtk.IconSize.BUTTON) add_button = button buttons.add (button) button.show () button = Gtk.Button.new_from_icon_name ("list-remove-symbolic", Gtk.IconSize.BUTTON) remove_button = button buttons.add (button) button.show () button = Gtk.Button.new_from_icon_name ("go-up-symbolic", Gtk.IconSize.BUTTON) move_up_button = button buttons.add (button) button.show () button = Gtk.Button.new_from_icon_name ("go-down-symbolic", Gtk.IconSize.BUTTON) move_down_button = button buttons.add (button) button.show () graph = SampleGraph (vset.get_model (), has_tooltip = True) hbox.pack_start (graph, True, True, 0) graph.show () graph.connect ("query-tooltip", self.graph_query_tooltip) item = self.Item ( widget = hbox, model = vset.get_model (), remove_button = remove_button, move_up_button = move_up_button, move_down_button = move_down_button ) self.items.insert (i, item) add_button.connect ("clicked", lambda *args: self.add_item ( self.items.index (item) + 1 )) remove_button.connect ("clicked", lambda *args: self.remove_item ( self.items.index (item) )) move_up_button.connect ("clicked", lambda *args: self.move_item ( self.items.index (item), -1 )) move_down_button.connect ("clicked", lambda *args: self.move_item ( self.items.index (item), +1 )) self.update_items () def remove_item (self, i): del self.items[i] self.update_items () def move_item (self, i, offset): item = self.items[i] del self.items[i] self.items.insert (i + offset, item) self.update_items () def graph_query_tooltip (self, graph, x, y, keyboard_mode, tooltip): if keyboard_mode: return False i = graph.x_to_sample (x) if i is None or i < 0 or i >= len (samples): return False grid = Gtk.Grid (column_spacing = 4) tooltip.set_custom (grid) grid.show () row = 0 label = Gtk.Label () grid.attach (label, 0, row, 2, 1) label.set_markup ("Sample %d" % i) label.show () row += 1 label = Gtk.Label () grid.attach (label, 0, row, 2, 1) label.set_markup ("%s" % format_duration (samples[i].t / 1000000)) label.get_style_context ().add_class ("dim-label") label.show () row += 1 for item in self.items: model = item.model vars = tuple (var[model.NAME] for var in model if var[model.ACTIVE]) if not vars: continue separator = Gtk.Separator (orientation = Gtk.Orientation.HORIZONTAL) grid.attach (separator, 0, row, 2, 1) separator.show () row += 1 for var in vars: color = format_color (var_defs[var].color) label = Gtk.Label (halign = Gtk.Align.START) grid.attach (label, 0, row, 1, 1) label.set_markup ( "%s" % (color, var) ) label.show () value = samples[i].vars[var].value text = var_types[var_defs[var].type].format (value) \ if value is not None else "N/A" label = Gtk.Label (label = text, halign = Gtk.Align.END) grid.attach (label, 1, row, 1, 1) label.show () row += 1 markers = samples[i].markers if markers: separator = Gtk.Separator (orientation = Gtk.Orientation.HORIZONTAL) grid.attach (separator, 0, row, 2, 1) separator.show () row += 1 for marker in markers: label = Gtk.Label (halign = Gtk.Align.START) grid.attach (label, 0, row, 1, 1) label.set_markup ("Marker %d" % (marker.id)) label.show () if marker.description: label = Gtk.Label (marker.description, halign = Gtk.Align.END) grid.attach (label, 1, row, 1, 1) label.show () row += 1 return True class InformationViewer (Gtk.ScrolledWindow): class Store (Gtk.ListStore): NAME = 0 VALUE = 1 def __init__ (self): Gtk.ListStore.__init__ (self, str, str) def __init__ (self, *args, **kwargs): Gtk.ScrolledWindow.__init__ ( self, *args, hscrollbar_policy = Gtk.PolicyType.AUTOMATIC, vscrollbar_policy = Gtk.PolicyType.AUTOMATIC, **kwargs ) vbox = Gtk.Box (orientation = Gtk.Orientation.VERTICAL, border_width = 32, margin_left = 64, margin_right = 64, spacing = 32) self.add (vbox) vbox.show () def add_element (element): name = { "params": "Log Parameters", "gimp-version": "GIMP Version", "env": "Environment", "gegl-config": "GEGL Config" }.get (element.tag, element.tag) text = element.text.strip () n_items = len (element) if not text and n_items == 0: return vbox2 = Gtk.Box (orientation = Gtk.Orientation.VERTICAL, spacing = 16) vbox.pack_start (vbox2, False, False, 0) vbox2.show () label = Gtk.Label (xalign = 0) vbox2.pack_start (label, False, False, 0) label.set_markup ("%s" % name) label.show () frame = Gtk.Frame (shadow_type = Gtk.ShadowType.IN) vbox2.pack_start (frame, False, False, 0) frame.show () if text: scrolled = Gtk.ScrolledWindow ( hscrollbar_policy = Gtk.PolicyType.AUTOMATIC, vscrollbar_policy = Gtk.PolicyType.AUTOMATIC, height_request = 400 ) frame.add (scrolled) scrolled.show () text = Gtk.TextView (editable = False, monospace = True, wrap_mode = Gtk.WrapMode.WORD, left_margin = 16, right_margin = 16, top_margin = 16, bottom_margin = 16) scrolled.add (text) text.get_buffer ().set_text (element.text.strip (), -1) text.show () else: scrolled = Gtk.ScrolledWindow ( hscrollbar_policy = Gtk.PolicyType.AUTOMATIC, vscrollbar_policy = Gtk.PolicyType.NEVER ) frame.add (scrolled) scrolled.show () store = self.Store () for item in element: store.append ((item.tag, item.text.strip ())) tree = Gtk.TreeView (model = store) scrolled.add (tree) tree.show () col = Gtk.TreeViewColumn (title = "Name") tree.append_column (col) cell = Gtk.CellRendererText () col.pack_start (cell, False) col.add_attribute (cell, "text", store.NAME) col = Gtk.TreeViewColumn (title = "Value") tree.append_column (col) col.set_alignment (0.5) cell = Gtk.CellRendererText (xalign = 1) col.pack_start (cell, False) col.add_attribute (cell, "text", store.VALUE) params = log.find ("params") if params: add_element (params) info = log.find ("info") if info: for element in info: add_element (element) class MarkersViewer (Gtk.ScrolledWindow): class Store (Gtk.ListStore): ID = 0 TIME = 1 DESC = 2 def __init__ (self): Gtk.ListStore.__init__ (self, int, GObject.TYPE_INT64, str) for marker in markers: self.append ((marker.id, marker.t, marker.description)) def __init__ (self, *args, **kwargs): Gtk.Box.__init__ (self, *args, hscrollbar_policy = Gtk.PolicyType.AUTOMATIC, vscrollbar_policy = Gtk.PolicyType.AUTOMATIC, **kwargs) self.needs_update = True store = self.Store () self.store = store tree = Gtk.TreeView (model = store) self.tree = tree self.add (tree) tree.show () tree.get_selection ().set_mode (Gtk.SelectionMode.MULTIPLE) self.tree_selection_changed_handler = tree.get_selection ().connect ( "changed", self.tree_selection_changed ) col = Gtk.TreeViewColumn (title = "#") tree.append_column (col) col.set_resizable (True) cell = Gtk.CellRendererText (xalign = 1) col.pack_start (cell, False) col.add_attribute (cell, "text", store.ID) def format_time_col (tree_col, cell, model, iter, col): time = model[iter][col] cell.set_property ("text", format_duration (time / 1000000)) col = Gtk.TreeViewColumn (title = "Time") tree.append_column (col) col.set_resizable (True) col.set_alignment (0.5) cell = Gtk.CellRendererText (xalign = 1) col.pack_start (cell, False) col.set_cell_data_func (cell, format_time_col, store.TIME) col = Gtk.TreeViewColumn (title = "Description") tree.append_column (col) col.set_resizable (True) col.set_alignment (0.5) cell = Gtk.CellRendererText () col.pack_start (cell, False) col.add_attribute (cell, "text", store.DESC) col = Gtk.TreeViewColumn () tree.append_column (col) selection.connect ("change-complete", self.selection_change_complete) def update (self): markers = set () if not self.needs_update: return self.needs_update = False for i in selection.selection: markers.update (marker.id for marker in samples[i].markers) tree_sel = self.tree.get_selection () GObject.signal_handler_block (tree_sel, self.tree_selection_changed_handler) tree_sel.unselect_all () for row in self.store: if row[self.store.ID] in markers: tree_sel.select_iter (row.iter) GObject.signal_handler_unblock (tree_sel, self.tree_selection_changed_handler) def do_map (self): self.update () Gtk.ScrolledWindow.do_map (self) def selection_change_complete (self, selection): self.needs_update = True if self.get_mapped (): self.update () def tree_selection_changed (self, tree_sel): sel = set () for row in self.store: if tree_sel.iter_is_selected (row.iter): id = row[self.store.ID] for i in range (len (samples)): if any (marker.id == id for marker in samples[i].markers): sel.add (i) selection.select (sel) selection.change_complete () class VariablesViewer (Gtk.ScrolledWindow): class Store (Gtk.ListStore): NAME = 0 DESC = 1 COLOR = 2 VALUE = 3 RAW = 4 MIN = 5 MAX = 6 MEDIAN = 7 MEAN = 8 STDEV = 9 LAST_COLUMN = 10 def __init__ (self): n_stats = self.LAST_COLUMN - self.COLOR Gtk.ListStore.__init__ (self, *((str, str, Gdk.RGBA) + n_stats * (str,))) for var, var_def in var_defs.items (): self.append (((var, var_def.desc, Gdk.RGBA (*var_def.color)) + n_stats * ("",))) def __init__ (self, *args, **kwargs): Gtk.Box.__init__ (self, *args, hscrollbar_policy = Gtk.PolicyType.AUTOMATIC, vscrollbar_policy = Gtk.PolicyType.AUTOMATIC, **kwargs) self.needs_update = True store = self.Store () self.store = store tree = Gtk.TreeView (model = store) self.add (tree) tree.set_tooltip_column (store.DESC) tree.show () self.single_sample_cols = [] self.multi_sample_cols = [] col = Gtk.TreeViewColumn (title = "Variable") tree.append_column (col) col.set_resizable (True) cell = CellRendererColorToggle (active = True) col.pack_start (cell, False) col.add_attribute (cell, "color", store.COLOR) cell = Gtk.CellRendererText () col.pack_start (cell, False) col.add_attribute (cell, "text", store.NAME) def add_value_column (title, column, single_sample): col = Gtk.TreeViewColumn (title = title) tree.append_column (col) col.set_resizable (True) col.set_alignment (0.5) cell = Gtk.CellRendererText (xalign = 1) col.pack_start (cell, False) col.add_attribute (cell, "text", column) if single_sample: self.single_sample_cols.append (col) else: self.multi_sample_cols.append (col) add_value_column ("Value", store.VALUE, True) add_value_column ("Raw", store.RAW, True) add_value_column ("Min", store.MIN, False) add_value_column ("Max", store.MAX, False) add_value_column ("Median", store.MEDIAN, False) add_value_column ("Mean", store.MEAN, False) add_value_column ("Std. Dev.", store.STDEV, False) col = Gtk.TreeViewColumn () tree.append_column (col) selection.connect ("change-complete", self.selection_change_complete) def update (self): if not self.needs_update: return self.needs_update = False sel = selection.get_effective_selection () n_sel = len (sel) if n_sel == 1: i, = sel for row in self.store: var_name = row[self.store.NAME] var = samples[i].vars[var_name] var_type = var_types[var_defs[var_name].type] row[self.store.VALUE] = var_type.format (var.value) row[self.store.RAW] = var.raw if var.raw is not None \ else "N/A" else: for row in self.store: var_name = row[self.store.NAME] var_type = var_types[var_defs[var_name].type] vals = (samples[i].vars[var_name].value for i in sel) vals = tuple (val for val in vals if val is not None) if vals: min_val = min (vals) max_val = max (vals) median = statistics.median (vals) mean = statistics.mean (vals) stdev = statistics.pstdev (vals, mean) row[self.store.MIN] = var_type.format (min_val) row[self.store.MAX] = var_type.format (max_val) row[self.store.MEDIAN] = var_type.format (median) row[self.store.MEAN] = var_type.format_numeric (mean) row[self.store.STDEV] = var_type.format_numeric (stdev) else: row[self.store.MIN] = \ row[self.store.MAX] = \ row[self.store.MEDIAN] = \ row[self.store.MEAN] = \ row[self.store.STDEV] = var_type.format (None) for col in self.single_sample_cols: col.set_visible (n_sel == 1) for col in self.multi_sample_cols: col.set_visible (n_sel > 1) def do_map (self): self.update () Gtk.ScrolledWindow.do_map (self) def selection_change_complete (self, selection): self.needs_update = True if self.get_mapped (): self.update () class BacktraceViewer (Gtk.Box): class ThreadStore (Gtk.ListStore): INDEX = 0 ID = 1 NAME = 2 STATE = 3 def __init__ (self): Gtk.ListStore.__init__ (self, int, int, str, str) class FrameStore (Gtk.ListStore): ID = 0 ADDRESS = 1 OBJECT = 2 FUNCTION = 3 OFFSET = 4 SOURCE = 5 LINE = 6 def __init__ (self): Gtk.ListStore.__init__ (self, int, str, str, str, str, str, str) class CellRendererViewSource (Gtk.CellRendererPixbuf): file = GObject.Property (type = Gio.File, default = None) line = GObject.Property (type = int, default = 0) def __init__ (self, *args, **kwargs): Gtk.CellRendererPixbuf.__init__ ( self, *args, icon_name = "text-x-generic-symbolic", mode = Gtk.CellRendererMode.ACTIVATABLE, **kwargs) self.connect ("notify::file", lambda *args: self.set_property ("visible", bool (self.file))) def do_activate (self, event, widget, path, *args): if self.file: run_editor (self.file, self.line) return True return False def __init__ (self, *args, **kwargs): Gtk.Box.__init__ (self, *args, orientation = Gtk.Orientation.HORIZONTAL, **kwargs) self.needs_update = True vbox = Gtk.Box (orientation = Gtk.Orientation.VERTICAL) self.pack_start (vbox, False, False, 0) vbox.show () header = Gtk.HeaderBar (title = "Threads", has_subtitle = False) vbox.pack_start (header, False, False, 0) header.show () scrolled = Gtk.ScrolledWindow ( hscrollbar_policy = Gtk.PolicyType.NEVER, vscrollbar_policy = Gtk.PolicyType.AUTOMATIC ) vbox.pack_start (scrolled, True, True, 0) scrolled.show () store = self.ThreadStore () self.thread_store = store store.set_sort_column_id (store.ID, Gtk.SortType.ASCENDING) tree = Gtk.TreeView (model = store) self.thread_tree = tree scrolled.add (tree) tree.set_search_column (store.NAME) tree.show () tree.connect ("row-activated", self.threads_row_activated) tree.get_selection ().connect ("changed", self.threads_selection_changed) col = Gtk.TreeViewColumn (title = "ID") tree.append_column (col) col.set_resizable (True) cell = Gtk.CellRendererText (xalign = 1) col.pack_start (cell, False) col.add_attribute (cell, "text", self.ThreadStore.ID) col = Gtk.TreeViewColumn (title = "Name") tree.append_column (col) col.set_resizable (True) cell = Gtk.CellRendererText () col.pack_start (cell, False) col.add_attribute (cell, "text", self.ThreadStore.NAME) col = Gtk.TreeViewColumn (title = "State") tree.append_column (col) col.set_resizable (True) cell = Gtk.CellRendererText () col.pack_start (cell, False) col.add_attribute (cell, "text", self.ThreadStore.STATE) separator = Gtk.Separator (orientation = Gtk.Orientation.VERTICAL) self.pack_start (separator, False, False, 0) separator.show () vbox = Gtk.Box (orientation = Gtk.Orientation.VERTICAL) self.pack_start (vbox, True, True, 0) vbox.show () header = Gtk.HeaderBar (title = "Stack", has_subtitle = False) vbox.pack_start (header, False, False, 0) header.show () scrolled = Gtk.ScrolledWindow ( hscrollbar_policy = Gtk.PolicyType.AUTOMATIC, vscrollbar_policy = Gtk.PolicyType.AUTOMATIC ) vbox.pack_start (scrolled, True, True, 0) scrolled.show () store = self.FrameStore () self.frame_store = store tree = Gtk.TreeView (model = store, has_tooltip = True) scrolled.add (tree) tree.set_search_column (store.FUNCTION) tree.show () tree.connect ("row-activated", self.frames_row_activated) tree.connect ("query-tooltip", self.frames_query_tooltip) def format_filename_col (tree_col, cell, model, iter, col): object = model[iter][col] cell.set_property ("text", get_basename (object) if object else "") self.tooltip_columns = {} col = Gtk.TreeViewColumn (title = "#") tree.append_column (col) col.set_resizable (True) cell = Gtk.CellRendererText (xalign = 1) col.pack_start (cell, False) col.add_attribute (cell, "text", self.FrameStore.ID) col = Gtk.TreeViewColumn (title = "Address") tree.append_column (col) col.set_resizable (True) cell = Gtk.CellRendererText (xalign = 1) col.pack_start (cell, False) col.add_attribute (cell, "text", self.FrameStore.ADDRESS) col = Gtk.TreeViewColumn (title = "Object") self.tooltip_columns[col] = store.OBJECT tree.append_column (col) col.set_resizable (True) cell = Gtk.CellRendererText () col.pack_start (cell, False) col.set_cell_data_func (cell, format_filename_col, store.OBJECT) col = Gtk.TreeViewColumn (title = "Function") tree.append_column (col) col.set_resizable (True) cell = Gtk.CellRendererText () col.pack_start (cell, False) col.add_attribute (cell, "text", self.FrameStore.FUNCTION) col = Gtk.TreeViewColumn (title = "Offset") tree.append_column (col) col.set_resizable (True) cell = Gtk.CellRendererText (xalign = 1) col.pack_start (cell, False) col.add_attribute (cell, "text", self.FrameStore.OFFSET) col = Gtk.TreeViewColumn (title = "Source") self.tooltip_columns[col] = store.SOURCE tree.append_column (col) col.set_resizable (True) cell = Gtk.CellRendererText () col.pack_start (cell, False) col.set_cell_data_func (cell, format_filename_col, store.SOURCE) col = Gtk.TreeViewColumn (title = "Line") tree.append_column (col) col.set_resizable (True) cell = Gtk.CellRendererText (xalign = 1) col.pack_start (cell, False) col.add_attribute (cell, "text", self.FrameStore.LINE) def format_view_source_col (tree_col, cell, model, iter, cols): filename = model[iter][cols[0]] or None line = model[iter][cols[1]] or "0" cell.set_property ("file", filename and find_file (filename)) cell.set_property ("line", int (line)) def format_view_source_tooltip (row): filename = row[store.SOURCE] if filename: file = find_file (filename) if file: return file.get_path () return None col = Gtk.TreeViewColumn () self.tooltip_columns[col] = format_view_source_tooltip tree.append_column (col) cell = self.CellRendererViewSource (xalign = 0) col.pack_start (cell, False) col.set_cell_data_func (cell, format_view_source_col, (store.SOURCE, store.LINE)) selection.connect ("change-complete", self.selection_change_complete) @GObject.Property (type = bool, default = False) def available (self): sel = selection.get_effective_selection () if len (sel) == 1: i, = sel return bool (samples[i].backtrace) return False def update (self): if not self.needs_update or not self.available: return self.needs_update = False tid = None sel_rows = self.thread_tree.get_selection ().get_selected_rows ()[1] if sel_rows: tid = self.thread_store[sel_rows[0]][self.ThreadStore.ID] i, = selection.get_effective_selection () self.thread_store.clear () for t in range (len (samples[i].backtrace)): thread = samples[i].backtrace[t] iter = self.thread_store.append ( (t, thread.id, thread.name, str (thread.state)) ) if thread.id == tid: self.thread_tree.get_selection ().select_iter (iter) def do_map (self): self.update () Gtk.Box.do_map (self) def selection_change_complete (self, selection): self.needs_update = True if self.get_mapped (): self.update () self.notify ("available") def threads_row_activated (self, tree, path, col): iter = self.thread_store.get_iter (path) tid = self.thread_store[iter][self.ThreadStore.ID] sel = set () for i in range (len (samples)): threads = filter (lambda thread: thread.id == tid and thread.state == ThreadState.RUNNING, samples[i].backtrace or []) if list (threads): sel.add (i) selection.select (sel) selection.change_complete () def threads_selection_changed (self, tree_sel): self.frame_store.clear () (store, rows) = tree_sel.get_selected_rows () if not rows: return i, = selection.get_effective_selection () try: frames = samples[i].backtrace[store[rows[0]][store.INDEX]].frames for frame in frames: info = frame.info self.frame_store.append (( frame.id, hex (frame.address), info.object, info.symbol, hex (info.offset) if info.offset is not None else None, info.source, str (info.line) if info.line else None )) except: pass def frames_row_activated (self, tree, path, col): iter = self.frame_store.get_iter (path) address = int (self.frame_store[iter][self.FrameStore.ADDRESS], 0) info = address_map.get (address, None) if not info: return id = info.id if not id: return sel = set () def has_frame (sample, id): for thread in sample.backtrace or []: for frame in thread.frames: if frame.info.id == id: return True return False for i in range (len (samples)): if has_frame (samples[i], id): sel.add (i) selection.select (sel) selection.change_complete () def frames_query_tooltip (self, tree, x, y, keyboard_mode, tooltip): hit, x, y, model, path, iter = tree.get_tooltip_context (x, y, keyboard_mode) if hit: column = None if keyboard_mode: cursor_path, cursor_col = tree.get_cursor () if path.compare (cursor_path) == 0: column = self.tooltip_columns[cursor_col] else: for col in self.tooltip_columns: area = tree.get_cell_area (path, col) if x >= area.x and x < area.x + area.width and \ y >= area.y and y < area.y + area.height: column = self.tooltip_columns[col] break if column is not None: value = None if type (column) == int: value = model[iter][column] else: value = column (model[iter]) if value: tooltip.set_text (str (value)) return True return False class CellRendererPercentage (Gtk.CellRendererText): padding = 0 def __init__ (self, *args, **kwargs): Gtk.CellRendererText.__init__ (self, *args, xalign = 1, **kwargs) self.value = 0 @GObject.Property (type = float) def value (self): return self.value_property @value.setter def value (self, value): self.value_property = value self.set_property ("text", format_percentage (value, 2)) def do_render (self, cr, widget, background_area, cell_area, flags): full_width = cell_area.width - 2 * self.padding full_height = cell_area.height - 2 * self.padding if full_width <= 0 or full_height <= 0: return state = widget.get_state_flags() style = widget.get_style_context () fg_color = style.get_color (state) rounded_rectangle (cr, cell_area.x + self.padding, cell_area.y + self.padding, full_width, full_height, 1) cr.clip () cr.set_source_rgba (*blend_colors ((0, 0, 0, 0), fg_color, 0.2)) cr.paint () Gtk.CellRendererText.do_render (self, cr, widget, background_area, cell_area, flags) value = min (max (self.value, 0), 1) width = round (full_width * value) height = full_height if width > 0 and height > 0: state = Gtk.StateFlags (state | Gtk.StateFlags.SELECTED) flags = Gtk.CellRendererState (flags | Gtk.CellRendererState.SELECTED) style.save () style.set_state (state) x = round ((full_width - width) * self.get_property ("xalign")) cr.rectangle (cell_area.x + self.padding + x, cell_area.y + self.padding, width, height) cr.clip () Gtk.render_background (style, cr, cell_area.x, cell_area.y, cell_area.width, cell_area.height) cr.set_source_rgba (0, 0, 0, 0.25) cr.paint () Gtk.CellRendererText.do_render (self, cr, widget, background_area, cell_area, flags) style.restore () class ProfileViewer (Gtk.ScrolledWindow): class ThreadFilter (Gtk.TreeView): class Store (Gtk.ListStore): VISIBLE = 0 ID = 1 NAME = 2 STATE = {list (ThreadState)[i]: 3 + i for i in range (len (ThreadState))} def __init__ (self): Gtk.ListStore.__init__ (self, bool, int, str, *(len (self.STATE) * (bool,))) threads = list ({thread.id for sample in samples for thread in sample.backtrace or ()}) threads.sort () states = [state == ThreadState.RUNNING for state in self.STATE] for id in threads: self.append ((False, id, None, *states)) def get_filter (self): return {row[self.ID]: {state for state, column in self.STATE.items () if row[column]} for row in self} def set_filter (self, filter): for row in self: states = filter[row[self.ID]] for state, column in self.STATE.items (): row[column] = state in states def __init__ (self, *args, **kwargs): Gtk.TreeView.__init__ (self, *args, **kwargs) self.needs_update = True store = self.Store () self.store = store filter = Gtk.TreeModelFilter (child_model = store) filter.set_visible_column (store.VISIBLE) self.set_model (filter) self.set_search_column (store.NAME) col = Gtk.TreeViewColumn (title = "ID") self.append_column (col) cell = Gtk.CellRendererText (xalign = 1) col.pack_start (cell, False) col.add_attribute (cell, "text", store.ID) col = Gtk.TreeViewColumn (title = "Name") self.append_column (col) cell = Gtk.CellRendererText () col.pack_start (cell, False) col.add_attribute (cell, "text", store.NAME) for state in store.STATE: col = Gtk.TreeViewColumn (title = str (state)) col.column = store.STATE[state] self.append_column (col) col.set_alignment (0.5) col.set_clickable (True) def col_clicked (col): active = not all (row[col.column] for row in filter) for row in filter: row[col.column] = active col.connect ("clicked", col_clicked) cell = Gtk.CellRendererToggle () cell.column = store.STATE[state] col.pack_start (cell, False) col.add_attribute (cell, "active", store.STATE[state]) def cell_toggled (cell, path): filter[path][cell.column] = not cell.get_property ("active") cell.connect ("toggled", cell_toggled) selection.connect ("change-complete", self.selection_change_complete) def update (self): if not self.needs_update: return self.needs_update = False sel = selection.get_effective_selection () threads = {thread.id: thread.name for i in sel for thread in samples[i].backtrace or ()} for row in self.store: id = row[self.store.ID] if id in threads: row[self.store.VISIBLE] = True row[self.store.NAME] = threads[id] else: row[self.store.VISIBLE] = False row[self.store.NAME] = None def do_map (self): self.update () Gtk.TreeView.do_map (self) def selection_change_complete (self, selection): self.needs_update = True if self.get_mapped (): self.update () class ThreadPopover (Gtk.Popover): def __init__ (self, *args, **kwargs): Gtk.Popover.__init__ (self, *args, border_width = 4, **kwargs) frame = Gtk.Frame (shadow_type = Gtk.ShadowType.IN) self.add (frame) frame.show () scrolled = Gtk.ScrolledWindow ( hscrollbar_policy = Gtk.PolicyType.NEVER, vscrollbar_policy = Gtk.PolicyType.AUTOMATIC, propagate_natural_height = True, max_content_height = 400 ) frame.add (scrolled) scrolled.show () thread_filter = ProfileViewer.ThreadFilter () self.thread_filter = thread_filter scrolled.add (thread_filter) thread_filter.show () class Profile (Gtk.Box): ProfileFrame = namedtuple ("ProfileFrame", ("sample", "stack", "i")) class Direction (enum.Enum): CALLEES = enum.auto () CALLERS = enum.auto () class Store (Gtk.ListStore): ID = 0 FUNCTION = 1 EXCLUSIVE = 2 INCLUSIVE = 3 def __init__ (self): Gtk.ListStore.__init__ (self, GObject.TYPE_UINT64, str, float, float) __gsignals__ = { "needs-update": (GObject.SignalFlags.RUN_FIRST, None, (bool,)), "subprofile-added": (GObject.SignalFlags.RUN_FIRST, None, (Gtk.Widget,)), "subprofile-removed": (GObject.SignalFlags.RUN_FIRST, None, (Gtk.Widget,)), "path-changed": (GObject.SignalFlags.RUN_FIRST, None, ()) } def __init__ (self, root = None, id = None, title = None, frames = None, direction = Direction.CALLEES, sort = (Store.INCLUSIVE, Gtk.SortType.DESCENDING), *args, **kwargs): Gtk.Box.__init__ (self, *args, orientation = Gtk.Orientation.HORIZONTAL, **kwargs) self.root = root or self self.id = id self.frames = frames self.direction = direction self.subprofile = None vbox = Gtk.Box (orientation = Gtk.Orientation.VERTICAL) self.pack_start (vbox, False, False, 0) vbox.show () header = Gtk.HeaderBar (title = title or "All Functions") self.header = header vbox.pack_start (header, False, False, 0) header.show () if not id: popover = ProfileViewer.ThreadPopover () thread_filter_store = popover.thread_filter.store self.thread_filter_store = thread_filter_store self.thread_filter = thread_filter_store.get_filter () history.add_source (self.thread_filter_source_get, self.thread_filter_source_set) button = Gtk.MenuButton (popover = popover) header.pack_end (button) button.show () button.connect ("toggled", self.thread_filter_button_toggled) hbox = Gtk.Box (orientation = Gtk.Orientation.HORIZONTAL, spacing = 4) button.add (hbox) hbox.show () label = Gtk.Label (label = "Threads") hbox.pack_start (label, False, False, 0) label.show () image = Gtk.Image.new_from_icon_name ("pan-down-symbolic", Gtk.IconSize.BUTTON) hbox.pack_start (image, False, False, 0) image.show () history.add_source (self.direction_source_get, self.direction_source_set) button = Gtk.Button (tooltip_text = "Call-graph direction") header.pack_end (button) button.show () button.connect ("clicked", self.direction_button_clicked) image = Gtk.Image () self.direction_image = image button.add (image) image.show () else: button = Gtk.Button.new_from_icon_name ( "edit-select-symbolic", Gtk.IconSize.BUTTON ) header.pack_end (button) button.set_tooltip_text ( str (Selection (frame.sample for frame in frames)) ) button.show () button.connect ("clicked", self.select_samples_clicked) scrolled = Gtk.ScrolledWindow ( hscrollbar_policy = Gtk.PolicyType.NEVER, vscrollbar_policy = Gtk.PolicyType.AUTOMATIC ) vbox.pack_start (scrolled, True, True, 0) scrolled.show () store = self.Store () self.store = store store.set_sort_column_id (*sort) tree = Gtk.TreeView (model = store) self.tree = tree scrolled.add (tree) tree.set_search_column (store.FUNCTION) tree.show () tree.get_selection ().connect ("changed", self.tree_selection_changed) tree.connect ("row-activated", self.tree_row_activated) tree.connect ("key-press-event", self.tree_key_press_event) col = Gtk.TreeViewColumn (title = "Function") tree.append_column (col) col.set_resizable (True) col.set_sort_column_id (store.FUNCTION) cell = Gtk.CellRendererText (ellipsize = Pango.EllipsizeMode.END) col.pack_start (cell, True) col.add_attribute (cell, "text", store.FUNCTION) cell.set_property ("width-chars", 40) col = Gtk.TreeViewColumn (title = "Self") tree.append_column (col) col.set_alignment (0.5) col.set_sort_column_id (store.EXCLUSIVE) cell = CellRendererPercentage () col.pack_start (cell, False) col.add_attribute (cell, "value", store.EXCLUSIVE) col = Gtk.TreeViewColumn (title = "All") tree.append_column (col) col.set_alignment (0.5) col.set_sort_column_id (store.INCLUSIVE) cell = CellRendererPercentage () col.pack_start (cell, False) col.add_attribute (cell, "value", store.INCLUSIVE) if id: self.update () def update (self): self.remove_subprofile () if not self.id: self.update_frames () self.update_store () self.update_ui () def update_frames (self): self.frames = [] for i in selection.get_effective_selection (): for thread in samples[i].backtrace or []: if thread.state in self.thread_filter[thread.id]: thread_frames = thread.frames if self.direction == self.Direction.CALLERS: thread_frames = reversed (thread_frames) stack = [] prev_id = 0 for frame in thread_frames: id = frame.info.id if id == prev_id: continue self.frames.append (self.ProfileFrame ( sample = i, stack = stack, i = len (stack) )) stack.append (frame) prev_id = id def update_store (self): stacks = {} symbols = {} sort = self.store.get_sort_column_id () self.store = self.Store () for frame in self.frames: info = frame.stack[frame.i].info symbol_id = info.id stack_id = builtins.id (frame.stack) symbol = symbols.get (symbol_id, None) if not symbol: symbol = [info, 0, 0] symbols[symbol_id] = symbol stack = stacks.get (stack_id, None) if not stack: stack = set () stacks[stack_id] = stack if frame.i == 0: symbol[1] += 1 if symbol_id not in stack: stack.add (symbol_id) symbol[2] += 1 n_stacks = len (stacks) for symbol in symbols.values (): id = symbol[0].id name = symbol[0].name if id != self.id else "[Self]" self.store.append ((id, name, symbol[1] / n_stacks, symbol[2] / n_stacks)) self.store.set_sort_column_id (*sort) self.tree.set_model (self.store) self.tree.set_search_column (self.store.FUNCTION) def update_ui (self): if not self.id: if self.direction == self.Direction.CALLEES: icon_name = "format-indent-more-symbolic" else: icon_name = "format-indent-less-symbolic" self.direction_image.set_from_icon_name (icon_name, Gtk.IconSize.BUTTON) else: if self.direction == self.Direction.CALLEES: subtitle = "Callees" else: subtitle = "Callers" self.header.set_subtitle (subtitle) def select (self, id): if id is not None: for row in self.store: if row[self.store.ID] == id: iter = row.iter path = self.store.get_path (iter) self.tree.get_selection ().select_iter (iter) self.tree.scroll_to_cell (path, None, True, 0.5, 0) break else: self.tree.get_selection ().unselect_all () def add_subprofile (self, subprofile): self.remove_subprofile () box = Gtk.Box (orientation = Gtk.Orientation.HORIZONTAL) self.subprofile_box = box self.pack_start (box, True, True, 0) box.show () separator = Gtk.Separator (orientation = Gtk.Orientation.VERTICAL) box.pack_start (separator, False, False, 0) separator.show () self.subprofile = subprofile box.pack_start (subprofile, True, True, 0) subprofile.show () subprofile.connect ("subprofile-added", lambda profile, subprofile: self.emit ("subprofile-added", subprofile)) subprofile.connect ("subprofile-removed", lambda profile, subprofile: self.emit ("subprofile-removed", subprofile)) subprofile.connect ("path-changed", lambda profile: self.emit ("path-changed")) self.emit ("subprofile-added", subprofile) def remove_subprofile (self): if self.subprofile: subprofile = self.subprofile self.remove (self.subprofile_box) self.subprofile = None self.subprofile_box = None self.emit ("subprofile-removed", subprofile) def get_path (self): tree_sel = self.tree.get_selection () sel_rows = tree_sel.get_selected_rows ()[1] if not sel_rows: return () id = self.store[sel_rows[0]][self.store.ID] if self.subprofile: return (id,) + self.subprofile.get_path () else: return (id,) def set_path (self, path): self.select (path[0] if path else None) if self.subprofile: self.subprofile.set_path (path[1:]) def thread_filter_source_get (self): return self.thread_filter_store.get_filter () def thread_filter_source_set (self, thread_filter): self.thread_filter = thread_filter self.thread_filter_store.set_filter (thread_filter) self.emit ("needs-update", False) def thread_filter_button_toggled (self, button): if not button.get_active (): thread_filter = self.thread_filter_store.get_filter () if thread_filter != self.thread_filter: self.thread_filter = thread_filter history.start_group () history.record () self.emit ("needs-update", True) history.end_group () def direction_source_get (self): return self.direction def direction_source_set (self, direction): self.direction = direction self.emit ("needs-update", False) def direction_button_clicked (self, button): if self.direction == self.Direction.CALLEES: self.direction = self.Direction.CALLERS else: self.direction = self.Direction.CALLEES history.start_group () history.record () self.emit ("needs-update", True) history.end_group () def select_samples_clicked (self, button): selection.select ({frame.sample for frame in self.frames}) selection.change_complete () def tree_selection_changed (self, tree_sel): self.remove_subprofile () sel_rows = tree_sel.get_selected_rows ()[1] if not sel_rows: self.emit ("path-changed") return id = self.store[sel_rows[0]][self.store.ID] title = self.store[sel_rows[0]][self.store.FUNCTION] frames = [] for frame in self.frames: if frame.stack[frame.i].info.id == id: frames.append (frame) if frame.i > 0 and id != self.id: frames.append (self.ProfileFrame (sample = frame.sample, stack = frame.stack, i = frame.i - 1)) if id != self.id: self.add_subprofile (ProfileViewer.Profile ( self.root, id, title, frames, self.direction, self.store.get_sort_column_id () )) else: filenames = {frame.stack[frame.i].info.source for frame in frames} filenames = list (filter (bool, filenames)) if len (filenames) == 1: file = find_file (filenames[0]) if file: self.add_subprofile (ProfileViewer.SourceProfile ( file, frames[0].stack[frames[0].i].info.name, frames )) self.emit ("path-changed") def tree_row_activated (self, tree, path, col): if self.root != self: self.root.select (self.store[path][self.store.ID]) def tree_key_press_event (self, tree, event): if event.keyval == Gdk.KEY_Escape: self.select (None) if self.root is not self: self.get_parent ().get_ancestor ( ProfileViewer.Profile ).tree.grab_focus () return True return False class SourceProfile (Gtk.Box): class Store (Gtk.ListStore): LINE = 0 HAS_FRAMES = 1 EXCLUSIVE = 2 INCLUSIVE = 3 TEXT = 4 def __init__ (self): Gtk.ListStore.__init__ (self, int, bool, float, float, str) __gsignals__ = { "subprofile-added": (GObject.SignalFlags.RUN_FIRST, None, (Gtk.Widget,)), "subprofile-removed": (GObject.SignalFlags.RUN_FIRST, None, (Gtk.Widget,)), "path-changed": (GObject.SignalFlags.RUN_FIRST, None, ()) } def __init__ (self, file, function, frames, *args, **kwargs): Gtk.Box.__init__ (self, *args, orientation = Gtk.Orientation.VERTICAL, **kwargs) self.file = file self.frames = frames header = Gtk.HeaderBar (title = file.get_basename (), subtitle = function) self.header = header self.pack_start (header, False, False, 0) header.show () box = Gtk.Box (orientation = Gtk.Orientation.HORIZONTAL) header.pack_start (box) box.get_style_context ().add_class ("linked") box.get_style_context ().add_class ("raised") box.show () button = Gtk.Button.new_from_icon_name ("go-up-symbolic", Gtk.IconSize.BUTTON) self.prev_button = button box.pack_start (button, False, True, 0) button.show () button.connect ("clicked", lambda *args: self.move (-1)) button = Gtk.Button.new_from_icon_name ("go-down-symbolic", Gtk.IconSize.BUTTON) self.next_button = button box.pack_end (button, False, True, 0) button.show () button.connect ("clicked", lambda *args: self.move (+1)) button = Gtk.Button.new_from_icon_name ("edit-select-symbolic", Gtk.IconSize.BUTTON) self.select_samples_button = button header.pack_end (button) button.show () button.connect ("clicked", self.select_samples_clicked) button = Gtk.Button.new_from_icon_name ("text-x-generic-symbolic", Gtk.IconSize.BUTTON) header.pack_end (button) button.set_tooltip_text (file.get_path ()) button.show () button.connect ("clicked", self.view_source_clicked) scrolled = Gtk.ScrolledWindow ( hscrollbar_policy = Gtk.PolicyType.NEVER, vscrollbar_policy = Gtk.PolicyType.AUTOMATIC ) self.pack_start (scrolled, True, True, 0) scrolled.show () store = self.Store () self.store = store tree = Gtk.TreeView (model = store) self.tree = tree scrolled.add (tree) tree.set_search_column (store.LINE) tree.show () tree.get_selection ().connect ("changed", self.tree_selection_changed) scale = 0.85 col = Gtk.TreeViewColumn (title = "Self") tree.append_column (col) col.set_alignment (0.5) col.set_sort_column_id (store.EXCLUSIVE) cell = CellRendererPercentage (scale = scale) col.pack_start (cell, False) col.add_attribute (cell, "visible", store.HAS_FRAMES) col.add_attribute (cell, "value", store.EXCLUSIVE) col = Gtk.TreeViewColumn (title = "All") tree.append_column (col) col.set_alignment (0.5) col.set_sort_column_id (store.INCLUSIVE) cell = CellRendererPercentage (scale = scale) col.pack_start (cell, False) col.add_attribute (cell, "visible", store.HAS_FRAMES) col.add_attribute (cell, "value", store.INCLUSIVE) col = Gtk.TreeViewColumn () tree.append_column (col) cell = Gtk.CellRendererText (xalign = 1, xpad = 8, family = "Monospace", weight = Pango.Weight.BOLD, scale = scale) col.pack_start (cell, False) col.add_attribute (cell, "text", store.LINE) cell = Gtk.CellRendererText (family = "Monospace", scale = scale) col.pack_start (cell, True) col.add_attribute (cell, "text", store.TEXT) self.update () def get_samples (self): sel_rows = self.tree.get_selection ().get_selected_rows ()[1] if sel_rows: line = self.store[sel_rows[0]][self.store.LINE] sel = {frame.sample for frame in self.frames if frame.stack[frame.i].info.line == line} return sel else: return {} def update (self): self.update_store () self.update_ui () def update_store (self): stacks = {} lines = {} for frame in self.frames: info = frame.stack[frame.i].info line_id = info.line stack_id = builtins.id (frame.stack) line = lines.get (line_id, None) if not line: line = [0, 0] lines[line_id] = line stack = stacks.get (stack_id, None) if not stack: stack = set () stacks[stack_id] = stack if frame.i == 0: line[0] += 1 if line_id not in stack: stack.add (line_id) line[1] += 1 self.lines = list (lines.keys ()) self.lines.sort () n_stacks = len (stacks) self.store.clear () i = 1 for text in open (self.file.get_path (), "r"): text = text.rstrip ("\n") line = lines.get (i, None) if line: self.store.append ((i, True, line[0] / n_stacks, line[1] / n_stacks, text)) else: self.store.append ((i, False, 0, 0, text)) i += 1 self.select (max (lines.items (), key = lambda line: line[1][1])[0]) def update_ui (self): sel_rows = self.tree.get_selection ().get_selected_rows ()[1] if sel_rows: line = self.store[sel_rows[0]][self.store.LINE] i = bisect.bisect_left (self.lines, line) self.prev_button.set_sensitive (i > 0) if i < len (self.lines) and self.lines[i] == line: i += 1 self.next_button.set_sensitive (i < len (self.lines)) else: self.prev_button.set_sensitive (False) self.next_button.set_sensitive (False) samples = self.get_samples () if samples: self.select_samples_button.set_sensitive (True) self.select_samples_button.set_tooltip_text ( str (Selection (samples)) ) else: self.select_samples_button.set_sensitive (False) self.select_samples_button.set_tooltip_text (None) def select (self, line): if line is not None: for row in self.store: if row[self.store.LINE] == line: iter = row.iter path = self.store.get_path (iter) self.tree.get_selection ().select_iter (iter) self.tree.scroll_to_cell (path, None, True, 0.5, 0) break else: self.tree.get_selection ().unselect_all () def move (self, dir): if dir == 0: return sel_rows = self.tree.get_selection ().get_selected_rows ()[1] if sel_rows: line = self.store[sel_rows[0]][self.store.LINE] i = bisect.bisect_left (self.lines, line) if dir < 0: i -= 1 elif i < len (self.lines) and self.lines[i] == line: i += 1 if i >= 0 and i < len (self.lines): self.select (self.lines[i]) else: self.select (None) def select_samples_clicked (self, button): selection.select (self.get_samples ()) selection.change_complete () def view_source_clicked (self, button): line = 0 sel_rows = self.tree.get_selection ().get_selected_rows ()[1] if sel_rows: line = self.store[sel_rows[0]][self.store.LINE] run_editor (self.file, line) def get_path (self): tree_sel = self.tree.get_selection () sel_rows = tree_sel.get_selected_rows ()[1] if not sel_rows: return () line = self.store[sel_rows[0]][self.store.LINE] return (line,) def set_path (self, path): self.select (path[0] if path else None) def tree_selection_changed (self, tree_sel): self.update_ui () self.emit ("path-changed") def __init__ (self, *args, **kwargs): Gtk.ScrolledWindow.__init__ ( self, *args, hscrollbar_policy = Gtk.PolicyType.AUTOMATIC, vscrollbar_policy = Gtk.PolicyType.NEVER, **kwargs ) self.adjustment_changed_handler = None self.needs_update = True self.path = () profile = self.Profile () self.root_profile = profile self.add (profile) profile.show () selection.connect ("change-complete", self.selection_change_complete) profile.connect ("needs-update", self.profile_needs_update) profile.connect ("subprofile-added", self.profile_subprofile_added) profile.connect ("subprofile-removed", self.profile_subprofile_removed) profile.connect ("path-changed", self.profile_path_changed) history.add_source (self.source_get, self.source_set) @GObject.Property (type = bool, default = False) def available (self): sel = selection.get_effective_selection () if len (sel) > 1: return any (samples[i].backtrace for i in sel) return False def update (self): if not self.available: return history.block () if self.needs_update: self.root_profile.update () self.needs_update = False self.root_profile.set_path (self.path) history.unblock () def queue_update (self, now = False): self.needs_update = True if now or self.get_mapped (): self.update () def do_map (self): self.update () Gtk.ScrolledWindow.do_map (self) def selection_change_complete (self, selection): self.queue_update () self.notify ("available") def profile_needs_update (self, profile, now): self.queue_update (now) def profile_subprofile_added (self, profile, subprofile): if not history.is_blocked (): self.path = profile.get_path () history.record () if not self.adjustment_changed_handler: adjustment = self.get_hadjustment () def adjustment_changed (adjustment): GObject.signal_handler_disconnect ( adjustment, self.adjustment_changed_handler ) self.adjustment_changed_handler = None adjustment.set_value (adjustment.get_upper ()) self.adjustment_changed_handler = adjustment.connect ( "changed", adjustment_changed ) def profile_subprofile_removed (self, profile, subprofile): if not history.is_blocked (): self.path = profile.get_path () history.record () def profile_path_changed (self, profile): if not history.is_blocked (): self.path = profile.get_path () history.update () def source_get (self): return self.path def source_set (self, path): self.path = path if self.get_mapped (): self.root_profile.set_path (path) class LogViewer (Gtk.Window): def __init__ (self, *args, **kwargs): Gtk.Window.__init__ ( self, *args, default_width = 1024, default_height = 768, window_position = Gtk.WindowPosition.CENTER, **kwargs) header = Gtk.HeaderBar ( title = "GIMP Performance Log Viewer", show_close_button = True ) self.header = header self.set_titlebar (header) header.show () box = Gtk.Box (orientation = Gtk.Orientation.HORIZONTAL) header.pack_start (box) box.get_style_context ().add_class ("linked") box.get_style_context ().add_class ("raised") box.show () button = Gtk.Button.new_from_icon_name ("go-previous-symbolic", Gtk.IconSize.BUTTON) box.pack_start (button, False, True, 0) button.show () history.bind_property ("can-undo", button, "sensitive", GObject.BindingFlags.SYNC_CREATE) button.connect ("clicked", lambda *args: history.undo ()) button = Gtk.Button.new_from_icon_name ("go-next-symbolic", Gtk.IconSize.BUTTON) box.pack_end (button, False, True, 0) button.show () history.bind_property ("can-redo", button, "sensitive", GObject.BindingFlags.SYNC_CREATE) button.connect ("clicked", lambda *args: history.redo ()) button = Gtk.MenuButton () header.pack_end (button) button.set_tooltip_text ("Find samples") button.show () image = Gtk.Image.new_from_icon_name ("edit-find-symbolic", Gtk.IconSize.BUTTON) button.add (image) image.show () popover = FindSamplesPopover (relative_to = button) self.find_popover = popover button.set_popover (popover) def selection_action (action): def f (*args): action (selection) selection.change_complete () return f button = Gtk.Button.new_from_icon_name ( "object-flip-horizontal-symbolic", Gtk.IconSize.BUTTON ) header.pack_end (button) button.set_tooltip_text ("Invert selection") button.show () button.connect ("clicked", selection_action (Selection.invert)) button = Gtk.Button.new_from_icon_name ( "edit-clear-symbolic", Gtk.IconSize.BUTTON ) self.clear_selection_button = button header.pack_end (button) button.set_tooltip_text ("Clear selection") button.show () button.connect ("clicked", selection_action (Selection.clear)) paned = Gtk.Paned (orientation = Gtk.Orientation.VERTICAL) self.paned = paned self.add (paned) paned.set_position (144) paned.show () graphs = SampleGraphList () paned.add1 (graphs) paned.child_set (graphs, shrink = False) graphs.show () hbox = Gtk.Box (orientation = Gtk.Orientation.HORIZONTAL) paned.add2 (hbox) hbox.show () sidebar = Gtk.StackSidebar () hbox.pack_start (sidebar, False, False, 0) sidebar.show () stack = Gtk.Stack (transition_type = Gtk.StackTransitionType.CROSSFADE) self.stack = stack hbox.pack_start (stack, True, True, 0) stack.show () sidebar.set_stack (stack) info_viewer = InformationViewer () stack.add_titled (info_viewer, "information", "Information") info_viewer.show () if markers: markers_viewer = MarkersViewer () stack.add_titled (markers_viewer, "markers", "Markers") markers_viewer.show () vars_viewer = VariablesViewer () stack.add_titled (vars_viewer, "variables", "Variables") vars_viewer.show () box = Gtk.Box (orientation = Gtk.Orientation.VERTICAL) self.cflow_box = box stack.add_named (box, "cflow") backtrace_viewer = BacktraceViewer () self.backtrace_viewer = backtrace_viewer box.pack_start (backtrace_viewer, True, True, 0) backtrace_viewer.bind_property ("available", backtrace_viewer, "visible", GObject.BindingFlags.SYNC_CREATE) backtrace_viewer.connect ("notify::available", self.cflow_notify_available) profile_viewer = ProfileViewer () self.profile_viewer = profile_viewer box.pack_start (profile_viewer, True, True, 0) profile_viewer.bind_property ("available", profile_viewer, "visible", GObject.BindingFlags.SYNC_CREATE) profile_viewer.connect ("notify::available", self.cflow_notify_available) self.cflow_notify_available (self) selection.connect ("change-complete", self.selection_change_complete) self.selection_change_complete (selection) def selection_change_complete (self, selection): self.header.set_subtitle (str (selection)) self.clear_selection_button.set_sensitive (selection.selection) def cflow_notify_available (self, *args): if self.backtrace_viewer.available: self.stack.child_set (self.cflow_box, title = "Backtrace") self.cflow_box.show () elif self.profile_viewer.available: self.stack.child_set (self.cflow_box, title = "Profile") self.cflow_box.show () else: self.cflow_box.hide () Gtk.Settings.get_default ().set_property ("gtk-application-prefer-dark-theme", True) window = LogViewer () window.show () window.connect ("destroy", Gtk.main_quit) history.record () Gtk.main ()