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.

Reply via email to