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

    Better highlighting of nested call sites in the source snippet
    
    Doing a deep search through the tree, until one fails to match.
---
 flamegraph.el | 156 ++++++++++++++++++++++++++++++++++++++++++----------------
 1 file changed, 113 insertions(+), 43 deletions(-)

diff --git a/flamegraph.el b/flamegraph.el
index 5a9cea6295..c8dcbe8108 100644
--- a/flamegraph.el
+++ b/flamegraph.el
@@ -540,17 +540,17 @@ necessarily its definition."
           n
           (buffer-substring (line-beginning-position) (line-end-position))))
 
-(defun flamegraph--source-snippet (path line &optional names context)
+(defun flamegraph--source-snippet (path line &optional find-regions 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.  NAMES is a list of callee names: lines
-within the enclosing defun where a name occurs as a whole symbol are also
-kept, and the occurrences highlighted with `flamegraph-call-site', so the
-call sites of a frame's children stay visible.  Skipped runs are elided
-with `⋯'.  CONTEXT defaults to 4."
+to the sampled line stays visible.  FIND-REGIONS, if non-nil, is called
+with the enclosing defun's start and end buffer positions; it returns
+a list of (BEG . END) ranges within the loaded buffer to highlight with
+`flamegraph-call-site'.  Their lines are kept as well.  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
@@ -574,17 +574,8 @@ with `⋯'.  CONTEXT defaults to 4."
                             (goto-char def-pos)
                             (ignore-errors (end-of-defun))
                             (if (> (point) def-pos) (point) (point-max))))
-                 ;; Anything that's plausibly a symbol — no whitespace,
-                 ;; brackets, parens, comma, semicolon.  Permissive on
-                 ;; purpose: Lisp names use `-' (and friends), C names use
-                 ;; `_'; `\_<...\_>' below enforces real symbol boundaries
-                 ;; in the target buffer's syntax.
-                 (names (cl-remove-if-not
-                         (lambda (n)
-                           (and (stringp n) (not (string-empty-p n))
-                                (not (string-match-p
-                                      "[][[:space:]();,]" n))))
-                         names))
+                 (regions (and find-regions
+                               (funcall find-regions def-pos def-end)))
                  (keep nil))
             ;; The sampled line and its immediate context.
             (cl-loop for n from lo to hi do (push n keep))
@@ -604,13 +595,9 @@ with `⋯'.  CONTEXT defaults to 4."
                         (push n keep)
                         (setq min-ind ind)))))))
             (push def keep)
-            ;; Callee call sites anywhere in the enclosing defun.
-            (dolist (name names)
-              (save-excursion
-                (goto-char def-pos)
-                (let ((re (concat "\\_<" (regexp-quote name) "\\_>")))
-                  (while (re-search-forward re def-end t)
-                    (push (line-number-at-pos (match-beginning 0)) keep)))))
+            ;; Lines containing a callee match.
+            (dolist (r regions)
+              (push (line-number-at-pos (car r)) keep))
             (setq keep (sort (delete-dups keep) #'<))
             ;; Render kept lines in order, eliding the gaps with `⋯'.
             (let ((prev nil) (out nil))
@@ -622,19 +609,109 @@ with `⋯'.  CONTEXT defaults to 4."
                 (ignore-errors
                   (font-lock-ensure (line-beginning-position)
                                     (line-end-position)))
-                ;; Highlight callee occurrences on this line, additively
-                ;; over the font-lock faces.
-                (dolist (name names)
-                  (goto-char (line-beginning-position))
-                  (let ((re (concat "\\_<" (regexp-quote name) "\\_>"))
-                        (eol (line-end-position)))
-                    (while (re-search-forward re eol t)
-                      (add-face-text-property (match-beginning 0) (match-end 0)
+                ;; Apply highlights to any regions that fall on this line
+                ;; — additively over the font-lock faces.
+                (let ((bol (line-beginning-position))
+                      (eol (line-end-position)))
+                  (dolist (r regions)
+                    (when (and (<= bol (car r)) (>= eol (cdr r)))
+                      (add-face-text-property (car r) (cdr r)
                                               'flamegraph-call-site))))
                 (push (flamegraph--snippet-line n (= n line)) out)
                 (setq prev n))
               (mapconcat #'identity (nreverse out) "\n"))))))))
 
+(defun flamegraph--skip-through-p (entry)
+  "Non-nil if ENTRY is a control construct to descend through when
+looking for a frame's real callees in source (e.g. `if', `let',
+`progn').  In an Elisp profile these special-form frames sit between a
+function and its actual callees and are not useful as call sites."
+  (and (symbolp entry) (special-form-p entry)))
+
+(defun flamegraph--enclosing-form-region (pos)
+  "Return (BEG . END) of the form surrounding the callee name at POS.
+Uses `backward-up-list' to find the parenthesized group POS sits in
+and `forward-sexp' to bound it.  Returns nil if no enclosing form
+exists (top level)."
+  (save-excursion
+    (goto-char pos)
+    (condition-case nil
+        (let ((beg (progn (backward-up-list) (point))))
+          (cons beg (progn (forward-sexp) (point))))
+      (error nil))))
+
+(defun flamegraph--callee-name-acceptable-p (name)
+  "Whether NAME is plausibly a symbol we can usefully search in source.
+Rejects empty strings and anything containing whitespace, brackets,
+parens, comma, or semicolon — `\\=\\_<...\\=\\_>' below enforces real
+symbol boundaries in the target buffer's syntax."
+  (and (stringp name) (not (string-empty-p name))
+       (not (string-match-p "[][[:space:]();,]" name))))
+
+(defun flamegraph--collect-nested-call-sites (node beg end)
+  "Walk NODE's call subtree against the buffer region [BEG END].
+For each child whose display name matches `\\=\\_<NAME\\=\\_>' in the
+current region, record the match as a highlight region (unless its
+entry is a skip-through) and recurse into the child with the matched
+call's enclosing form as the new search bound.
+
+Skip-through (special-form) children are PURELY TRANSPARENT: not
+searched, never narrow the region, just descended through.  Their
+textual presence in source is unreliable as an anchor — the profile
+records post-macro-expansion frames whose names may exist in source
+in completely unrelated forms (a literal `(if …)' elsewhere), or
+may not exist at all (`dolist' → `while'/`let').  Only real function
+matches narrow, which is what preserves structural attribution.
+
+Returns a list of (POS-BEG . POS-END) regions to highlight."
+  (let (regions)
+    (cl-labels
+        ((walk (n region)
+           (dolist (k (profiler-calltree-children n))
+             (let* ((entry (profiler-calltree-entry k))
+                    (name (flamegraph--frame-display-name entry))
+                    (skip (flamegraph--skip-through-p entry)))
+               (cond
+                (skip
+                 ;; Special form — transparent descend, no narrowing.
+                 (walk k region))
+                ((flamegraph--callee-name-acceptable-p name)
+                 ;; Real function — search in the current region.
+                 ;; Each match is highlighted and narrows the search
+                 ;; for this child's own children.
+                 (save-excursion
+                   (goto-char (car region))
+                   (let ((re (concat "\\_<" (regexp-quote name) "\\_>")))
+                     (while (re-search-forward re (cdr region) t)
+                       (let ((mb (match-beginning 0))
+                             (me (match-end 0)))
+                         (push (cons mb me) regions)
+                         (when-let* ((sub (flamegraph--enclosing-form-region
+                                           mb)))
+                           (walk k sub))))))))))))
+      (walk node (cons beg end)))
+    (nreverse regions)))
+
+(defun flamegraph--insert-call-tree (node total ref depth directory)
+  "Insert NODE's children as a tree, expanding a skip-through child
+inline so the path to its real callees is visible.  TOTAL is the grand
+total for the call graph; REF is the count of the originally-described
+frame, used so the printed percentages add up under that frame.  DEPTH
+controls the indentation."
+  (let ((kids (sort (copy-sequence (profiler-calltree-children node))
+                    (lambda (a b) (> (profiler-calltree-count a)
+                                     (profiler-calltree-count b))))))
+    (dolist (k kids)
+      (insert (make-string (+ 2 (* 2 depth)) ?\s))
+      (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) ref)))
+      (when (flamegraph--skip-through-p (profiler-calltree-entry k))
+        (flamegraph--insert-call-tree k total ref (1+ depth) directory)))))
+
 (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
@@ -684,10 +761,9 @@ frames, with Help-style back/forward navigation."
            'help-echo "mouse-1, RET: visit this line")
           (when-let* ((snippet (flamegraph--source-snippet
                                 path (cdr loc)
-                                (mapcar (lambda (k)
-                                          (flamegraph--frame-display-name
-                                           (profiler-calltree-entry k)))
-                                        kids))))
+                                (lambda (b e)
+                                  (flamegraph--collect-nested-call-sites
+                                   node b e)))))
             (insert "\n\n" snippet)))
         (insert (format "\n\nSamples  %s (%s of total)    self  %s (%s)\n"
                         (profiler-format-number count)
@@ -700,13 +776,7 @@ frames, with Help-style back/forward navigation."
           (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)))))))))
+          (flamegraph--insert-call-tree node total count 0 directory))))))
 
 (defun flamegraph-redraw ()
   "Redraw the flame graph, e.g. to pick up a new window width."

Reply via email to