tools: in performance-log-viewer.py, add annotated source view
Add an annotated source view to the performance-log viewer's
profile view. When selecting the [Self] entry of a function's
profile, for which source information is available and whose source
is found locally, a new column opens, showing the function's
source, annotated with sample statistics. Header-bar buttons allow
navigation through the annotated lines, selection of all the
samples corresponding to a given line, and opening the text editor
at the current line.
(cherry picked from commit 88438c5055
)
This commit is contained in:
@ -21,7 +21,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
Usage: performance-log-viewer.py < infile
|
||||
"""
|
||||
|
||||
import builtins, sys, os, math, statistics, functools, enum, re, subprocess
|
||||
import builtins, sys, os, math, statistics, bisect, functools, enum, re, \
|
||||
subprocess
|
||||
|
||||
from collections import namedtuple
|
||||
from xml.etree import ElementTree
|
||||
@ -113,6 +114,11 @@ def find_file (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"))
|
||||
|
||||
@ -422,6 +428,23 @@ class History (GObject.GObject):
|
||||
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 ()
|
||||
|
||||
@ -1796,13 +1819,7 @@ class BacktraceViewer (Gtk.Box):
|
||||
|
||||
def do_activate (self, event, widget, path, *args):
|
||||
if self.file:
|
||||
subprocess.call (
|
||||
editor_command.format (
|
||||
file = "\"%s\"" % self.file.get_path (),
|
||||
line = self.line
|
||||
),
|
||||
shell = True
|
||||
)
|
||||
run_editor (self.file, self.line)
|
||||
|
||||
return True
|
||||
|
||||
@ -2327,7 +2344,9 @@ class ProfileViewer (Gtk.ScrolledWindow):
|
||||
"subprofile-added": (GObject.SIGNAL_RUN_FIRST,
|
||||
None, (Gtk.Widget,)),
|
||||
"subprofile-removed": (GObject.SIGNAL_RUN_FIRST,
|
||||
None, (Gtk.Widget,))
|
||||
None, (Gtk.Widget,)),
|
||||
"path-changed": (GObject.SIGNAL_RUN_FIRST,
|
||||
None, ())
|
||||
}
|
||||
|
||||
def __init__ (self,
|
||||
@ -2621,6 +2640,8 @@ class ProfileViewer (Gtk.ScrolledWindow):
|
||||
lambda profile, subprofile:
|
||||
self.emit ("subprofile-removed",
|
||||
subprofile))
|
||||
subprofile.connect ("path-changed",
|
||||
lambda profile: self.emit ("path-changed"))
|
||||
|
||||
self.emit ("subprofile-added", subprofile)
|
||||
|
||||
@ -2714,33 +2735,49 @@ class ProfileViewer (Gtk.ScrolledWindow):
|
||||
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]
|
||||
|
||||
if id == self.id:
|
||||
return
|
||||
|
||||
frames = []
|
||||
|
||||
for frame in self.frames:
|
||||
if frame.stack[frame.i].info.id == id:
|
||||
frames.append (frame)
|
||||
|
||||
if frame.i > 0:
|
||||
if frame.i > 0 and id != self.id:
|
||||
frames.append (self.ProfileFrame (sample = frame.sample,
|
||||
stack = frame.stack,
|
||||
i = frame.i - 1))
|
||||
|
||||
self.add_subprofile (ProfileViewer.Profile (
|
||||
self.root,
|
||||
id,
|
||||
title,
|
||||
frames,
|
||||
self.direction,
|
||||
self.store.get_sort_column_id ()
|
||||
))
|
||||
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:
|
||||
@ -2759,6 +2796,320 @@ class ProfileViewer (Gtk.ScrolledWindow):
|
||||
|
||||
return False
|
||||
|
||||
class SourceProfile (Gtk.Box):
|
||||
class Store (Gtk.ListStore):
|
||||
LINE = 0
|
||||
EXCLUSIVE = 1
|
||||
INCLUSIVE = 2
|
||||
TEXT = 3
|
||||
|
||||
def __init__ (self):
|
||||
Gtk.ListStore.__init__ (self, int, float, float, str)
|
||||
|
||||
__gsignals__ = {
|
||||
"subprofile-added": (GObject.SIGNAL_RUN_FIRST,
|
||||
None, (Gtk.Widget,)),
|
||||
"subprofile-removed": (GObject.SIGNAL_RUN_FIRST,
|
||||
None, (Gtk.Widget,)),
|
||||
"path-changed": (GObject.SIGNAL_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-all-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
|
||||
|
||||
def format_percentage_col (tree_col, cell, model, iter, col):
|
||||
value = model[iter][col]
|
||||
|
||||
if value >= 0:
|
||||
cell.set_property ("text", format_percentage (value, 2))
|
||||
else:
|
||||
cell.set_property ("text", "")
|
||||
|
||||
col = Gtk.TreeViewColumn (title = "Self")
|
||||
tree.append_column (col)
|
||||
col.set_alignment (0.5)
|
||||
col.set_sort_column_id (store.EXCLUSIVE)
|
||||
|
||||
cell = Gtk.CellRendererText (xalign = 1, scale = scale)
|
||||
col.pack_start (cell, False)
|
||||
col.set_cell_data_func (cell,
|
||||
format_percentage_col, store.EXCLUSIVE)
|
||||
|
||||
col = Gtk.TreeViewColumn (title = "All")
|
||||
tree.append_column (col)
|
||||
col.set_alignment (0.5)
|
||||
col.set_sort_column_id (store.INCLUSIVE)
|
||||
|
||||
cell = Gtk.CellRendererText (xalign = 1, scale = scale)
|
||||
col.pack_start (cell, False)
|
||||
col.set_cell_data_func (cell,
|
||||
format_percentage_col, 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, [-1, -1])
|
||||
|
||||
self.store.append ((i,
|
||||
line[0] / n_stacks,
|
||||
line[1] / n_stacks,
|
||||
text))
|
||||
|
||||
i += 1
|
||||
|
||||
self.select (max (lines.items (), key = lambda line: line[1][1])[0])
|
||||
|
||||
def update_ui (self):
|
||||
sel_rows = self.tree.get_selection ().get_selected_rows ()[1]
|
||||
|
||||
if sel_rows:
|
||||
line = self.store[sel_rows[0]][self.store.LINE]
|
||||
|
||||
i = bisect.bisect_left (self.lines, line)
|
||||
|
||||
self.prev_button.set_sensitive (i > 0)
|
||||
|
||||
if i < len (self.lines) and self.lines[i] == line:
|
||||
i += 1
|
||||
|
||||
self.next_button.set_sensitive (i < len (self.lines))
|
||||
else:
|
||||
self.prev_button.set_sensitive (False)
|
||||
self.next_button.set_sensitive (False)
|
||||
|
||||
samples = self.get_samples ()
|
||||
|
||||
if samples:
|
||||
self.select_samples_button.set_sensitive (True)
|
||||
self.select_samples_button.set_tooltip_text (
|
||||
str (Selection (samples))
|
||||
)
|
||||
else:
|
||||
self.select_samples_button.set_sensitive (False)
|
||||
self.select_samples_button.set_tooltip_text (None)
|
||||
|
||||
def select (self, line):
|
||||
if line is not None:
|
||||
for row in self.store:
|
||||
if row[self.store.LINE] == line:
|
||||
iter = row.iter
|
||||
path = self.store.get_path (iter)
|
||||
|
||||
self.tree.get_selection ().select_iter (iter)
|
||||
|
||||
self.tree.scroll_to_cell (path, None, True, 0.5, 0)
|
||||
|
||||
break
|
||||
else:
|
||||
self.tree.get_selection ().unselect_all ()
|
||||
|
||||
|
||||
def move (self, dir):
|
||||
if dir == 0:
|
||||
return
|
||||
|
||||
sel_rows = self.tree.get_selection ().get_selected_rows ()[1]
|
||||
|
||||
if sel_rows:
|
||||
line = self.store[sel_rows[0]][self.store.LINE]
|
||||
|
||||
i = bisect.bisect_left (self.lines, line)
|
||||
|
||||
if dir < 0:
|
||||
i -= 1
|
||||
elif i < len (self.lines) and self.lines[i] == line:
|
||||
i += 1
|
||||
|
||||
if i >= 0 and i < len (self.lines):
|
||||
self.select (self.lines[i])
|
||||
else:
|
||||
self.select (None)
|
||||
|
||||
def select_samples_clicked (self, button):
|
||||
selection.select (self.get_samples ())
|
||||
|
||||
selection.change_complete ()
|
||||
|
||||
def view_source_clicked (self, button):
|
||||
line = 0
|
||||
|
||||
sel_rows = self.tree.get_selection ().get_selected_rows ()[1]
|
||||
|
||||
if sel_rows:
|
||||
line = self.store[sel_rows[0]][self.store.LINE]
|
||||
|
||||
run_editor (self.file, line)
|
||||
|
||||
def get_path (self):
|
||||
tree_sel = self.tree.get_selection ()
|
||||
|
||||
sel_rows = tree_sel.get_selected_rows ()[1]
|
||||
|
||||
if not sel_rows:
|
||||
return ()
|
||||
|
||||
line = self.store[sel_rows[0]][self.store.LINE]
|
||||
|
||||
return (line,)
|
||||
|
||||
def set_path (self, path):
|
||||
self.select (path[0] if path else None)
|
||||
|
||||
def tree_selection_changed (self, tree_sel):
|
||||
self.update_ui ()
|
||||
|
||||
self.emit ("path-changed")
|
||||
|
||||
def __init__ (self, *args, **kwargs):
|
||||
Gtk.ScrolledWindow.__init__ (
|
||||
self,
|
||||
@ -2782,6 +3133,7 @@ class ProfileViewer (Gtk.ScrolledWindow):
|
||||
profile.connect ("needs-update", self.profile_needs_update)
|
||||
profile.connect ("subprofile-added", self.profile_subprofile_added)
|
||||
profile.connect ("subprofile-removed", self.profile_subprofile_removed)
|
||||
profile.connect ("path-changed", self.profile_path_changed)
|
||||
|
||||
history.add_source (self.source_get, self.source_set)
|
||||
|
||||
@ -2857,6 +3209,13 @@ class ProfileViewer (Gtk.ScrolledWindow):
|
||||
|
||||
history.record ()
|
||||
|
||||
|
||||
def profile_path_changed (self, profile):
|
||||
if not history.is_blocked ():
|
||||
self.path = profile.get_path ()
|
||||
|
||||
history.update ()
|
||||
|
||||
def source_get (self):
|
||||
return self.path
|
||||
|
||||
|
Reference in New Issue
Block a user