summaryrefslogtreecommitdiffstats
path: root/tools/performance-log-viewer.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/performance-log-viewer.py')
-rwxr-xr-xtools/performance-log-viewer.py3662
1 files changed, 3662 insertions, 0 deletions
diff --git a/tools/performance-log-viewer.py b/tools/performance-log-viewer.py
new file mode 100755
index 0000000..8e87eb8
--- /dev/null
+++ b/tools/performance-log-viewer.py
@@ -0,0 +1,3662 @@
+#!/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 <https://www.gnu.org/licenses/>.
+
+
+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 ("<b>Sample %d</b>" % i)
+ label.show ()
+
+ row += 1
+
+ label = Gtk.Label ()
+ grid.attach (label, 0, row, 2, 1)
+ label.set_markup ("<sub>%s</sub>" %
+ 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 (
+ "<span color=\"%s\"><b>%s</b></span>" % (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 ("<b>Marker %d</b>" % (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 ("<b>%s</b>" % 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 ()