branch: elpa/flamegraph
commit e8c5ccabf3b6ba403c49d25c422c3620bb7ac4c3
Author: Dmitry Gutov <[email protected]>
Commit: Dmitry Gutov <[email protected]>
Nest the Calls tree by source reachability
Drive the describe-buffer "Calls" tree from the same source-attributed
walk as the snippet: a callee is expanded inline only when one of its own
calls is found in the defun's source (through transparent skip-through
frames), so the tree stops where attribution runs out instead of
unfolding a function's entire internal subtree.
flamegraph--collect-nested-call-sites now fills a REACHED table the tree
consults; the threshold gates snippet highlights only, never the tree.
---
flamegraph.el | 104 ++++++++++++++++++++++++++++-------------------
test/flamegraph-tests.el | 57 +++++++++++++++++++-------
2 files changed, 105 insertions(+), 56 deletions(-)
diff --git a/flamegraph.el b/flamegraph.el
index 95d3186add..cf66934195 100644
--- a/flamegraph.el
+++ b/flamegraph.el
@@ -674,7 +674,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)
+(defun flamegraph--collect-nested-call-sites (node beg end &optional reached)
"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
@@ -689,49 +689,66 @@ 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.
-Callees below `flamegraph-call-site-threshold' of NODE's count are
-dropped along with their subtree. Each region is (POS-BEG POS-END
-WEIGHT), WEIGHT being the callee's share of NODE's count, for heat
-styling."
+Each region is (POS-BEG POS-END WEIGHT), WEIGHT being the callee's share
+of NODE's count, for heat styling. Callees below
+`flamegraph-call-site-threshold' are omitted from the regions, but still
+traversed.
+
+If REACHED (a hash table) is non-nil, mark in it every node whose callees
+should be shown nested in the \"Calls\" tree: a node is reached when it
+has a child found in the source (directly, or through transparent
+skip-through frames). This is independent of the threshold, so the Calls
+tree may show cold callees the snippet omits — but it stops, like the
+snippet, where source attribution runs out (a function whose own body is
+not in this defun)."
(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)
- (dolist (k (profiler-calltree-children n))
- (let* ((entry (profiler-calltree-entry k))
- (name (flamegraph--frame-display-name entry))
- (skip (flamegraph--skip-through-p entry))
- (weight (/ (float (profiler-calltree-count k)) total)))
- (cond
- (skip
- ;; Special form — transparent descend, no narrowing.
- (walk k region))
- ((< weight flamegraph-call-site-threshold)
- ;; Too cold to be worth showing — drop it and its subtree.
- nil)
- ((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 (list mb me weight) regions)
- (when-let* ((sub (flamegraph--enclosing-form-region
- mb)))
- (walk k sub))))))))))))
+ (let ((reachable 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)
+ ;; Special form — transparent descend, no narrowing.
+ (when (walk k region)
+ (when reached (puthash k t reached))
+ (setq reachable t)))
+ ((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) "\\_>"))
+ (found nil))
+ (while (re-search-forward re (cdr region) t)
+ (setq found t)
+ (let ((mb (match-beginning 0))
+ (me (match-end 0)))
+ (when (>= weight flamegraph-call-site-threshold)
+ (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)))
(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."
+(defun flamegraph--insert-call-tree (node total ref depth directory reached)
+ "Insert NODE's children as a tree, nesting those found in the source.
+REACHED is the hash table populated by `flamegraph--collect-nested-call-sites':
+a child is expanded inline (its own callees shown indented) only when it
+is a key there, so the tree follows the same source-attributed nesting as
+the snippet. 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))))))
@@ -743,8 +760,9 @@ controls the indentation."
(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)))))
+ (when (gethash k reached)
+ (flamegraph--insert-call-tree k total ref (1+ depth) directory
+ reached)))))
(defun flamegraph--describe-button (node total directory)
"Insert a button that describes NODE, passing TOTAL and DIRECTORY along."
@@ -774,7 +792,8 @@ frames, with Help-style back/forward navigation."
(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)))
+ (parent (profiler-calltree-parent node))
+ (reached (make-hash-table :test 'eq)))
(with-help-window (help-buffer)
(with-current-buffer standard-output
(if (and (symbolp entry) (fboundp entry))
@@ -797,7 +816,7 @@ frames, with Help-style back/forward navigation."
path (cdr loc)
(lambda (b e)
(flamegraph--collect-nested-call-sites
- node b e)))))
+ node b e reached)))))
(insert "\n\n" snippet)))
(insert (format "\n\nSamples %s (%s of total) self %s (%s)\n"
(profiler-format-number count)
@@ -810,7 +829,8 @@ frames, with Help-style back/forward navigation."
(insert "\n"))
(when kids
(insert "\nCalls\n")
- (flamegraph--insert-call-tree node total count 0 directory))))))
+ (flamegraph--insert-call-tree node total count 0 directory
+ reached))))))
(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 ae3800d6f8..600f0230ef 100644
--- a/test/flamegraph-tests.el
+++ b/test/flamegraph-tests.el
@@ -193,6 +193,30 @@ 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."
+ (flamegraph-test--with-elisp-source
+ "(defun outer ()
+ (wrap (inner))
+ (leaf))"
+ (let* ((inner (profiler-make-calltree :entry 'inner :count 10))
+ (wrap (profiler-make-calltree :entry 'wrap :count 10
+ :children (list inner)))
+ (hidden (profiler-make-calltree :entry 'not-in-source :count 5))
+ (leaf (profiler-make-calltree :entry 'leaf :count 5
+ :children (list hidden)))
+ (outer (profiler-make-calltree :entry 'outer :count 20
+ :children (list wrap leaf)))
+ (reached (make-hash-table :test 'eq))
+ (regions (flamegraph--collect-nested-call-sites
+ outer (point-min) (point-max) reached)))
+ (should (equal (flamegraph-test--region-texts regions)
+ '("wrap" "inner" "leaf")))
+ (should (gethash wrap reached))
+ (should-not (gethash leaf reached)))))
+
(ert-deftest flamegraph-test-walker-below-threshold-dropped ()
"Callees below `flamegraph-call-site-threshold' are dropped with subtree.
hot at 50% is kept; cold at 1% (and its child) are not."
@@ -270,12 +294,15 @@ 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)
+(defun flamegraph-test--render-call-tree (node ref &optional reached-nodes)
"Render NODE's call tree (REF samples for percent scaling) and return
-the resulting string."
- (with-temp-buffer
- (flamegraph--insert-call-tree node ref ref 0 nil)
- (buffer-string)))
+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))
+ (with-temp-buffer
+ (flamegraph--insert-call-tree node ref ref 0 nil reached)
+ (buffer-string))))
(defun flamegraph-test--line-with (str lines)
"Return the first LINES element containing STR as a whole symbol."
@@ -300,15 +327,16 @@ the resulting string."
(should bar-pos)
(should (< baz-pos bar-pos)))))
-(ert-deftest flamegraph-test-call-tree-skip-through-expanded-indented ()
- "Skip-through children render their sub-children indented underneath."
+(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."
(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))
+ (let* ((out (flamegraph-test--render-call-tree root 100 (list if-node)))
(lines (split-string out "\n" t))
(if-line (flamegraph-test--line-with "if" lines))
(rf-line (flamegraph-test--line-with "real-fn" lines))
@@ -316,20 +344,21 @@ the resulting string."
(should if-line)
(should rf-line)
(should other-line)
- ;; real-fn is indented deeper than its skip-through parent.
+ ;; real-fn is indented deeper than its reached parent.
(should (> (flamegraph-test--indent rf-line)
(flamegraph-test--indent if-line)))
- ;; The non-skip sibling sits at the same depth as `if'.
+ ;; The unreached 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-non-skip-not-expanded ()
- "A non-skip child's own descendants are NOT inlined into the tree."
+(ert-deftest flamegraph-test-call-tree-unreached-not-expanded ()
+ "A child not in REACHED is a leaf: its own descendants are not inlined."
(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))
(lines (split-string out "\n" t)))
(should (flamegraph-test--line-with "kid" lines))
@@ -344,7 +373,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))
+ (let* ((out (flamegraph-test--render-call-tree root 1000 (list if-node)))
(fn-line (flamegraph-test--line-with
"fn" (split-string out "\n" t))))
(should fn-line)
@@ -357,7 +386,7 @@ not their immediate parent — so all percents add up under
that frame."
(foo (profiler-make-calltree :entry 'foo :count 1
:children (list bar))))
(with-temp-buffer
- (flamegraph--insert-call-tree foo 1 1 0 nil)
+ (flamegraph--insert-call-tree foo 1 1 0 nil (make-hash-table :test 'eq))
(goto-char (point-min))
(let ((b (next-button (point))))
(should b)