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)