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

    Implement flamegraph-describe for non-ELisp profiles
    
    (rbspy and py-spy include file names into the labels.)
---
 flamegraph.el | 183 +++++++++++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 161 insertions(+), 22 deletions(-)

diff --git a/flamegraph.el b/flamegraph.el
index 0b4badac3d..737d0e7e02 100644
--- a/flamegraph.el
+++ b/flamegraph.el
@@ -375,17 +375,20 @@ outermost frames.")
   (flamegraph--goto nil))
 
 (defun flamegraph-describe ()
-  "Describe the function of the frame at point."
+  "Describe the frame at point.
+For Emacs functions this shows the usual function documentation.  For
+other frames it shows a report: the sampled source line in context, the
+frame's sample counts, and buttons to its caller and callees."
   (interactive)
   (let ((frame (flamegraph--frame-at-point)))
     (if (null frame)
         (message "No frame at point")
-      (let ((entry (profiler-calltree-entry
-                    (flamegraph-frame-node frame))))
+      (let* ((node (flamegraph-frame-node frame))
+             (entry (profiler-calltree-entry node)))
         (if (or (and (symbolp entry) (fboundp entry)) (functionp entry))
             (progn (require 'help-fns) (describe-function entry))
-          (message "Not a function: %s"
-                   (flamegraph--entry-name entry)))))))
+          (flamegraph--describe-frame
+           node flamegraph--grand-total flamegraph--directory))))))
 
 (defun flamegraph--entry-location (entry)
   "Extract a (FILE . LINE) source location embedded in ENTRY, or nil.
@@ -395,20 +398,20 @@ in each frame (e.g. \"work (app/main.py:20)\")."
     (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--resolve-source (file directory)
+  "Expand FILE against the configured source directory and return it.
+A relative FILE is resolved against `flamegraph-source-directory', then
+DIRECTORY, then `default-directory'."
+  (expand-file-name file (or flamegraph-source-directory
+                             directory
+                             default-directory)))
+
+(defun flamegraph--visit-file (path line)
+  "Visit PATH at LINE in another window."
+  (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.
@@ -429,12 +432,148 @@ necessarily its definition."
           (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))))
+            (if (null loc)
+                (message "No source location in: %s" entry)
+              (let ((path (flamegraph--resolve-source
+                           (car loc) flamegraph--directory)))
+                (if (file-exists-p path)
+                    (flamegraph--visit-file path (cdr loc))
+                  (message "Source file not found: %s" path))))))
          (t (message "Cannot find definition of: %s"
                      (flamegraph--entry-name entry))))))))
 
+(defun flamegraph--frame-display-name (entry)
+  "Return ENTRY's name with any embedded FILE:LINE location stripped."
+  (if (and (stringp entry)
+           (string-match "\\([^ ()]*\\.[^ ():]+\\):\\([0-9]+\\)" entry))
+      (string-trim-right (substring entry 0 (match-beginning 0)) "[ (-]+")
+    (flamegraph--entry-name entry)))
+
+(defun flamegraph--snippet-line (n marked)
+  "Format buffer line N for the snippet, marking it when MARKED."
+  (format " %s %4d  %s"
+          (if marked "▸" " ")
+          n
+          (buffer-substring (line-beginning-position) (line-end-position))))
+
+(defun flamegraph--source-snippet (path line &optional context)
+  "Return source context around LINE of PATH as a fontified string, or nil.
+The file is loaded in its major mode so the text is syntax-highlighted.
+Besides CONTEXT lines on each side of the sampled LINE (which is marked),
+the enclosing structural lines up to the definition are kept — each line
+that is less indented than the one below it — so the nesting that leads
+to the sampled line stays visible.  Skipped runs are elided with `⋯'.
+CONTEXT defaults to 4."
+  (setq context (or context 4))
+  (when (and (file-readable-p path) (> line 0))
+    (with-temp-buffer
+      (insert-file-contents path)
+      (let ((buffer-file-name path)
+            (enable-local-variables nil))
+        (delay-mode-hooks (set-auto-mode)))
+      (let ((maxline (line-number-at-pos (point-max))))
+        (when (<= line maxline)
+          (let* ((lo (max 1 (- line context)))
+                 (hi (min maxline (+ line context)))
+                 (def (save-excursion
+                        (goto-char (point-min))
+                        (forward-line (1- line))
+                        (let ((p (point)))
+                          (ignore-errors (beginning-of-defun))
+                          (if (< (point) p) (line-number-at-pos) 1))))
+                 (keep nil))
+            ;; The sampled line and its immediate context.
+            (cl-loop for n from lo to hi do (push n keep))
+            ;; Enclosing structural lines (each less indented than the one
+            ;; below) from the sampled line up to the definition.
+            (save-excursion
+              (goto-char (point-min))
+              (forward-line (1- line))
+              (let ((min-ind (current-indentation))
+                    (n line))
+                (while (> n def)
+                  (forward-line -1)
+                  (setq n (1- n))
+                  (unless (looking-at-p "[ \t]*$")
+                    (let ((ind (current-indentation)))
+                      (when (< ind min-ind)
+                        (push n keep)
+                        (setq min-ind ind)))))))
+            (push def keep)
+            (setq keep (sort (delete-dups keep) #'<))
+            ;; Render kept lines in order, eliding the gaps with `⋯'.
+            (let ((prev nil) (out nil))
+              (dolist (n keep)
+                (when (and prev (> n (1+ prev)))
+                  (push "      ⋯" out))
+                (goto-char (point-min))
+                (forward-line (1- n))
+                (ignore-errors
+                  (font-lock-ensure (line-beginning-position)
+                                    (line-end-position)))
+                (push (flamegraph--snippet-line n (= n line)) out)
+                (setq prev n))
+              (mapconcat #'identity (nreverse out) "\n"))))))))
+
+(defun flamegraph--describe-button (node total directory)
+  "Insert a button that describes NODE, passing TOTAL and DIRECTORY along."
+  (insert-text-button (flamegraph--frame-display-name
+                       (profiler-calltree-entry node))
+                      'type 'help-xref
+                      'help-function #'flamegraph--describe-frame
+                      'help-args (list node total directory)))
+
+(defun flamegraph--describe-frame (node total directory)
+  "Show a report for calltree NODE in the *Help* buffer.
+TOTAL is the grand total for percentages; DIRECTORY resolves relative
+source paths.  Caller and callee names are buttons describing those
+frames, with Help-style back/forward navigation."
+  (require 'help-mode)
+  (help-setup-xref (list #'flamegraph--describe-frame node total directory)
+                   (called-interactively-p 'interactive))
+  (let* ((entry (profiler-calltree-entry node))
+         (loc (and (stringp entry) (flamegraph--entry-location entry)))
+         (path (and loc (flamegraph--resolve-source (car loc) directory)))
+         (count (profiler-calltree-count node))
+         (kids (sort (copy-sequence (profiler-calltree-children node))
+                     (lambda (a b) (> (profiler-calltree-count a)
+                                      (profiler-calltree-count b)))))
+         (self (- count (apply #'+ 0 (mapcar #'profiler-calltree-count kids))))
+         (parent (profiler-calltree-parent node)))
+    (with-help-window (help-buffer)
+      (with-current-buffer standard-output
+        (insert (flamegraph--frame-display-name entry))
+        (when loc
+          (insert "  ")
+          (insert-text-button
+           (format "%s:%d" (car loc) (cdr loc))
+           'action (lambda (_)
+                     (if (file-exists-p path)
+                         (flamegraph--visit-file path (cdr loc))
+                       (message "Source file not found: %s" path)))
+           'follow-link t
+           'help-echo "mouse-1, RET: visit this line")
+          (when-let* ((snippet (flamegraph--source-snippet path (cdr loc))))
+            (insert "\n\n" snippet)))
+        (insert (format "\n\nSamples  %s (%s of total)    self  %s (%s)\n"
+                        (profiler-format-number count)
+                        (flamegraph--percent count total)
+                        (profiler-format-number self)
+                        (flamegraph--percent self total)))
+        (when (and parent (profiler-calltree-entry parent))
+          (insert "\nCalled from  ")
+          (flamegraph--describe-button parent total directory)
+          (insert "\n"))
+        (when kids
+          (insert "\nCalls\n")
+          (dolist (k kids)
+            (insert (format "  %7s  " (profiler-format-number
+                                       (profiler-calltree-count k))))
+            (flamegraph--describe-button k total directory)
+            (insert (format "  (%s)\n"
+                            (flamegraph--percent
+                             (profiler-calltree-count k) count)))))))))
+
 (defun flamegraph-redraw ()
   "Redraw the flame graph, e.g. to pick up a new window width."
   (interactive)

Reply via email to