branch: elpa/flamegraph
commit 6441288bf3a0631a18cc4ad7b7c727816cc983ba
Author: Dmitry Gutov <[email protected]>
Commit: Dmitry Gutov <[email protected]>

    flamegraph-find: New command
---
 flamegraph.el | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 65 insertions(+), 3 deletions(-)

diff --git a/flamegraph.el b/flamegraph.el
index 93cc441f0d..0b4badac3d 100644
--- a/flamegraph.el
+++ b/flamegraph.el
@@ -37,6 +37,7 @@
 ;;   n / TAB         move to the next frame
 ;;   p / S-TAB       move to the previous frame
 ;;   d               describe the function at point
+;;   f               visit the frame's source (definition or file:line)
 ;;   g               redraw (e.g. after resizing the window)
 ;;   q               quit
 ;;
@@ -70,6 +71,13 @@ Separates adjacent frames visually.  0 disables it.  On a 
text
 terminal a \"pixel\" is one column."
   :type 'natnum)
 
+(defcustom flamegraph-source-directory nil
+  "Directory to resolve relative source paths against when visiting a frame.
+Used by \\[flamegraph-find] for folded stacks whose frames embed a
+FILE:LINE location.  If nil, paths are resolved against the directory of
+the data file the graph was loaded from."
+  :type '(choice (const :tag "Data file's directory" nil) directory))
+
 ;;; Layout: call tree -> flat list of frames
 
 (cl-defstruct (flamegraph-frame
@@ -247,6 +255,8 @@ outermost frames.")
   "Count unit, as a string (e.g. \"samples\" or \"bytes\").")
 (defvar-local flamegraph--title ""
   "Short label for the data source, shown in the header.")
+(defvar-local flamegraph--directory nil
+  "Directory of the data file, for resolving relative source paths.")
 
 (defun flamegraph--header ()
   "Return the header line for the current view."
@@ -377,6 +387,54 @@ outermost frames.")
           (message "Not a function: %s"
                    (flamegraph--entry-name entry)))))))
 
+(defun flamegraph--entry-location (entry)
+  "Extract a (FILE . LINE) source location embedded in ENTRY, or nil.
+Recognizes the FILE:LINE form that profilers such as py-spy and rbspy put
+in each frame (e.g. \"work (app/main.py:20)\")."
+  (when (string-match "\\([^ ()]*\\.[^ ():]+\\):\\([0-9]+\\)" entry)
+    (cons (match-string 1 entry)
+          (string-to-number (match-string 2 entry)))))
+
+(defun flamegraph--visit-location (file line)
+  "Visit FILE at LINE in another window.
+A relative FILE is resolved against `flamegraph-source-directory', or the
+data file's directory, or `default-directory'."
+  (let ((path (expand-file-name
+               file (or flamegraph-source-directory
+                        flamegraph--directory
+                        default-directory))))
+    (if (not (file-exists-p path))
+        (message "Source file not found: %s" path)
+      (find-file-other-window path)
+      (when (> line 0)
+        (goto-char (point-min))
+        (forward-line (1- line))))))
+
+(defun flamegraph-find ()
+  "Visit the source of the function of the frame at point.
+For Emacs functions this jumps to the definition.  For folded stacks
+whose frames embed a FILE:LINE location (as py-spy and rbspy emit), it
+visits that file and line.  Note that this is the line the frame was
+executing when sampled, which is somewhere in the function body, not
+necessarily its definition."
+  (interactive)
+  (let ((frame (flamegraph--frame-at-point)))
+    (if (null frame)
+        (message "No frame at point")
+      (let ((entry (profiler-calltree-entry
+                    (flamegraph-frame-node frame))))
+        (cond
+         ((and (symbolp entry) (fboundp entry))
+          (require 'find-func)
+          (find-function-other-window entry))
+         ((stringp entry)
+          (let ((loc (flamegraph--entry-location entry)))
+            (if loc
+                (flamegraph--visit-location (car loc) (cdr loc))
+              (message "No source location in: %s" entry))))
+         (t (message "Cannot find definition of: %s"
+                     (flamegraph--entry-name entry))))))))
+
 (defun flamegraph-redraw ()
   "Redraw the flame graph, e.g. to pick up a new window width."
   (interactive)
@@ -416,6 +474,7 @@ Do nothing when the echo area already shows an unrelated 
message."
   "p"         #'flamegraph-previous
   "<backtab>" #'flamegraph-previous
   "d"         #'flamegraph-describe
+  "f"         #'flamegraph-find
   "g"         #'flamegraph-redraw
   "q"         #'quit-window)
 
@@ -428,11 +487,12 @@ Do nothing when the echo area already shows an unrelated 
message."
 
 ;;; Entry points
 
-(defun flamegraph--show (top unit title)
+(defun flamegraph--show (top unit title &optional directory)
   "Display a flame graph for calltree TOP and return its buffer.
 TOP is a dummy root node whose children are the outermost frames.  UNIT
 names the count unit (e.g. \"samples\"); TITLE labels the data source in
-the header."
+the header.  DIRECTORY, if given, resolves relative source paths when
+visiting a frame."
   (let ((buffer (get-buffer-create (format "*Flamegraph: %s*" title))))
     (with-current-buffer buffer
       (flamegraph-mode)
@@ -443,6 +503,7 @@ the header."
             flamegraph--forward nil
             flamegraph--unit unit
             flamegraph--title title
+            flamegraph--directory directory
             flamegraph--grand-total
             (max 1 (apply #'+ (mapcar #'profiler-calltree-count
                                       (profiler-calltree-children top))))))
@@ -520,7 +581,8 @@ frames separated by \";\" (outermost first), then a space 
and a count."
   (interactive (list (read-file-name "Folded stacks file: " nil nil t)))
   (flamegraph--show (flamegraph--read-folded filename)
                     "samples"
-                    (file-name-nondirectory filename)))
+                    (file-name-nondirectory filename)
+                    (file-name-directory (expand-file-name filename))))
 
 (provide 'flamegraph)
 ;;; flamegraph.el ends here

Reply via email to