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)