gimp/tools/performance-log-viewer.py

3663 lines
114 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 ()