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

    Limit Calls tree shaping to profiler.el
---
 README.md                | 17 +++++++-----
 flamegraph.el            | 72 +++++++++++++++++++++++++++++-------------------
 test/flamegraph-tests.el | 59 ++++++++++++++++++++++++++++++++++++---
 3 files changed, 108 insertions(+), 40 deletions(-)

diff --git a/README.md b/README.md
index 1853c9d0c3..24cd09d708 100644
--- a/README.md
+++ b/README.md
@@ -13,11 +13,10 @@ frame sits on the top row, and the stack grows downward. 
Identical
 call paths are merged and children are sorted heaviest-first.
 
 `RET` / `mouse-1` zooms into a frame; `d` opens a description (parent,
-callees, self-time) with a source code snippet highlighting its
-outgoing calls; `f` opens that source. The snippet and `f` need the
-data to include `file:line` — Elisp profiles always do; for perf,
-fold with `-F +srcline` (see [Recording](#recording-profile-data)
-below).
+callees, self-time) with a source code snippet; `f` opens that source.
+The snippet and `f` need a source location: Elisp profiler frames can be
+resolved through their definitions; for perf, fold with `-F +srcline`
+(see [Recording](#recording-profile-data) below).
 
 <p style="white-space:nowrap">
   <img src="docs/img/flamegraph-light.png" width="400">
@@ -28,11 +27,15 @@ below).
   <img src="docs/img/describe-dark.png"   width="400">
 </p>
 
-In the snippet, we pull in and highlight the source lines where the
-frame's outgoing calls appear. Parent and callees are cross-references
+In the snippet, we try to pull in and highlight the source lines where
+the frame's outgoing calls appear. Parent and callees are cross-references
 (click to describe them); `l` / `r` walk the navigation history,
 help-mode style.
 
+For Emacs profiler data, those source matches may also add structure to
+the Calls tree. Folded-stacks profiles use them only for snippet
+highlighting, and keep the Calls list flat.
+
 ## Installation
 
 Until this lands on a package archive, clone the repo and add it to
diff --git a/flamegraph.el b/flamegraph.el
index 753270ebf4..3fd3b243a4 100644
--- a/flamegraph.el
+++ b/flamegraph.el
@@ -331,6 +331,8 @@ outermost frames.")
   "Short label for the data source, shown in the header.")
 (defvar-local flamegraph--directory nil
   "Directory of the data file, for resolving relative source paths.")
+(defvar-local flamegraph--profiler-el-calls nil
+  "Non-nil when calltree entries come from profiler.el.")
 
 (defun flamegraph--header ()
   "Return the header line for the current view."
@@ -477,7 +479,8 @@ sample counts, and buttons to the caller and callees."
         (message "No frame at point")
       (flamegraph--describe-frame
        (flamegraph-frame-node frame)
-       flamegraph--grand-total flamegraph--directory))))
+       flamegraph--grand-total flamegraph--directory
+       flamegraph--profiler-el-calls))))
 
 (defun flamegraph--entry-location (entry)
   "Extract a (FILE . LINE) source location embedded in ENTRY, or nil.
@@ -593,9 +596,9 @@ the enclosing structural lines up to the definition are 
kept — each line
 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 SHOWN).  Skipped runs are elided with `⋯'.  CONTEXT
-defaults to 4."
+and their lines kept too (see `flamegraph--collect-nested-call-sites').
+When SHOWN is non-nil, it is filled for Calls tree rendering.  Skipped
+runs are elided with `⋯'.  CONTEXT defaults to 4."
   (setq context (or context 4))
   (when (and (file-readable-p path) (> line 0))
     (with-temp-buffer
@@ -804,13 +807,15 @@ callees the snippet omits."
       (walk node (cons beg end) t))
     (nreverse regions)))
 
-(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."
+(defun flamegraph--insert-call-tree (node total ref depth directory shown
+                                      profiler-el-calls)
+  "Insert NODE's children whose nodes are present in SHOWN.
+A child absent from SHOWN is skipped, along with its subtree.  When SHOWN
+contains descendants, render them recursively under their shown parents.
+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.  PROFILER-EL-CALLS is passed
+through to describe buttons."
   (let ((kids (sort (copy-sequence (profiler-calltree-children node))
                     (lambda (a b) (> (profiler-calltree-count a)
                                      (profiler-calltree-count b))))))
@@ -823,31 +828,37 @@ add up under that frame.  DEPTH controls the indentation."
         ;; (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))
+         (and profiler-el-calls
+              (< (/ (float (profiler-calltree-count k)) ref)
+                 flamegraph-call-site-threshold))
+         profiler-el-calls)
         (insert (format "  (%s)\n"
                         (flamegraph--percent
                          (profiler-calltree-count k) ref)))
         (flamegraph--insert-call-tree k total ref (1+ depth) directory
-                                      shown)))))
+                                      shown profiler-el-calls)))))
 
-(defun flamegraph--describe-button (node total directory &optional dim)
-  "Insert a button that describes NODE, passing TOTAL and DIRECTORY along.
+(defun flamegraph--describe-button (node total directory &optional dim
+                                      profiler-el-calls)
+  "Insert a button that describes NODE, passing context along.
 With DIM non-nil, shadow the name (but keep it clickable)."
   (apply #'insert-text-button (flamegraph--frame-display-name
                                (profiler-calltree-entry node))
          'type 'help-xref
          'help-function #'flamegraph--describe-frame
-         'help-args (list node total directory)
+         'help-args (list node total directory profiler-el-calls)
          (when dim '(face (shadow button)))))
 
-(defun flamegraph--describe-frame (node total directory)
+(defun flamegraph--describe-frame (node total directory &optional 
profiler-el-calls)
   "Show a report for calltree NODE in the *Help* buffer.
 TOTAL is the grand total for percentages; DIRECTORY resolves relative
-source paths.  Caller and callee names are buttons describing those
+source paths.  PROFILER-EL-CALLS means entries come from profiler.el;
+source matches may add structure to the Calls tree.  Otherwise they only
+affect the snippet.  Caller and callee names are buttons describing those
 frames, with Help-style back/forward navigation."
   (require 'help-mode)
-  (help-setup-xref (list #'flamegraph--describe-frame node total directory)
+  (help-setup-xref (list #'flamegraph--describe-frame node total directory
+                         profiler-el-calls)
                    (called-interactively-p 'interactive))
   (let* ((entry (profiler-calltree-entry node))
          (loc (cond ((stringp entry) (flamegraph--entry-location entry))
@@ -882,7 +893,8 @@ 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 shown)))
+                                path (cdr loc) node
+                                (and profiler-el-calls shown))))
             (insert "\n\n" snippet)))
         (insert (format "\n\nSamples  %s (%s of total)    self  %s (%s)\n"
                         (profiler-format-number count)
@@ -891,17 +903,17 @@ frames, with Help-style back/forward navigation."
                         (flamegraph--percent self total)))
         (when (and parent (profiler-calltree-entry parent))
           (insert "\nCalled from  ")
-          (flamegraph--describe-button parent total directory)
+          (flamegraph--describe-button parent total directory nil
+                                      profiler-el-calls)
           (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.
+          ;; With no profiler.el source matches for the Calls tree, 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
-                                        shown))))))
+                                        shown profiler-el-calls))))))
 
 (defun flamegraph-redraw ()
   "Redraw the flame graph, e.g. to pick up a new window width."
@@ -966,12 +978,12 @@ since this runs before that relocation happens."
 
 ;;; Entry points
 
-(defun flamegraph--show (top unit title &optional directory)
+(defun flamegraph--show (top unit title &optional directory profiler-el-calls)
   "Display a flame graph for calltree TOP and return its buffer.
 TOP is a dummy root node whose children are the outermost frames.  UNIT
 names the count unit (e.g. \"samples\"); TITLE labels the data source in
 the header.  DIRECTORY, if given, resolves relative source paths when
-visiting a frame."
+visiting a frame.  PROFILER-EL-CALLS means entries come from profiler.el."
   (let ((buffer (get-buffer-create (format "*Flamegraph: %s*" title))))
     (with-current-buffer buffer
       (flamegraph-mode)
@@ -983,6 +995,7 @@ visiting a frame."
             flamegraph--unit unit
             flamegraph--title title
             flamegraph--directory directory
+            flamegraph--profiler-el-calls profiler-el-calls
             flamegraph--grand-total
             (max 1 (apply #'+ (mapcar #'profiler-calltree-count
                                       (profiler-calltree-children top))))))
@@ -997,7 +1010,8 @@ visiting a frame."
   (flamegraph--show
    (profiler-calltree-build (profiler-profile-log profile))
    (if (eq (profiler-profile-type profile) 'memory) "bytes" "samples")
-   (if (eq (profiler-profile-type profile) 'cpu) "CPU" "Memory")))
+   (if (eq (profiler-profile-type profile) 'cpu) "CPU" "Memory")
+   nil t))
 
 ;;;###autoload
 (defun flamegraph-profiler-report ()
diff --git a/test/flamegraph-tests.el b/test/flamegraph-tests.el
index 29b8917082..c187a81bba 100644
--- a/test/flamegraph-tests.el
+++ b/test/flamegraph-tests.el
@@ -479,6 +479,13 @@ is still shown."
            for f = (get-text-property i 'face string)
            thereis (or (eq f face) (and (listp f) (memq face f)))))
 
+(defun flamegraph-test--text-has-face (string text face)
+  "Non-nil if TEXT in STRING has FACE at its first character."
+  (let ((pos (string-match-p (regexp-quote text) string)))
+    (and pos
+         (let ((f (get-text-property pos 'face string)))
+           (or (eq f face) (and (listp f) (memq face f)))))))
+
 (ert-deftest flamegraph-test-source-snippet-applies-highlight-face ()
   "Callee matches in the snippet carry a heat face scaled by share.
 foo at 100% of outer is hot; bar at 10% is warm."
@@ -494,6 +501,28 @@ foo at 100% of outer is hot; bar at 10% is warm."
       (should (flamegraph-test--has-face s 'flamegraph-call-site-hot))
       (should (flamegraph-test--has-face s 'flamegraph-call-site-warm)))))
 
+(ert-deftest flamegraph-test-source-snippet-threshold-only-affects-regions ()
+  "Below-threshold callees fill SHOWN but are not highlighted."
+  (flamegraph-test--with-temp-source path
+      "(defun outer ()
+  (foo)
+  (cold))
+"
+    (let* ((foo (profiler-make-calltree :entry 'foo :count 99))
+           (cold (profiler-make-calltree :entry 'cold :count 1))
+           (outer (profiler-make-calltree :entry 'outer :count 100
+                                          :children (list foo cold)))
+           (flamegraph-call-site-threshold 0.02)
+           (shown (make-hash-table :test 'eq))
+           (s (flamegraph--source-snippet path 1 outer shown)))
+      (should s)
+      (should (gethash foo shown))
+      (should (gethash cold shown))
+      (should (flamegraph-test--text-has-face
+               s "foo" 'flamegraph-call-site-hot))
+      (should-not (flamegraph-test--text-has-face
+                   s "cold" 'flamegraph-call-site-cool)))))
+
 (ert-deftest flamegraph-test-source-snippet-lines-ret-visits-source ()
   "RET on a snippet source line, including the final EOL, visits that line."
   (flamegraph-test--with-temp-source path
@@ -532,7 +561,7 @@ nil, every descendant of NODE is shown."
                       (mark k))))
         (mark node)))
     (with-temp-buffer
-      (flamegraph--insert-call-tree node ref ref 0 nil shown)
+      (flamegraph--insert-call-tree node ref ref 0 nil shown t)
       (buffer-string))))
 
 (defun flamegraph-test--line-with (str lines)
@@ -641,7 +670,7 @@ not their immediate parent — so all percents add up under 
that frame."
          (shown (make-hash-table :test 'eq)))
     (puthash bar t shown)
     (with-temp-buffer
-      (flamegraph--insert-call-tree foo 1 1 0 nil shown)
+      (flamegraph--insert-call-tree foo 1 1 0 nil shown t)
       (goto-char (point-min))
       (let ((b (next-button (point))))
         (should b)
@@ -651,11 +680,12 @@ not their immediate parent — so all percents add up under 
that frame."
 
 ;;; Heading rendering in `flamegraph--describe-frame'
 
-(defun flamegraph-test--describe-help (node)
+(defun flamegraph-test--describe-help (node &optional profiler-el-calls)
   "Render NODE in the help buffer via `flamegraph--describe-frame' and
 return that buffer's contents (a propertized string)."
   (require 'help-mode)
-  (flamegraph--describe-frame node (profiler-calltree-count node) nil)
+  (flamegraph--describe-frame node (profiler-calltree-count node) nil
+                              profiler-el-calls)
   (with-current-buffer (help-buffer) (buffer-string)))
 
 (ert-deftest flamegraph-test-describe-symbol-heading-is-help-button ()
@@ -683,6 +713,27 @@ return that buffer's contents (a propertized string)."
                        (eq (button-type (button-at (point)))
                            'help-function))))))
 
+(ert-deftest flamegraph-test-describe-folded-stack-calls-stay-flat ()
+  "String frames get source snippets but not profiler.el Calls pruning."
+  (flamegraph-test--with-temp-source path
+      "(defun outer ()
+  (foo (bar)))
+"
+    (let* ((bar (profiler-make-calltree :entry "bar" :count 10))
+           (foo (profiler-make-calltree :entry "foo" :count 10
+                                        :children (list bar)))
+           (outer (profiler-make-calltree
+                   :entry (format "outer (%s:1)" path)
+                   :count 10 :children (list foo)))
+           (out (flamegraph-test--describe-help outer)))
+      (should (string-match-p "foo (bar)" out))
+      (should (flamegraph-test--has-face out 'flamegraph-call-site-hot))
+      (should (string-match "\nCalls\n" out))
+      (let ((calls (substring out (match-end 0))))
+        (should (flamegraph-test--line-with "foo" (split-string calls "\n" t)))
+        (should-not (flamegraph-test--line-with "bar"
+                                                (split-string calls "\n" 
t)))))))
+
 ;;; `flamegraph-find-source' branches
 
 (defun flamegraph-test--frame (entry)

Reply via email to