branch: elpa/flamegraph
commit e578081d4d58fac6c1d1c7c584fa88b9bfb9cc88
Author: Dmitry Gutov <[email protected]>
Commit: Dmitry Gutov <[email protected]>
flamegraph-find-file: New command
---
flamegraph.el | 126 ++++++++++++++++++++++++++++++++++++++--------------------
1 file changed, 84 insertions(+), 42 deletions(-)
diff --git a/flamegraph.el b/flamegraph.el
index 1132a3dee5..e76204663b 100644
--- a/flamegraph.el
+++ b/flamegraph.el
@@ -14,13 +14,15 @@
;;; Commentary:
-;; Visualize the data recorded by Emacs's native profiler (see
-;; `profiler.el') as a flame graph.
+;; View profiling data as a flame graph. Data can come from Emacs's native
+;; profiler (see `profiler.el') or from a folded stacks file (the common
+;; flame-graph interchange format).
;;
;; Start the profiler with `profiler-start', run the code you want to
;; measure, then `M-x flamegraph-profiler-report' to view the result.
;; `M-x flamegraph-find-profiler-report' opens a profile previously saved with
-;; `profiler-report-write-profile'.
+;; `profiler-report-write-profile'. `M-x flamegraph-find-file' opens a
+;; folded-stacks file.
;;
;; The graph is drawn top-down ("icicle" orientation): the outermost frame
;; (bottom of the call stack) is the top row, and the stack grows downward.
@@ -37,8 +39,8 @@
;; q quit
;;
;; The pipeline has three stages:
-;; 1. `profiler-calltree-build' (from profiler.el) merges the recorded
-;; backtraces into a call tree.
+;; 1. A data source produces a `profiler-calltree': the profiler via
+;; `profiler-calltree-build', a folded-stacks file by direct parsing.
;; 2. `flamegraph--frames' lays that tree out into a flat list of
;; `flamegraph-frame' records, each carrying a fractional
;; START and WIDTH in [0,1] plus a DEPTH.
@@ -218,29 +220,26 @@ MAX-DEPTH is the deepest row; GRAND-TOTAL the
whole-profile count."
;;; Buffer state and drawing
-(defvar-local flamegraph--profile nil
- "The `profiler-profile' being displayed.")
(defvar-local flamegraph--top nil
- "The full call tree (dummy root node from `profiler-calltree-build').")
+ "The full call tree: a dummy root node whose children are the
+outermost frames.")
(defvar-local flamegraph--root nil
"Currently zoomed-in calltree node, or nil for the full view.")
(defvar-local flamegraph--grand-total 0
- "Total sample/byte count of the whole profile.")
-
-(defun flamegraph--unit ()
- "Return the count unit of the current profile, as a string."
- (if (eq (profiler-profile-type flamegraph--profile) 'memory)
- "bytes" "samples"))
+ "Total count of the whole graph.")
+(defvar-local flamegraph--unit "samples"
+ "Count unit, as a string (e.g. \"samples\" or \"bytes\").")
+(defvar-local flamegraph--title ""
+ "Short label for the data source, shown in the header.")
(defun flamegraph--header ()
"Return the header line for the current view."
- (let* ((type (profiler-profile-type flamegraph--profile))
- (root flamegraph--root)
+ (let* ((root flamegraph--root)
(zoomed (and root (profiler-calltree-entry root))))
- (format " %s flame graph · %s %s · root: %s%s"
- (if (eq type 'cpu) "CPU" "Memory")
+ (format " %s · %s %s · root: %s%s"
+ flamegraph--title
(profiler-format-number flamegraph--grand-total)
- (flamegraph--unit)
+ flamegraph--unit
(if zoomed
(flamegraph--entry-name (profiler-calltree-entry root))
"all")
@@ -345,7 +344,7 @@ MAX-DEPTH is the deepest row; GRAND-TOTAL the whole-profile
count."
(message "%s %s %s (%s of total)"
(flamegraph--entry-name (profiler-calltree-entry node))
(profiler-format-number count)
- (flamegraph--unit)
+ flamegraph--unit
(profiler-format-percent count flamegraph--grand-total))))))
;;; Mode
@@ -364,7 +363,7 @@ MAX-DEPTH is the deepest row; GRAND-TOTAL the whole-profile
count."
"q" #'quit-window)
(define-derived-mode flamegraph-mode special-mode "Flamegraph"
- "Major mode for viewing a flame graph of profiler data."
+ "Major mode for viewing a flame graph."
(setq buffer-read-only t
truncate-lines t
buffer-undo-list t)
@@ -372,33 +371,33 @@ MAX-DEPTH is the deepest row; GRAND-TOTAL the
whole-profile count."
;;; Entry points
-(defun flamegraph--setup-buffer (profile)
- "Create and return a flame-graph buffer for PROFILE.
-The buffer's state is initialized but it is not drawn; draw it with
-`flamegraph--draw' once it is shown in a window, so the canvas
-matches the window width."
- (let ((buffer (get-buffer-create
- (format "*Flamegraph: %s*"
- (if (eq (profiler-profile-type profile) 'cpu)
- "CPU" "Memory")))))
+(defun flamegraph--show (top unit title)
+ "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."
+ (let ((buffer (get-buffer-create (format "*Flamegraph: %s*" title))))
(with-current-buffer buffer
(flamegraph-mode)
- (let ((top (profiler-calltree-build (profiler-profile-log profile))))
- (profiler-calltree-sort top #'profiler-calltree-count>)
- (setq flamegraph--profile profile
- flamegraph--top top
- flamegraph--root nil
- flamegraph--grand-total
- (max 1 (apply #'+ (mapcar #'profiler-calltree-count
- (profiler-calltree-children top)))))))
+ (profiler-calltree-sort top #'profiler-calltree-count>)
+ (setq flamegraph--top top
+ flamegraph--root nil
+ flamegraph--unit unit
+ flamegraph--title title
+ flamegraph--grand-total
+ (max 1 (apply #'+ (mapcar #'profiler-calltree-count
+ (profiler-calltree-children top))))))
+ ;; Display first, then draw, so the canvas spans the actual window.
+ (pop-to-buffer buffer)
+ (flamegraph--draw)
buffer))
(defun flamegraph--profile (profile)
"Display a flame graph for PROFILE, a `profiler-profile' object."
- (let ((buffer (flamegraph--setup-buffer profile)))
- ;; Display first, then draw, so the canvas spans the actual window.
- (pop-to-buffer buffer)
- (flamegraph--draw)))
+ (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")))
;;;###autoload
(defun flamegraph-profiler-report ()
@@ -421,5 +420,48 @@ Uses the CPU profile if one was recorded, otherwise the
memory profile."
(interactive (list (read-file-name "Find profile: " default-directory)))
(flamegraph--profile (profiler-read-profile filename)))
+;;; Folded stacks
+
+(defun flamegraph--find-child (node name)
+ "Return NODE's child whose entry is the string NAME, or nil."
+ (cl-find name (profiler-calltree-children node)
+ :key #'profiler-calltree-entry :test #'equal))
+
+(defun flamegraph--read-folded (filename)
+ "Parse folded stacks in FILENAME into a calltree root node.
+Each non-empty line has the form \"FRAME;FRAME;... COUNT\", outermost
+frame first, as produced by Brendan Gregg's stackcollapse-* tools.
+COUNT is added to every frame along the stack."
+ (let ((root (profiler-make-calltree)))
+ (with-temp-buffer
+ (insert-file-contents filename)
+ (goto-char (point-min))
+ (while (not (eobp))
+ (let ((line (buffer-substring-no-properties
+ (line-beginning-position) (line-end-position))))
+ (when (string-match "\\`\\(.+\\) \\([0-9]+\\)\\'" line)
+ (let ((node root)
+ (count (string-to-number (match-string 2 line))))
+ (dolist (frame (split-string (match-string 1 line) ";" t))
+ (let ((child (flamegraph--find-child node frame)))
+ (unless child
+ (setq child (profiler-make-calltree :entry frame :parent
node))
+ (push child (profiler-calltree-children node)))
+ (cl-incf (profiler-calltree-count child) count)
+ (setq node child))))))
+ (forward-line)))
+ root))
+
+;;;###autoload
+(defun flamegraph-find-file (filename)
+ "Display a flame graph for the folded stacks in FILENAME.
+This is the interchange format produced by Brendan Gregg's
+stackcollapse-*.pl tools and read by flamegraph.pl: one line per stack,
+frames separated by \";\" (outermost first), then a space and a count."
+ (interactive (list (read-file-name "Folded stacks file: " nil nil t)))
+ (flamegraph--show (flamegraph--read-folded filename)
+ "samples"
+ (file-name-nondirectory filename)))
+
(provide 'flamegraph)
;;; flamegraph.el ends here