branch: elpa/flamegraph
commit 8f99b4db3715daffb39222a748cc9c041e7c34fc
Author: Dmitry Gutov <[email protected]>
Commit: Dmitry Gutov <[email protected]>
Ensure anonymous callback frames in describe call tree
---
flamegraph.el | 18 ++++++++---
test/flamegraph-tests.el | 83 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 97 insertions(+), 4 deletions(-)
diff --git a/flamegraph.el b/flamegraph.el
index 94508c8d47..a098c289e6 100644
--- a/flamegraph.el
+++ b/flamegraph.el
@@ -679,6 +679,10 @@ symbol boundaries in the target buffer's syntax."
(and (stringp name) (not (string-empty-p name))
(not (string-match-p "[][[:space:]();,]" name))))
+(defun flamegraph--anonymous-function-p (entry)
+ "Non-nil if ENTRY is a function object with no source-searchable name."
+ (and (not (symbolp entry)) (functionp entry)))
+
(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
@@ -701,8 +705,11 @@ 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."
+callee's own body and is omitted. An anonymous function object is searched
+as `lambda', using the same source anchoring flow as named functions; if no
+lambda form is found, the object itself is still shown as an unanchored
+callback frame. 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
@@ -714,7 +721,10 @@ tree may show cold callees the snippet omits."
(let ((any nil))
(dolist (k (profiler-calltree-children n))
(let* ((entry (profiler-calltree-entry k))
- (name (flamegraph--frame-display-name entry))
+ (anonymous (flamegraph--anonymous-function-p entry))
+ (name (if anonymous
+ "lambda"
+ (flamegraph--frame-display-name entry)))
(weight (/ (float (profiler-calltree-count k)) total)))
(cond
((flamegraph--skip-through-p entry)
@@ -754,7 +764,7 @@ tree may show cold callees the snippet omits."
;; re-matching its own name.
(walk k (cons me (min (cdr sub) (cdr region)))
nil)))))
- (when (or found concealed-ok)
+ (when (or found concealed-ok anonymous)
(when shown (puthash k t shown))
(setq any t))))))))
any)))
diff --git a/test/flamegraph-tests.el b/test/flamegraph-tests.el
index 4410fee4e7..4601c9db3b 100644
--- a/test/flamegraph-tests.el
+++ b/test/flamegraph-tests.el
@@ -234,6 +234,89 @@ only the real call in code is matched, not the earlier
textual mentions."
outer (point-min) (point-max))))
(should (equal (flamegraph-test--region-texts regions) '("foo"))))))
+(ert-deftest flamegraph-test-walker-shows-external-anonymous-callback-frame ()
+ "An external callback invoked via `funcall' is shown as a frame, but
+its body is not part of the current definition."
+ (flamegraph-test--with-elisp-source
+ "(defun outer (cb)
+ (funcall cb))"
+ (let* ((deep (profiler-make-calltree :entry 'deep-call :count 10))
+ (clos (profiler-make-calltree
+ :entry (make-byte-code 257 "\300\207" [] 2)
+ :count 10 :children (list deep)))
+ (fc (profiler-make-calltree :entry 'funcall :count 10
+ :children (list clos)))
+ (outer (profiler-make-calltree :entry 'outer :count 10
+ :children (list fc)))
+ (shown (make-hash-table :test 'eq)))
+ (flamegraph--collect-nested-call-sites
+ outer (point-min) (point-max) shown)
+ (should (gethash fc shown))
+ (should (gethash clos shown))
+ (should-not (gethash deep shown)))))
+
+(ert-deftest flamegraph-test-walker-shows-source-lambda-callback ()
+ "An anonymous callback is shown when it corresponds to a lambda in the
+current definition, but the lambda's callees still use normal anchoring."
+ (flamegraph-test--with-elisp-source
+ "(defun outer ()
+ (funcall (lambda ()
+ (deep-call))))"
+ (let* ((hidden (profiler-make-calltree :entry 'hidden-body :count 5))
+ (deep (profiler-make-calltree :entry 'deep-call :count 10
+ :children (list hidden)))
+ (clos (profiler-make-calltree
+ :entry (make-byte-code 257 "\300\207" [] 2)
+ :count 10 :children (list deep)))
+ (fc (profiler-make-calltree :entry 'funcall :count 10
+ :children (list clos)))
+ (outer (profiler-make-calltree :entry 'outer :count 10
+ :children (list fc)))
+ (shown (make-hash-table :test 'eq))
+ (regions (flamegraph--collect-nested-call-sites
+ outer (point-min) (point-max) shown)))
+ (should (equal (flamegraph-test--region-texts regions)
+ '("funcall" "lambda" "deep-call")))
+ (should (gethash fc shown))
+ (should (gethash clos shown))
+ (should (gethash deep shown))
+ (should-not (gethash hidden shown)))))
+
+(ert-deftest
flamegraph-test-walker-multiple-source-lambdas-use-normal-matching ()
+ "Anonymous callbacks use the same all-occurrences matching as named calls."
+ (flamegraph-test--with-elisp-source
+ "(defun outer ()
+ (register (lambda ()
+ (first-call))
+ (lambda ()
+ (second-call))))"
+ (let* ((first-call (profiler-make-calltree :entry 'first-call :count 6))
+ (second-call (profiler-make-calltree :entry 'second-call :count 4))
+ (first-clos (profiler-make-calltree
+ :entry (make-byte-code 257 "\300\207" [] 2)
+ :count 6 :children (list first-call)))
+ (second-clos (profiler-make-calltree
+ :entry (make-byte-code 257 "\300\207" [] 2)
+ :count 4 :children (list second-call)))
+ (register (profiler-make-calltree :entry 'register :count 10
+ :children (list first-clos
+ second-clos)))
+ (outer (profiler-make-calltree :entry 'outer :count 10
+ :children (list register)))
+ (shown (make-hash-table :test 'eq))
+ (regions (flamegraph--collect-nested-call-sites
+ outer (point-min) (point-max) shown)))
+ ;; As with named functions, the profile does not identify which
+ ;; textual occurrence corresponds to this frame, so each anonymous
+ ;; frame searches all lambda occurrences in the current region.
+ (should (equal (flamegraph-test--region-texts regions)
+ '("register" "lambda" "first-call" "lambda"
+ "lambda" "lambda" "second-call")))
+ (should (gethash first-clos shown))
+ (should (gethash first-call shown))
+ (should (gethash second-clos shown))
+ (should (gethash second-call shown)))))
+
(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.