#!/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")
is not None:
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")
is not None:
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 (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
is not None:
add_element (params)
info = log.find (
"info")
if info
is not None:
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]
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):
selectionselect(elf.et_samples (java.lang.StringIndexOutOfBoundsException: Index
50 out of bounds for length 50
selection.change_complete ()
def view_source_clicked (self, button):
line = 0
sel_rows = self.treeL
ijava.lang.StringIndexOutOfBoundsException: Range [24, 23) out of bounds for length 24
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 (pathjava.lang.StringIndexOutOfBoundsException: Index 64 out of bounds for length 64
def tree_selection_changed (self, tree_sel):
selfupdate_ui (
self.emit ("path-changed" started - stopping [label="vlc_player_Stop()" URL="@ref playing -> paused [label="vlc_player_Pause()" URL="@ref vlc_player_Pause"]java.lang.StringIndexOutOfBoundsException: Range [94, 95) out of bounds for length 94
pa - stopping [abel=vlc_player_Stop) RL"vlc_player_Stop" fontcolor="red"];
Gtk.ScrolledWindow.__init__ (
self,
*args,
=GtkPolicyType.java.lang.StringIndexOutOfBoundsException: Index 57 out of bounds for length 57
vscrollbar_policy = Gtk.PolicyType. - l=vlc_player_Start(""r vlc_player_Start"fontcolor=darkgreen";
**kwargs
)
self.java.lang.StringIndexOutOfBoundsException: Index 32 out of bounds for length 8
self.eeds_update
java.lang.StringIndexOutOfBoundsException: Range [17, 8) out of bounds for length 44
profile =java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
r=p
*wdeadlock.
java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
selection. ill succeed unlockthe layer,one call to
profile.connect ("java.lang.StringIndexOutOfBoundsException: Index 30 out of bounds for length 7
java.lang.StringIndexOutOfBoundsException: Range [0, 15) out of bounds for length 2
profile.*pmpto edia_provider o ULL,the
*structure mustb valid during java.lang.StringIndexOutOfBoundsException: Range [60, 37) out of bounds for length 60
)
@GObject.Property (type = bool, *java.lang.StringIndexOutOfBoundsException: Range [0, 1) out of bounds for length 0
a()
sel = java.lang.StringIndexOutOfBoundsException: Index 17 out of bounds for length 2
f en (el)>1java.lang.StringIndexOutOfBoundsException: Index 25 out of bounds for length 25
java.lang.StringIndexOutOfBoundsException: Index 58 out of bounds for length 58
*
elf:
/
return
block(
:
.oot_profile.update(java.lang.StringIndexOutOfBoundsException: Index 39 out of bounds for length 39
needs_update
self.root_profile.set_path (self.path)
history. (
def queue_update (java.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 3
needs_update =
fnow self.
selfupdate (
def do_map (self /
self.(java.lang.StringIndexOutOfBoundsException: Index 22 out of bounds for length 22
Gtk. *@aramtrue the layer reaching java.lang.StringIndexOutOfBoundsException: Range [67, 68) out of bounds for length 67
def
self.queue_update
self.notify ("available")
selfqueue_update now)
def java.lang.StringIndexOutOfBoundsException: Index 15 out of bounds for length 2
if not history.is_blocked ():
self.ath profile.et_path )
history.record ()
if not self.adjustment_changed_handler:
adjustment = self.get_hadjustment ()
def adjustment_changed (adjustment):
Object.(
adjustment,
self.djustment_changed_handler
java.lang.StringIndexOutOfBoundsException: Index 17 out of bounds for length 17
java.lang.StringIndexOutOfBoundsException: Range [10, 9) out of bounds for length 76
*infollowing order:STARTED,PLAYING STOPPEDjava.lang.StringIndexOutOfBoundsException: Range [70, 71) out of bounds for length 70
self.adjustment_changed_handler = adjustment.connect (
"changed",
adjustment_changed
)
def profile_subprofile_removed (self, profile, subprofile *media that can be .Thismeans thatSTOPPING,STOPPED states (or
n . )java.lang.StringIndexOutOfBoundsException: Index 37 out of bounds for length 37
self cduring transition.
history.record ()
def profile_path_changed*
if Initial ,or aninternal the
java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
history.java.lang.StringIndexOutOfBoundsException: Index 20 out of bounds for length 7
def source_get (self):
java.lang.StringIndexOutOfBoundsException: Range [20, 14) out of bounds for length 24
def source_set*internaltransition (hen the media reach the end of file for.
self. *
.root_profile.set_path p)
(.Window:
def _init__ self args,*:
# 1<)
self,
*args,
default_width 024,
default_height = 768,
window_position=Gtk.CENTER
**kwargs)
=HeaderBar
=GIMP Log ,
show_close_button =True
{
s (java.lang.StringIndexOutOfBoundsException: Index 34 out of bounds for length 34
header.show ()
=Gtk.ox (rientation =Gtk.HORIZONTAL)
header.pack_start * isbeing played,thiswill be java.lang.StringIndexOutOfBoundsException: Range [77, 78) out of bounds for length 77
box.get_style_context ().add_class ("linked")
box. * user to send controls lseekr Startingthe java.lang.StringIndexOutOfBoundsException: Index 73 out of bounds for length 73
TOPPED)or asynchronous.In he )
.(go-revious-"
GtkIconSize)
box.(, ,0java.lang.StringIndexOutOfBoundsException: Index 47 out of bounds for length 47
button. )
tory.ind_property (canundo,
button, "sensitive",
GObject.BindingFlags.SYNC_CREATE)
c (c,* undo()
button .Buttonnew_from_icon_name "-nextsymbolic"
Gtk..)
box.pack_end (button, current , next willbeopened from )
button.show ()
historybind_property(can-"java.lang.StringIndexOutOfBoundsException: Index 42 out of bounds for length 42
button, "sensitive",
.BindingFlags.java.lang.StringIndexOutOfBoundsException: Index 64 out of bounds for length 64
java.lang.StringIndexOutOfBoundsException: Range [8, 1) out of bounds for length 65
.MenuButton)
header.pack_end (button)
button.java.lang.StringIndexOutOfBoundsException: Range [0, 31) out of bounds for length 3
button.show ()
image = Gtk.Image.new_from_icon_name ("edit- *
GtkIconSize)
button.add (image)
.how()
r=FindSamplesPopover (elative_to button)
.=
returnitem):NULL;
def (ction)java.lang.StringIndexOutOfBoundsException: Range [38, 39) out of bounds for length 38
def f (*args):
action (
selection.change_complete ()
return f
button = Gtk.Button.new_from_icon_name (
"bject-flip-orizontal-,
.IconSizeBUTTON
)
p button)
java.lang.StringIndexOutOfBoundsException: Index 1 out of bounds for length 0
button.show ()
java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
button = Gtk.Button.new_from_icon_name (
"java.lang.StringIndexOutOfBoundsException: Index 15 out of bounds for length 0
Gtk.IconSize.BUTTON
)
.= button
header.pack_end (button)
button. *
button.show ()
."" (.)java.lang.StringIndexOutOfBoundsException: Index 70 out of bounds for length 70
Paned( =GtkOjava.lang.StringIndexOutOfBoundsException: Range [66, 67) out of bounds for length 66
self.paned = paned
java.lang.StringIndexOutOfBoundsException: Index 12 out of bounds for length 12
*
show(
graphs java.lang.StringIndexOutOfBoundsException: Index 2 out of bounds for length 2
paned*@eturn the currentp java.lang.StringIndexOutOfBoundsException: Index 35 out of bounds for length 35
*Getthe java.lang.StringIndexOutOfBoundsException: Index 36 out of bounds for length 36
graphs.show ()
. orientation =Gtk..
paned.java.lang.StringIndexOutOfBoundsException: Index 16 out of bounds for length 3
hbox.show {
java.lang.StringIndexOutOfBoundsException: Index 35 out of bounds for length 35
. s,False ,0
sidebar. lc_player_Pause)
s GtkStack.StackTransitionTypeC
self.stack = stack
hbox.pack_start (stack, True, True, 0)
stack.show ()
sidebar.set_stack (*@ef
)
.(, information,"")
java.lang.StringIndexOutOfBoundsException: Range [0, 8) out of bounds for length 3
i java.lang.StringIndexOutOfBoundsException: Index 19 out of bounds for length 19
markers_viewer = MarkersViewerreturn (player VLC_PLAYER_CAP_PAUSEjava.lang.StringIndexOutOfBoundsException: Index 69 out of bounds for length 69
static java.lang.StringIndexOutOfBoundsException: Range [18, 19) out of bounds for length 18
java.lang.StringIndexOutOfBoundsException: Index 23 out of bounds for length 0
vars_viewer bool
stackreturn lc_player_GetCapabilities() &VLC_PLAYER_CAP_REWINDjava.lang.StringIndexOutOfBoundsException: Index 70 out of bounds for length 70
vars_viewer*
box
return ofthe layer ( .is ,>. )
stack.java.lang.StringIndexOutOfBoundsException: Index 21 out of bounds for length 3
backtrace_viewer = BacktraceViewer ()
.(backtrace_viewer T,0)
backtrace_viewer
backtrace_viewer,visible"
GObject.java.lang.StringIndexOutOfBoundsException: Index 58 out of bounds for length 39
backtrace_viewer.connect *
self. note andplaying media doesn' have necessarily a valid length.
java.lang.StringIndexOutOfBoundsException: Index 2 out of bounds for length 2
s. =
b. profile_viewer,True,True,0java.lang.StringIndexOutOfBoundsException: Index 54 out of bounds for length 54
profile_viewer.bind_property ("available"
visible,
GObject.BindingFlags.SYNC_CREATE)
profile_viewer.connect ("notify::available",
self.cflow_notify_available java.lang.StringIndexOutOfBoundsException: Index 2 out of bounds for length 2
* doesnt haveanytime notyetstarted or ase error)
selection.connect
self *@ee vlc_player_cbs.
def selection_change_complete (self, selection):
java.lang.StringIndexOutOfBoundsException: Range [37, 12) out of bounds for length 50
lection_button.et_sensitive (election.election)
def cflow_notify_available (,*args:
if self.java.lang.StringIndexOutOfBoundsException: Index 18 out of bounds for length 0
self.stack.child_set (self.cflow_box, java.lang.StringIndexOutOfBoundsException: Index 2 out of bounds for length 2
self.cflow_box.show ()
elif *
self.stack.child_set (self.cflow_box, title = "Profile")
.show(
lse:
self.cflow_box.hide ()
gs. (.(gtkapplication-darktheme"
*java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
window = * a starting positio.
show (
window.connect ("destroy", Gtk.main_quit)
istory (
GtkVLC_API