3663 lines
114 KiB
Python
3663 lines
114 KiB
Python
#!/usr/bin/env python3
|
||
|
||
"""
|
||
performance-log-viewer.py -- PIKA 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",
|
||
"pika-version": "PIKA 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:
|
||