[gimp] tools: in performance-log-viewer.py, add annotated source view



commit 88438c50558533e1b0626824d9d516e6bda36037
Author: Ell <ell_se yahoo com>
Date:   Sun Sep 30 08:54:34 2018 -0400

    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.

 tools/performance-log-viewer.py | 401 +++++++++++++++++++++++++++++++++++++---
 1 file changed, 380 insertions(+), 21 deletions(-)
---
diff --git a/tools/performance-log-viewer.py b/tools/performance-log-viewer.py
index 6d8254d608..d210448fe5 100755
--- a/tools/performance-log-viewer.py
+++ b/tools/performance-log-viewer.py
@@ -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
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]