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

    Hide matched callees' bodies in the Calls tree
    
    The tree showed a matched function's own body (its internal calls), e.g.
    the whole find-function-noselect subtree under flamegraph--symbol-location.
    
    Mark a "shown" set during the source walk: a frame absent from source is
    shown only when concealed by a macro in the current definition (its parent
    is scaffolding), not when it is a matched call's body.  insert-call-tree
    now gates printing on that set, falling back to a flat list when there is
    no source to attribute against.
---
 flamegraph.el            | 108 +++++++++++++++++++++++++++--------------------
 test/flamegraph-tests.el |  96 +++++++++++++++++++++++++++++------------
 2 files changed, 131 insertions(+), 73 deletions(-)

diff --git a/flamegraph.el b/flamegraph.el
index 6aa1f5434b..ff5b879ec1 100644
--- a/flamegraph.el
+++ b/flamegraph.el
@@ -564,7 +564,7 @@ necessarily its definition."
           n
           (buffer-substring (line-beginning-position) (line-end-position))))
 
-(defun flamegraph--source-snippet (path line &optional node reached context)
+(defun flamegraph--source-snippet (path line &optional node shown 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),
@@ -573,7 +573,7 @@ that is less indented than the one below it — so the 
nesting that leads
 to the sampled line stays visible.  When NODE (a calltree) is non-nil, its
 nested calls found in the enclosing defun are highlighted with a heat face
 and their lines kept too (see `flamegraph--collect-nested-call-sites',
-which also fills REACHED).  Skipped runs are elided with `⋯'.  CONTEXT
+which also fills SHOWN).  Skipped runs are elided with `⋯'.  CONTEXT
 defaults to 4."
   (setq context (or context 4))
   (when (and (file-readable-p path) (> line 0))
@@ -600,7 +600,7 @@ defaults to 4."
                             (if (> (point) def-pos) (point) (point-max))))
                  (regions (and node
                                (flamegraph--collect-nested-call-sites
-                                node def-pos def-end reached)))
+                                node def-pos def-end shown)))
                  (keep nil))
             ;; The sampled line and its immediate context.
             (cl-loop for n from lo to hi do (push n keep))
@@ -679,7 +679,7 @@ 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 &optional reached)
+(defun flamegraph--collect-nested-call-sites (node beg end &optional shown)
   "Walk NODE's call subtree against the buffer region [BEG END],
 recording each child whose display name matches `\\=\\_<NAME\\=\\_>' in
 the current region as a highlight region.
@@ -695,33 +695,43 @@ of NODE's count, for heat styling.  Callees below
 `flamegraph-call-site-threshold' are omitted from the regions, but still
 traversed.
 
-REACHED, if non-nil, is a hash table into which every node with a child
-found in the source is marked; `flamegraph--insert-call-tree' uses it to
-nest the \"Calls\" tree.  It ignores the threshold, so the tree may show
-cold callees the snippet omits."
+SHOWN, if non-nil, is a hash table into which every node to display in
+the \"Calls\" tree is marked: a skip-through that leads to a shown call,
+a function matched in the source, or a function not in the source but
+whose parent is scaffolding (a skip-through, or the described frame
+itself) — i.e. a macro-concealed call of the current definition.  A
+function not in the source whose parent is a *matched* call is the
+callee's own body and is omitted.  SHOWN ignores the threshold, so the
+tree may show cold callees the snippet omits."
   (let ((total (max 1 (profiler-calltree-count node)))
         regions)
     (cl-labels
-        ;; Walk N's children against REGION; return non-nil if any of them
-        ;; is found in source (so N's callees are worth showing nested).
-        ((walk (n region)
-           (let ((reachable nil))
+        ;; Walk N's children against REGION.  CONCEALED-OK means a child
+        ;; not found in source is current-definition code hidden by a macro
+        ;; (N is scaffolding), so still shown; otherwise it is N's callee
+        ;; body.  Return non-nil if anything in the subtree was shown.
+        ((walk (n region concealed-ok)
+           (let ((any nil))
              (dolist (k (profiler-calltree-children n))
                (let* ((entry (profiler-calltree-entry k))
                       (name (flamegraph--frame-display-name entry))
                       (weight (/ (float (profiler-calltree-count k)) total)))
                  (cond
                   ((flamegraph--skip-through-p entry)
-                   ;; Transparent: descend without narrowing.
-                   (when (walk k region)
-                     (when reached (puthash k t reached))
-                     (setq reachable t)))
+                   ;; Scaffolding: shown only if it leads to a shown call.
+                   ;; It conceals current-definition code only where the
+                   ;; surrounding context already does (CONCEALED-OK), not
+                   ;; inside a matched callee's body.
+                   (when (walk k region concealed-ok)
+                     (when shown (puthash k t shown))
+                     (setq any t)))
                   ((flamegraph--callee-name-acceptable-p name)
-                   ;; A match narrows the region for this child's own walk.
                    (save-excursion
                      (goto-char (car region))
                      (let ((re (concat "\\_<" (regexp-quote name) "\\_>"))
                            (found nil))
+                       ;; A match narrows the region for this child's own
+                       ;; walk, whose callee-body children are then hidden.
                        (while (re-search-forward re (cdr region) t)
                          (setq found t)
                          (let ((mb (match-beginning 0))
@@ -730,39 +740,40 @@ cold callees the snippet omits."
                              (push (list mb me weight) regions))
                            (when-let* ((sub (flamegraph--enclosing-form-region
                                              mb)))
-                             (when (and (walk k sub) reached)
-                               (puthash k t reached)))))
-                       (when found (setq reachable t))))))))
-             reachable)))
-      (walk node (cons beg end)))
+                             (walk k sub nil))))
+                       (when (or found concealed-ok)
+                         (when shown (puthash k t shown))
+                         (setq any t))))))))
+             any)))
+      (walk node (cons beg end) t))
     (nreverse regions)))
 
-(defun flamegraph--insert-call-tree (node total ref depth directory reached)
-  "Insert NODE's children as a tree.
-A child is expanded inline (its own callees shown indented) only when it
-is a key in REACHED, the table from `flamegraph--collect-nested-call-sites'.
-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."
+(defun flamegraph--insert-call-tree (node total ref depth directory shown)
+  "Insert NODE's children as a tree, those in SHOWN nested under it.
+SHOWN is the table from `flamegraph--collect-nested-call-sites'.  A child
+absent from it is skipped, along with its subtree; these are a callee's
+internal calls.  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 (format "%7s  " (profiler-format-number
-                               (profiler-calltree-count k))))
-      (insert (make-string (* 2 depth) ?\s))
-      ;; Dim callees below the threshold: they are absent from the snippet
-      ;; (see `flamegraph-call-site-threshold'), so mark why here.
-      (flamegraph--describe-button
-       k total directory
-       (< (/ (float (profiler-calltree-count k)) ref)
-          flamegraph-call-site-threshold))
-      (insert (format "  (%s)\n"
-                      (flamegraph--percent
-                       (profiler-calltree-count k) ref)))
-      (when (gethash k reached)
+      (when (gethash k shown)
+        (insert (format "%7s  " (profiler-format-number
+                                 (profiler-calltree-count k))))
+        (insert (make-string (* 2 depth) ?\s))
+        ;; Dim callees below the threshold: they are absent from the snippet
+        ;; (see `flamegraph-call-site-threshold'), so mark why here.
+        (flamegraph--describe-button
+         k total directory
+         (< (/ (float (profiler-calltree-count k)) ref)
+            flamegraph-call-site-threshold))
+        (insert (format "  (%s)\n"
+                        (flamegraph--percent
+                         (profiler-calltree-count k) ref)))
         (flamegraph--insert-call-tree k total ref (1+ depth) directory
-                                      reached)))))
+                                      shown)))))
 
 (defun flamegraph--describe-button (node total directory &optional dim)
   "Insert a button that describes NODE, passing TOTAL and DIRECTORY along.
@@ -795,7 +806,7 @@ frames, with Help-style back/forward navigation."
                                       (profiler-calltree-count b)))))
          (self (- count (apply #'+ 0 (mapcar #'profiler-calltree-count kids))))
          (parent (profiler-calltree-parent node))
-         (reached (make-hash-table :test 'eq)))
+         (shown (make-hash-table :test 'eq)))
     (with-help-window (help-buffer)
       (with-current-buffer standard-output
         (if (and (symbolp entry) (fboundp entry))
@@ -815,7 +826,7 @@ frames, with Help-style back/forward navigation."
            'follow-link t
            'help-echo "mouse-1, RET: visit this line")
           (when-let* ((snippet (flamegraph--source-snippet
-                                path (cdr loc) node reached)))
+                                path (cdr loc) node shown)))
             (insert "\n\n" snippet)))
         (insert (format "\n\nSamples  %s (%s of total)    self  %s (%s)\n"
                         (profiler-format-number count)
@@ -827,9 +838,14 @@ frames, with Help-style back/forward navigation."
           (flamegraph--describe-button parent total directory)
           (insert "\n"))
         (when kids
+          ;; With no source to attribute against (e.g. a C frame, or a
+          ;; missing file), nothing was marked: fall back to the direct
+          ;; children as a flat list.
+          (when (zerop (hash-table-count shown))
+            (dolist (k kids) (puthash k t shown)))
           (insert "\nCalls\n")
           (flamegraph--insert-call-tree node total count 0 directory
-                                        reached))))))
+                                        shown))))))
 
 (defun flamegraph-redraw ()
   "Redraw the flame graph, e.g. to pick up a new window width."
diff --git a/test/flamegraph-tests.el b/test/flamegraph-tests.el
index 816b06ece7..dda20a1547 100644
--- a/test/flamegraph-tests.el
+++ b/test/flamegraph-tests.el
@@ -193,10 +193,11 @@ callees outside it must still be found."
                      outer (point-min) (point-max))))
       (should (equal (flamegraph-test--region-texts regions) '("foo"))))))
 
-(ert-deftest flamegraph-test-walker-reached-tracks-source-nesting ()
-  "REACHED marks a node iff a nested call of it is found in the source.
-`wrap' is found and its child `inner' is found inside it -> reached;
-`leaf' is found but its child is not in source -> not reached."
+(ert-deftest flamegraph-test-walker-shown-marks-source-calls ()
+  "SHOWN marks calls found in the source and their source-found nesting,
+but not a matched callee's own body.
+`wrap', `inner' and `leaf' are in the source -> shown; `inner' nests
+under `wrap'.  `not-in-source' is `leaf''s body -> not shown."
   (flamegraph-test--with-elisp-source
       "(defun outer ()
   (wrap (inner))
@@ -209,13 +210,15 @@ callees outside it must still be found."
                                           :children (list hidden)))
            (outer (profiler-make-calltree :entry 'outer :count 20
                                           :children (list wrap leaf)))
-           (reached (make-hash-table :test 'eq))
+           (shown (make-hash-table :test 'eq))
            (regions (flamegraph--collect-nested-call-sites
-                     outer (point-min) (point-max) reached)))
+                     outer (point-min) (point-max) shown)))
       (should (equal (flamegraph-test--region-texts regions)
                      '("wrap" "inner" "leaf")))
-      (should (gethash wrap reached))
-      (should-not (gethash leaf reached)))))
+      (should (gethash wrap shown))
+      (should (gethash inner shown))
+      (should (gethash leaf shown))
+      (should-not (gethash hidden shown)))))
 
 (ert-deftest flamegraph-test-walker-below-threshold-dropped ()
   "Callees below `flamegraph-call-site-threshold' are dropped with subtree.
@@ -236,6 +239,37 @@ hot at 50% is kept; cold at 1% (and its child) are not."
       ;; hot (50%) kept; deep is 1% of outer -> dropped; cold (1%) dropped.
       (should (equal (flamegraph-test--region-texts regions) '("hot"))))))
 
+(ert-deftest flamegraph-test-walker-hides-matched-callee-body ()
+  "A call found in source hides its own body, even through skip-throughs;
+a call concealed by a macro (under a skip-through in the described body)
+is still shown."
+  (flamegraph-test--with-elisp-source
+      "(defun outer ()
+  (when flag
+    (concealed))
+  (matched))"
+    (let* ((;; `matched' body: matched -> if -> deep-fn  (callee internals)
+            deep-fn (profiler-make-calltree :entry 'deep-fn :count 40))
+           (body-if (profiler-make-calltree :entry 'if :count 40
+                                            :children (list deep-fn)))
+           (matched (profiler-make-calltree :entry 'matched :count 40
+                                            :children (list body-if)))
+           ;; `when' expands to `if' (skip-through) in the described body,
+           ;; concealing the real call `concealed'.
+           (concealed (profiler-make-calltree :entry 'concealed :count 30))
+           (when-if (profiler-make-calltree :entry 'if :count 30
+                                            :children (list concealed)))
+           (outer (profiler-make-calltree :entry 'outer :count 100
+                                          :children (list matched when-if)))
+           (shown (make-hash-table :test 'eq)))
+      (flamegraph--collect-nested-call-sites
+       outer (point-min) (point-max) shown)
+      (should (gethash matched shown))      ; in source
+      (should (gethash when-if shown))      ; scaffolding leading to a call
+      (should (gethash concealed shown))    ; macro-concealed current-def call
+      (should-not (gethash body-if shown))  ; matched's body
+      (should-not (gethash deep-fn shown))))) ; matched's body, deeper
+
 ;;; `flamegraph--symbol-location'
 
 (ert-deftest flamegraph-test-symbol-location ()
@@ -291,14 +325,20 @@ foo at 100% of outer is hot; bar at 10% is warm."
 
 ;;; Calls-tree rendering — `flamegraph--insert-call-tree'
 
-(defun flamegraph-test--render-call-tree (node ref &optional reached-nodes)
+(defun flamegraph-test--render-call-tree (node ref &optional shown-nodes)
   "Render NODE's call tree (REF samples for percent scaling) and return
-the resulting string.  REACHED-NODES are the calltrees to mark as
-expandable (their children shown nested)."
-  (let ((reached (make-hash-table :test 'eq)))
-    (dolist (n reached-nodes) (puthash n t reached))
+the resulting string.  Only nodes in SHOWN-NODES are rendered; when it is
+nil, every descendant of NODE is shown."
+  (let ((shown (make-hash-table :test 'eq)))
+    (if shown-nodes
+        (dolist (n shown-nodes) (puthash n t shown))
+      (cl-labels ((mark (n)
+                    (dolist (k (profiler-calltree-children n))
+                      (puthash k t shown)
+                      (mark k))))
+        (mark node)))
     (with-temp-buffer
-      (flamegraph--insert-call-tree node ref ref 0 nil reached)
+      (flamegraph--insert-call-tree node ref ref 0 nil shown)
       (buffer-string))))
 
 (defun flamegraph-test--line-with (str lines)
@@ -326,16 +366,16 @@ expandable (their children shown nested)."
       (should bar-pos)
       (should (< baz-pos bar-pos)))))
 
-(ert-deftest flamegraph-test-call-tree-reached-expanded-indented ()
-  "A reached child renders its sub-children indented underneath; an
-unreached sibling does not, and stays at the top depth."
+(ert-deftest flamegraph-test-call-tree-shown-child-indented ()
+  "A shown child renders its shown sub-children indented underneath."
   (let* ((real-child (profiler-make-calltree :entry 'real-fn :count 80))
          (if-node    (profiler-make-calltree :entry 'if :count 80
                                              :children (list real-child)))
          (other      (profiler-make-calltree :entry 'other-fn :count 20))
          (root (profiler-make-calltree :entry 'root :count 100
                                        :children (list if-node other))))
-    (let* ((out (flamegraph-test--render-call-tree root 100 (list if-node)))
+    (let* ((out (flamegraph-test--render-call-tree
+                 root 100 (list if-node real-child other)))
            (lines (split-string out "\n" t))
            (if-line    (flamegraph-test--line-with "if" lines))
            (rf-line    (flamegraph-test--line-with "real-fn" lines))
@@ -343,22 +383,22 @@ unreached sibling does not, and stays at the top depth."
       (should if-line)
       (should rf-line)
       (should other-line)
-      ;; real-fn is indented deeper than its reached parent.
+      ;; real-fn is indented deeper than its parent.
       (should (> (flamegraph-test--indent rf-line)
                  (flamegraph-test--indent if-line)))
-      ;; The unreached sibling sits at the same depth as `if'.
+      ;; The sibling sits at the same depth as `if'.
       (should (= (flamegraph-test--indent if-line)
                  (flamegraph-test--indent other-line))))))
 
-(ert-deftest flamegraph-test-call-tree-unreached-not-expanded ()
-  "A child not in REACHED is a leaf: its own descendants are not inlined."
+(ert-deftest flamegraph-test-call-tree-unshown-omitted ()
+  "A child absent from SHOWN is omitted, along with its subtree."
   (let* ((grand (profiler-make-calltree :entry 'grand :count 50))
          (kid   (profiler-make-calltree :entry 'kid :count 50
                                         :children (list grand)))
          (root (profiler-make-calltree :entry 'root :count 100
                                        :children (list kid))))
-    ;; kid is not passed as reached.
-    (let* ((out (flamegraph-test--render-call-tree root 100))
+    ;; Only kid is shown, not grand.
+    (let* ((out (flamegraph-test--render-call-tree root 100 (list kid)))
            (lines (split-string out "\n" t)))
       (should (flamegraph-test--line-with "kid" lines))
       (should-not (flamegraph-test--line-with "grand" lines)))))
@@ -392,7 +432,7 @@ not their immediate parent — so all percents add up under 
that frame."
                                           :children (list fn)))
          (root (profiler-make-calltree :entry 'root :count 1000
                                        :children (list if-node))))
-    (let* ((out (flamegraph-test--render-call-tree root 1000 (list if-node)))
+    (let* ((out (flamegraph-test--render-call-tree root 1000 (list if-node 
fn)))
            (fn-line (flamegraph-test--line-with
                      "fn" (split-string out "\n" t))))
       (should fn-line)
@@ -403,9 +443,11 @@ not their immediate parent — so all percents add up under 
that frame."
   "Every rendered row is a clickable describe-button."
   (let* ((bar (profiler-make-calltree :entry 'bar :count 1))
          (foo (profiler-make-calltree :entry 'foo :count 1
-                                      :children (list bar))))
+                                      :children (list bar)))
+         (shown (make-hash-table :test 'eq)))
+    (puthash bar t shown)
     (with-temp-buffer
-      (flamegraph--insert-call-tree foo 1 1 0 nil (make-hash-table :test 'eq))
+      (flamegraph--insert-call-tree foo 1 1 0 nil shown)
       (goto-char (point-min))
       (let ((b (next-button (point))))
         (should b)

Reply via email to