GHCのプロファイル出力を読むツールを書いた
(追記(2011-07-30): '記号を含むprofファイルの解析に失敗するバグを修正したものをGitHub - mkotha/viewprof: A viewer of GHC .prof files with curses interfaceに上げた)
+RTS -pで出力される.profファイルには呼び出しグラフが含まれていて大変役に立つが、大きなプログラムだと木構造が一見して分かり辛い。特に、「この関数に時間が掛かっているのは分かったが、コストが高いのはどこから呼ばれる場合か」のようなことを知るのが面倒だ。そこで.profファイルを対話的に閲覧できるツールを書いた。
使い方
- j: 一行下へ
- k: 一行上へ
- Ctrl-d: 半ページ下へ
- Ctrl-u: 半ページ上へ
- スペース: 木構造の展開/畳み
- l: 右へスクロール
- h: 左へスクロール
- r: ボトムアップモードへ
- q: 終了
適当な行にカーソルを合わせてrキーを押すと木構造の親子関係が逆転する。選択されたコスト集約点が根となり、それを呼んでいる各地点が子になる。この状態では、選択されたコスト集約点の時間/空間コストのうち、それぞれの呼び出し地点の寄与分が「contribution」列に表示される。もう一度rキーを押すと元に戻る。
検索機能は未実装。これがないと使い物にならないかもしれないが、とりあえず今日はここまで。
コード
実行にはSBCLとcl-ncursesが必要。初心者なので変なことをしていたら教えてくれると有難い。
#!/usr/bin/sbcl --script (require 'asdf) (require 'cl-ncurses) (defpackage viewprof (:use common-lisp) (:use cl-ncurses)) (in-package viewprof) ;;;; rose tree (defstruct rtree value children) ; list of rtrees (defun maptree (fn tree) (make-rtree :value (funcall fn (rtree-value tree)) :children (mapcar (lambda (sub) (maptree fn sub)) (rtree-children tree)))) (defun tree-from-list (list) (destructuring-bind (val . children) list (make-rtree :value val :children (mapcar #'tree-from-list children)))) ;;;; tree cursor (defstruct cursor current parents) ; list of (node . index) conses, deepest first (defun head-cursor (tree) (make-cursor :current tree :parents nil)) ; returns the next visitable node in the tree, or nil ; if there is none (defun cursor-next (cursor visitable) (cursor-next-upwards (cons (cons (cursor-current cursor) -1) (cursor-parents cursor)) visitable)) (defun cursor-next-upwards (context visitable) (and context (destructuring-bind ((node . index) . rest) context (let* ((parents (mapcar #'car context)) (next (position-if (lambda (child) (funcall visitable child parents)) (rtree-children node) :start (1+ index)))) (if next (make-cursor :current (elt (rtree-children node) next) :parents (cons (cons node next) rest)) (cursor-next-upwards rest visitable)))))) (defun cursor-prev (cursor visitable) (let ((context (cursor-parents cursor))) (and context (destructuring-bind ((node . index) . rest) context (cursor-prev-downwards node index rest visitable))))) (defun cursor-prev-downwards (node end context visitable) (let* ((parents (cons node (mapcar #'car context))) (prev (position-if (lambda (child) (funcall visitable child parents)) (rtree-children node) :end end :from-end t))) (if prev (cursor-prev-downwards (elt (rtree-children node) prev) nil (cons (cons node prev) context) visitable) (make-cursor :current node :parents context)))) (defun cursor-move (cursor count visitable) (loop while (< 0 count) do (decf count) (setf cursor (or (cursor-next cursor visitable) cursor))) (loop while (< count 0) do (incf count) (setf cursor (or (cursor-prev cursor visitable) cursor))) cursor) ;;;; .prof parser (defun unindent (str) (let ((pos (or (position-if (lambda (x) (not (eql x #\ ))) str) 0))) (cons pos (subseq str pos)))) (defun get-unindented (bufstream) (if (cdr bufstream) (let ((putback (cdr bufstream))) (setf (cdr bufstream) nil) putback) (let ((line (read-line (car bufstream) nil))) (when line (unindent line))))) (defun unget-unindented (bufstream value) (setf (cdr bufstream) value) nil) (defun read-trees (input) (let ((buf (cons input nil))) (loop for tree = (read-subtree buf 0) while tree collect (car tree)))) (defun read-subtree (input minimum-level) (let ((head (get-unindented input))) (when head (destructuring-bind (head-level . head-str) head (if (< head-level minimum-level) (unget-unindented input head) (let* ((children (read-children input head-level)) (tree (make-rtree :value head-str :children children))) (cons tree head-level))))))) (defun read-children (input parent-level) (let ((fst (read-subtree input (+ 1 parent-level)))) (when fst (destructuring-bind (fst-subtree . level) fst (cons fst-subtree (loop for item = (read-subtree input level) while item collect (car item))))))) (defun read-trees-from-prof (input) (let ((cost-centre-count 0)) (loop for line = (read-line input) do (when (search "COST CENTRE" line) (when (< 1 (incf cost-centre-count)) (read-line input); skip an empty line (return (read-trees input))))))) ;;;; internal representation of an entire profile (defstruct line name module idnum count individual inherited) (defparameter *case-sensitive-readtable* (copy-readtable nil)) (setf (readtable-case *case-sensitive-readtable*) :preserve) (defun parse-line (line) (let* ((*read-eval* nil) (*readtable* *case-sensitive-readtable*) (list-str (concatenate 'string "(" line ")")) (list (read-from-string list-str))) (destructuring-bind (name module idnum count idtime idalloc ihtime ihalloc) list (make-line :name name :module module :idnum idnum :count count :individual (cons idtime idalloc) :inherited (cons ihtime ihalloc))))) (defun trees-from-file (file) (mapcar (lambda (tree) (maptree #'parse-line tree)) (with-open-file (input file) (read-trees-from-prof input)))) ; info on a cost center (defstruct cc name module key occurrences) ; an occurrence in a call tree (defstruct occurrence cc count parent ; occurrence children ; list of occurrences individual inherited) ; i store the whole information from a profiling report as a graph. ; a graph is represented by a hashtable from keys to ccs ; a key is a symbol of the form: ; <name> <module> ; build a graph from a set of trees containing lines. (defun build-graph (trees) (let ((table (make-hash-table))) (dolist (tree trees) (add-tree table tree)) table)) (defun add-tree (table tree &optional parent) (let* ((root (rtree-value tree)) (rootkey (key-from-line root)) (cc (or (gethash rootkey table) (setf (gethash rootkey table) (make-cc :name (line-name root) :module (line-module root) :key rootkey :occurrences nil)))) (occ (new-occurrence parent cc root))) (setf (cc-occurrences cc) (cons occ (cc-occurrences cc))) (dolist (child (rtree-children tree)) (add-tree table child occ)))) (defun key-from-line (line) (make-key (line-name line) (line-module line))) (defun make-key (name module) (intern (concatenate 'string (symbol-name name) " " (symbol-name module)))) (defun new-occurrence (parent cc line) (let ((occ (make-occurrence :cc cc :count (line-count line) :parent parent :children nil :individual (line-individual line) :inherited (line-inherited line)))) (when parent (setf (occurrence-children parent) (cons occ (occurrence-children parent)))) occ)) ;;;; tree reversal (defun parent-tree (root) (parent-tree-from (mapcar (lambda (occ) (cons occ (occurrence-inherited occ))) (cc-occurrences root)))) (defun parent-tree-from (heads) (make-rtree :value (cons (occurrence-cc (car (car heads))) (reduce #'add-timealloc (mapcar #'cdr heads))) :children (let* ((next-heads (remove-if (lambda (x) (not (car x))) (mapcar (lambda (x) (cons (occurrence-parent (car x)) (cdr x))) heads))) (g (group-on (lambda (x) (occurrence-cc (car x))) next-heads))) (mapcar #'parent-tree-from g)))) (defun add-timealloc (x y) (destructuring-bind ((a . b) (c . d)) (list x y) (cons (+ a c) (+ b d)))) (defun show-parent-tree-item (item) (destructuring-bind (cc . timealloc) item (format nil "(~A% ~A%) ~A ~A" (car timealloc) (cdr timealloc) (cc-name cc) (cc-module cc)))) ;;;; curses interface (defun init-curses () (initscr) (start-color) (init-pair 1 COLOR_WHITE COLOR_BLACK) (init-pair 2 COLOR_BLACK COLOR_WHITE) (init-pair 3 COLOR_CYAN COLOR_BLACK) (noecho) (curs-set 0) (bkgd (color-pair 1)) (clear) (move 0 0)) (defun render-tree-part (visitable height cursor cursor-pos) (let* ((endpoint (cursor-move cursor (- height cursor-pos 1) visitable)) (startpoint (cursor-move endpoint (- 1 height) visitable))) (loop for c = startpoint then (cursor-next c visitable) for i from 1 to height while c collect (cursor-current c)))) (defun fix-cursor-pos (height requested-cursor-pos) (cond ((< height 5) 0) ((< requested-cursor-pos 2) 2) ((< (- height 3) requested-cursor-pos) (- height 3)) (t requested-cursor-pos))) (defun tree-view-filtering (view) (lambda (node context) (declare (ignore node)) (or (not context) (gethash (car context) (tree-view-unfold-table view))))) (defstruct tree-view heading ypos current unfold-table height width cursor-pos) (defmacro with-color (w color-id &rest body) `(progn (wattron ,w (color-pair ,color-id)) ,@body (wattroff ,w (color-pair ,color-id)))) (defun tree-view-render (w view) (let ((lines (render-tree-part (tree-view-filtering view) (1- (tree-view-height view)) (tree-view-current view) (tree-view-cursor-pos view)))) (wmove w 0 0) (werase w) (with-color w 3 (wprintw w (fixed-width-line "" (tree-view-heading view) (tree-view-width view)))) (dolist (node lines) (let ((line (format-tree-view-item (rtree-value node) (gethash node (tree-view-unfold-table view)) (consp (rtree-children node)) (tree-view-ypos view) (tree-view-width view)))) (if (eq node (cursor-current (tree-view-current view))) (with-color w 2 (wprintw w line)) (wprintw w line)))))) (defstruct tree-view-item name module infostr indentation-level) (defun format-tree-view-item (item unfolded has-children &optional (offset 0) (width 80)) (let* ((idstr (format nil "~v,0T~A ~A ~0,13T~A" (tree-view-item-indentation-level item) (cond ((not has-children) " ") (unfolded "-") (t "+")) (tree-view-item-name item) (tree-view-item-module item))) (head (subseq idstr (min offset (length idstr)))) (info (tree-view-item-infostr item))) (fixed-width-line head info width))) (defun fixed-width-line (x y width) (let* ((x-length (length x)) (x-width (- width 1 (length y)))) (if (<= x-length x-width) (format nil "~A~v,0T ~A~%" x x-width y) (format nil "~A... ~A~%" (subseq x 0 (- x-width 3)) y)))) (defun make-tree-view-tree (item-maker tree &optional (level 0)) (make-rtree :value (funcall item-maker level (rtree-value tree)) :children (mapcar (lambda (child) (make-tree-view-tree item-maker child (1+ level))) (rtree-children tree)))) (defun tree-view-item-from-line (level line) (make-tree-view-item :name (line-name line) :module (line-module line) :infostr (format nil "~A ~6,2F ~6,2F ~6,2F ~6,2F" (line-count line) (car (line-individual line)) (cdr (line-individual line)) (car (line-inherited line)) (cdr (line-inherited line))) :indentation-level level)) (defun tree-view-item-from-parent-tree-item (level pitem) (destructuring-bind (cc . (time . alloc)) pitem (let ((total-inher (cc-sum #'add-timealloc #'occurrence-inherited cc)) (total-indiv (cc-sum #'add-timealloc #'occurrence-individual cc))) (make-tree-view-item :name (cc-name cc) :module (cc-module cc) :infostr (format nil "~A ~6,2F ~6,2F ~6,2F ~6,2F ~6,2F ~6,2F" (cc-sum #'+ #'occurrence-count cc) time alloc (car total-indiv) (cdr total-indiv) (car total-inher) (cdr total-inher)) :indentation-level level)))) (defun cc-sum (reducer mapper cc) (reduce reducer (mapcar mapper (cc-occurrences cc)))) (defun tree-view (tree heading height width) (make-tree-view :heading heading :ypos 0 :current (head-cursor tree) :unfold-table (make-hash-table) :height height :width width :cursor-pos 0)) (defun ui-loop (tree window width height) (let* ((base-view (tree-view (make-tree-view-tree #'tree-view-item-from-line tree) "count individual inherited" height width)) (view base-view) (graph (build-graph (list tree)))) (flet ((reverse-tree-view () (let ((cc (gethash (tree-view-current-key view) graph))) (when cc (tree-view (make-tree-view-tree #'tree-view-item-from-parent-tree-item (parent-tree cc)) "count contribution individual inherited" height width))))) (loop do (tree-view-render window view) (let ((ch (getch))) (cond ((= ch (char-code #\j)) (tree-view-dn view)) ((= ch (char-code #\k)) (tree-view-up view)) ((= ch (ctrl-code #\D)) (tree-view-dn-halfwindow view)) ((= ch (ctrl-code #\U)) (tree-view-up-halfwindow view)) ((= ch (char-code #\l)) (tree-view-right view)) ((= ch (char-code #\h)) (tree-view-left view)) ((= ch (char-code #\ )) (tree-view-toggle view)) ((= ch (char-code #\r)) (setf view (if (eq view base-view) (or (reverse-tree-view) view) base-view))) ((= ch (char-code #\q)) (return))))) nil))) (defun ctrl-code (char) (logandc1 64 (char-code char))) (defun ui-main (tree) (init-curses) (ui-loop tree *stdscr* (min 120 (1- *cols*)) *lines*) (endwin)) (defun tree-view-set-cursor-pos (view pos) (setf (tree-view-cursor-pos view) (fix-cursor-pos (tree-view-height view) pos))) (defun tree-view-up (view) (let ((p (cursor-prev (tree-view-current view) (tree-view-filtering view)))) (when p (setf (tree-view-current view) p) (tree-view-set-cursor-pos view (1- (tree-view-cursor-pos view)))))) (defun tree-view-dn (view) (let ((p (cursor-next (tree-view-current view) (tree-view-filtering view)))) (when p (setf (tree-view-current view) p) (tree-view-set-cursor-pos view (1+ (tree-view-cursor-pos view)))))) (defun tree-view-up-halfwindow (view) (tree-view-move-halfwindow #'tree-view-up view)) (defun tree-view-dn-halfwindow (view) (tree-view-move-halfwindow #'tree-view-dn view)) (defun tree-view-move-halfwindow (mv view) (dotimes (k (floor (/ (tree-view-height view) 2))) (funcall mv view))) (defun tree-view-right (view) (setf (tree-view-ypos view) (+ 2 (tree-view-ypos view)))) (defun tree-view-left (view) (setf (tree-view-ypos view) (max 0 (- (tree-view-ypos view) 2)))) (defun tree-view-toggle (view) (tree-view-toggle-unfold view (cursor-current (tree-view-current view)))) (defun tree-view-toggle-unfold (view node) (setf (gethash node (tree-view-unfold-table view)) (not (gethash node (tree-view-unfold-table view))))) (defun tree-view-current-key (view) (let ((item (rtree-value (cursor-current (tree-view-current view))))) (make-key (tree-view-item-name item) (tree-view-item-module item)))) ;;;; utilities (defun group-on (make-key list) (let ((table (make-hash-table))) (dolist (item list) (let ((key (funcall make-key item))) (setf (gethash key table) (cons item (gethash key table))))) (loop for x being the hash-values in table collect x))) ;;;; driver (defun main (args) (if (= (length args) 1) (let ((trees (trees-from-file (car args)))) (if trees (ui-main (car trees)) (format t "viewprof: Failed to read ~A" (car args)))) (format t "Usage: viewprof.lisp PROG.prof"))) (main (cdr sb-ext:*posix-argv*))