#-(or clisp openmcl clasp)
(warn "metering.lisp does not support your Lisp implementation!")
(defpackage "SWANK-MONITOR" (:use "COMMON-LISP")
(:export "*MONITORED-FUNCTIONS*"
"MONITOR" "MONITOR-ALL" "UNMONITOR" "MONITOR-FORM"
"WITH-MONITORING"
"RESET-MONITORING-INFO" "RESET-ALL-MONITORING"
"MONITORED"
"REPORT-MONITORING"
"DISPLAY-MONITORING-RESULTS"
"MONITORING-ENCAPSULATE" "MONITORING-UNENCAPSULATE"
"REPORT"))
(in-package "SWANK-MONITOR")
(eval-when (eval)
(warn "This file should be compiled before loading for best results."))
(defparameter *metering-version* "v2.1 25-JAN-94"
"Current version number/date for Metering.")
#-(or clasp clisp openmcl)
(eval-when (compile eval)
(warn
"You may want to supply implementation-specific get-time functions."))
(defconstant time-units-per-second internal-time-units-per-second)
#+(or clasp openmcl)
(progn
(deftype time-type () 'unsigned-byte)
(deftype consing-type () 'unsigned-byte))
(defmacro get-time ()
`(the time-type (get-internal-run-time)))
#+clisp
(defun get-cons ()
(multiple-value-bind (real1 real2 run1 run2 gc1 gc2 space1 space2 gccount)
(sys::%%time)
(declare (ignore real1 real2 run1 run2 gc1 gc2 gccount))
(dpb space1 (byte 24 24) space2)))
#+openmcl
(defmacro get-cons () `(the consing-type (ccl::total-bytes-allocated)))
#+clasp
(defmacro get-cons ()
`(the consing-type (gctools::bytes-allocated)))
#-(or clasp clisp openmcl)
(progn
(eval-when (compile eval)
(warn "No consing will be reported unless a get-cons function is ~
defined."))
(defmacro get-cons () '(the consing-type 0)))
#-:clisp
(defmacro with-time/cons ((delta-time delta-cons) form &body post-process)
(let ((start-cons (gensym "START-CONS-"))
(start-time (gensym "START-TIME-")))
`(let ((,start-time (get-time)) (,start-cons (get-cons)))
(declare (type time-type ,start-time)
(type consing-type ,start-cons))
(multiple-value-prog1 ,form
(let ((,delta-time (- (get-time) ,start-time))
(,delta-cons (- (get-cons) ,start-cons)))
,@post-process)))))
#+clisp
(progn
(defmacro delta4 (nv1 nv2 ov1 ov2 by)
`(- (dpb (- ,nv1 ,ov1) (byte ,by ,by) ,nv2) ,ov2))
(let ((del (find-symbol "DELTA4" "SYS")))
(when del (setf (fdefinition 'delta4) (fdefinition del))))
(if (< internal-time-units-per-second 1000000)
(defmacro delta4-time (new-time1 new-time2 old-time1 old-time2)
`(delta4 ,new-time1 ,new-time2 ,old-time1 ,old-time2 16))
(defmacro delta4-time (new-time1 new-time2 old-time1 old-time2)
`(+ (* (- ,new-time1 ,old-time1) internal-time-units-per-second)
(- ,new-time2 ,old-time2))))
(defmacro delta4-cons (new-cons1 new-cons2 old-cons1 old-cons2)
`(delta4 ,new-cons1 ,new-cons2 ,old-cons1 ,old-cons2 24))
(defmacro with-time/cons ((delta-time delta-cons) form &body post-process)
(let ((beg-cons1 (gensym "BEG-CONS1-")) (end-cons1 (gensym "END-CONS1-"))
(beg-cons2 (gensym "BEG-CONS2-")) (end-cons2 (gensym "END-CONS2-"))
(beg-time1 (gensym "BEG-TIME1-")) (end-time1 (gensym "END-TIME1-"))
(beg-time2 (gensym "BEG-TIME2-")) (end-time2 (gensym "END-TIME2-"))
(re1 (gensym)) (re2 (gensym)) (gc1 (gensym)) (gc2 (gensym)))
`(multiple-value-bind (,re1 ,re2 ,beg-time1 ,beg-time2
,gc1 ,gc2 ,beg-cons1 ,beg-cons2)
(sys::%%time)
(declare (ignore ,re1 ,re2 ,gc1 ,gc2))
(multiple-value-prog1 ,form
(multiple-value-bind (,re1 ,re2 ,end-time1 ,end-time2
,gc1 ,gc2 ,end-cons1 ,end-cons2)
(sys::%%time)
(declare (ignore ,re1 ,re2 ,gc1 ,gc2))
(let ((,delta-time (delta4-time ,end-time1 ,end-time2
,beg-time1 ,beg-time2))
(,delta-cons (delta4-cons ,end-cons1 ,end-cons2
,beg-cons1 ,beg-cons2)))
,@post-process)))))))
#+openmcl
(defun required-arguments (name)
(let* ((function (symbol-function name))
(args (ccl:arglist function))
(pos (position-if #'(lambda (x)
(and (symbolp x)
(let ((name (symbol-name x)))
(and (>= (length name) 1)
(char= (schar name 0)
#\&)))))
args)))
(if pos
(values pos t)
(values (length args) nil))))
#+clisp
(defun required-arguments (name)
(multiple-value-bind (name req-num opt-num rest-p key-p keywords allow-p)
(sys::function-signature name t)
(if name (values req-num (or (/= 0 opt-num) rest-p key-p keywords allow-p))
(values 0 t))))
#+clasp
(defun required-arguments (name)
(multiple-value-bind (arglist foundp)
(core:function-lambda-list name)
(if foundp
(let ((position-and
(position-if #'(lambda (x)
(and (symbolp x)
(let ((name (symbol-name x)))
(and (>= (length name) 1)
(char= (schar name 0)
#\&)))))
arglist)))
(if position-and
(values position-and t)
(values (length arglist) nil)))
(values 0 t))))
#-(or clasp clisp openmcl)
(progn
(eval-when (compile eval)
(warn
"You may want to add an implementation-specific ~
Required-Arguments function."))
(eval-when (load eval)
(defun required-arguments (name)
(declare (ignore name))
(values 0 t))))
(defvar *MONITOR-TIME-OVERHEAD* nil
"The amount of time an empty monitored function costs.")
(defvar *MONITOR-CONS-OVERHEAD* nil
"The amount of cons an empty monitored function costs.")
(defvar *TOTAL-TIME* 0
"Total amount of time monitored so far.")
(defvar *TOTAL-CONS* 0
"Total amount of consing monitored so far.")
(defvar *TOTAL-CALLS* 0
"Total number of calls monitored so far.")
(proclaim '(type time-type *total-time*))
(proclaim '(type consing-type *total-cons*))
(proclaim '(fixnum *total-calls*))
(defmacro PLACE-FUNCTION (function-place)
"Return the function found at FUNCTION-PLACE. Evals FUNCTION-PLACE
if it isn't a symbol, to allow monitoring of closures located in
variables/arrays/structures."
(if (fboundp 'fdefinition)
`(if (fboundp ,function-place)
(fdefinition ,function-place)
(eval ,function-place))
`(if (symbolp ,function-place)
(symbol-function ,function-place)
(eval ,function-place))))
(defsetf PLACE-FUNCTION (function-place) (function)
"Set the function in FUNCTION-PLACE to FUNCTION."
(if (fboundp 'fdefinition)
`(if (fboundp ,function-place)
(setf (fdefinition ,function-place) ,function)
(eval '(setf ,function-place ',function)))
`(if (symbolp ,function-place)
(setf (symbol-function ,function-place) ,function)
(eval '(setf ,function-place ',function)))))
(defun PLACE-FBOUNDP (function-place)
"Test to see if FUNCTION-PLACE is a function."
(if (symbolp function-place)
(fboundp function-place)
(functionp (place-function function-place))))
(defun PLACE-MACROP (function-place)
"Test to see if FUNCTION-PLACE is a macro."
(when (symbolp function-place)
(macro-function function-place)))
(defvar *monitored-functions* nil
"List of monitored symbols.")
(defstruct metering-functions
(name nil)
(old-definition nil :type function)
(new-definition nil :type function)
(read-metering nil :type function)
(reset-metering nil :type function))
(defvar *monitor* (make-hash-table :test #'equal)
"Hash table in which METERING-FUNCTIONS structures are stored.")
(defun get-monitor-info (name)
(gethash name *monitor*))
(defsetf get-monitor-info (name) (info)
`(setf (gethash ,name *monitor*) ,info))
(defun MONITORED (function-place)
"Test to see if a FUNCTION-PLACE is monitored."
(and (place-fboundp function-place) (get-monitor-info function-place)))
(defun reset-monitoring-info (name)
"Reset the monitoring info for the specified function."
(let ((finfo (get-monitor-info name)))
(when finfo
(funcall (metering-functions-reset-metering finfo)))))
(defun reset-all-monitoring ()
"Reset monitoring info for all functions."
(setq *total-time* 0
*total-cons* 0
*total-calls* 0)
(dolist (symbol *monitored-functions*)
(when (monitored symbol)
(reset-monitoring-info symbol))))
(defun monitor-info-values (name &optional (nested :exclusive) warn)
"Returns monitoring information values for the named function,
adjusted for overhead."
(let ((finfo (get-monitor-info name)))
(if finfo
(multiple-value-bind (inclusive-time inclusive-cons
exclusive-time exclusive-cons
calls nested-calls)
(funcall (metering-functions-read-metering finfo))
(unless (or (null warn)
(eq (place-function name)
(metering-functions-new-definition finfo)))
(warn "Funtion ~S has been redefined, so times may be inaccurate.~@
MONITOR it again to record calls to the new definition."
name))
(case nested
(:exclusive (values calls
nested-calls
(- exclusive-time
(* calls *monitor-time-overhead*))
(- exclusive-cons
(* calls *monitor-cons-overhead*))))
(:inclusive (values calls
nested-calls
(- inclusive-time
(* nested-calls *monitor-time-overhead*))
(- inclusive-cons
(* nested-calls *monitor-cons-overhead*))))))
(values 0 0 0 0))))
(eval-when (compile load eval)
(defun make-monitoring-encapsulation (min-args optionals-p)
(let (required-args)
(dotimes (i min-args) (push (gensym) required-args))
`(lambda (name)
(let ((inclusive-time 0)
(inclusive-cons 0)
(exclusive-time 0)
(exclusive-cons 0)
(calls 0)
(nested-calls 0)
(old-definition (place-function name)))
(declare (type time-type inclusive-time)
(type time-type exclusive-time)
(type consing-type inclusive-cons)
(type consing-type exclusive-cons)
(fixnum calls)
(fixnum nested-calls))
(pushnew name *monitored-functions*)
(setf (place-function name)
#'(lambda (,@required-args
,@(when optionals-p
`(&rest optional-args)))
(let ((prev-total-time *total-time*)
(prev-total-cons *total-cons*)
(prev-total-calls *total-calls*)
)
(declare (type time-type prev-total-time)
(type consing-type prev-total-cons)
(fixnum prev-total-calls))
(with-time/cons (delta-time delta-cons)
,(if optionals-p
`(apply old-definition
,@required-args optional-args)
`(funcall old-definition ,@required-args))
(incf calls)
(incf *total-calls*)
(incf nested-calls (the fixnum
(- *total-calls*
prev-total-calls)))
(incf inclusive-time (the time-type delta-time))
(incf exclusive-time (the time-type
(+ delta-time
(- prev-total-time
*total-time*))))
(setf *total-time* (the time-type
(+ delta-time
prev-total-time)))
(incf inclusive-cons (the consing-type delta-cons))
(incf exclusive-cons (the consing-type
(+ delta-cons
(- prev-total-cons
*total-cons*))))
(setf *total-cons*
(the consing-type
(+ delta-cons prev-total-cons)))))))
(setf (get-monitor-info name)
(make-metering-functions
:name name
:old-definition old-definition
:new-definition (place-function name)
:read-metering #'(lambda ()
(values inclusive-time
inclusive-cons
exclusive-time
exclusive-cons
calls
nested-calls))
:reset-metering #'(lambda ()
(setq inclusive-time 0
inclusive-cons 0
exclusive-time 0
exclusive-cons 0
calls 0
nested-calls 0)
t)))))))
)
(eval-when (compile eval)
(defconstant precomputed-encapsulations 8))
(defvar *existing-encapsulations* (make-hash-table :test #'equal))
(defun find-encapsulation (min-args optionals-p)
(or (gethash (cons min-args optionals-p) *existing-encapsulations*)
(setf (gethash (cons min-args optionals-p) *existing-encapsulations*)
(compile nil
(make-monitoring-encapsulation min-args optionals-p)))))
(macrolet ((frob ()
(let ((res ()))
(dotimes (i precomputed-encapsulations)
(push `(setf (gethash '(,i . nil) *existing-encapsulations*)
#',(make-monitoring-encapsulation i nil))
res)
(push `(setf (gethash '(,i . t) *existing-encapsulations*)
#',(make-monitoring-encapsulation i t))
res))
`(progn ,@res))))
(frob))
(defun monitoring-encapsulate (name &optional warn)
"Monitor the function Name. If already monitored, unmonitor first."
(cond ((not (place-fboundp name)) (when warn
(warn "Ignoring undefined function ~S." name)))
((place-macrop name) (when warn
(warn "Ignoring macro ~S." name)))
(t (when (get-monitor-info name) (when warn
(warn "~S already monitored, so unmonitoring it first." name))
(monitoring-unencapsulate name))
(multiple-value-bind (min-args optionals-p)
(required-arguments name)
(funcall (find-encapsulation min-args optionals-p) name)))))
(defun monitoring-unencapsulate (name &optional warn)
"Removes monitoring encapsulation code from around Name."
(let ((finfo (get-monitor-info name)))
(when finfo (remprop name 'metering-functions)
(setq *monitored-functions*
(remove name *monitored-functions* :test #'equal))
(if (eq (place-function name)
(metering-functions-new-definition finfo))
(setf (place-function name)
(metering-functions-old-definition finfo))
(when warn
(warn "Preserving current definition of redefined function ~S."
name))))))
(defmacro MONITOR (&rest names)
"Monitor the named functions. As in TRACE, the names are not evaluated.
If a function is already monitored, then unmonitor and remonitor (useful
to notice function redefinition). If a name is undefined, give a warning
and ignore it. See also unmonitor, report-monitoring,
display-monitoring-results and reset-time."
`(progn
,@(mapcar #'(lambda (name) `(monitoring-encapsulate ',name)) names)
*monitored-functions*))
(defmacro UNMONITOR (&rest names)
"Remove the monitoring on the named functions.
Names defaults to the list of all currently monitored functions."
`(dolist (name ,(if names `',names '*monitored-functions*) (values))
(monitoring-unencapsulate name)))
(defun MONITOR-ALL (&optional (package *package*))
"Monitor all functions in the specified package."
(let ((package (if (packagep package)
package
(find-package package))))
(do-symbols (symbol package)
(when (eq (symbol-package symbol) package)
(monitoring-encapsulate symbol)))))
(defmacro MONITOR-FORM (form
&optional (nested :exclusive) (threshold 0.01)
(key :percent-time))
"Monitor the execution of all functions in the current package
during the execution of FORM. All functions that are executed above
THRESHOLD % will be reported."
`(unwind-protect
(progn
(monitor-all)
(reset-all-monitoring)
(prog1
(time ,form)
(report-monitoring :all ,nested ,threshold ,key :ignore-no-calls)))
(unmonitor)))
(defmacro WITH-MONITORING ((&rest functions)
(&optional (nested :exclusive)
(threshold 0.01)
(key :percent-time))
&body body)
"Monitor the specified functions during the execution of the body."
`(unwind-protect
(progn
(dolist (fun ',functions)
(monitoring-encapsulate fun))
(reset-all-monitoring)
,@body
(report-monitoring :all ,nested ,threshold ,key))
(unmonitor)))
(defconstant overhead-iterations 5000
"Number of iterations over which the timing overhead is averaged.")
(defun STUB-FUNCTION (x)
(declare (ignore x))
nil)
(proclaim '(notinline stub-function))
(defun SET-MONITOR-OVERHEAD ()
"Determines the average overhead of monitoring by monitoring the execution
of an empty function many times."
(setq *monitor-time-overhead* 0
*monitor-cons-overhead* 0)
(stub-function nil)
(monitor stub-function)
(reset-all-monitoring)
(let ((overhead-function (symbol-function 'stub-function)))
(dotimes (x overhead-iterations)
(funcall overhead-function overhead-function)))
(let ((fiter (float overhead-iterations)))
(multiple-value-bind (calls nested-calls time cons)
(monitor-info-values 'stub-function)
(declare (ignore calls nested-calls))
(setq *monitor-time-overhead* (/ time fiter)
*monitor-cons-overhead* (/ cons fiter))))
(unmonitor stub-function))
(set-monitor-overhead)
(defvar *monitor-results* nil
"A table of monitoring statistics is stored here.")
(defvar *no-calls* nil
"A list of monitored functions which weren't called.")
(defvar *estimated-total-overhead* 0)
(defstruct (monitoring-info
(:conc-name m-info-)
(:constructor make-monitoring-info
(name calls time cons
percent-time percent-cons
time-per-call cons-per-call)))
name
calls
time
cons
percent-time
percent-cons
time-per-call
cons-per-call)
(defun REPORT (&key (names :all)
(nested :exclusive)
(threshold 0.01)
(sort-key :percent-time)
(ignore-no-calls nil))
"Same as REPORT-MONITORING but with a nicer keyword interface"
(declare (type (member :function :percent-time :time :percent-cons
:cons :calls :time-per-call :cons-per-call)
sort-key)
(type (member :inclusive :exclusive) nested))
(report-monitoring names nested threshold sort-key ignore-no-calls))
(defun REPORT-MONITORING (&optional names
(nested :exclusive)
(threshold 0.01)
(key :percent-time)
ignore-no-calls)
"Report the current monitoring state.
The percentage of the total time spent executing unmonitored code
in each function (:exclusive mode), or total time (:inclusive mode)
will be printed together with the number of calls and
the unmonitored time per call. Functions that have been executed
below THRESHOLD % of the time will not be reported. To report on all
functions set NAMES to be either NIL or :ALL."
(when (or (null names) (eq names :all)) (setq names *monitored-functions*))
(let ((total-time 0)
(total-cons 0)
(total-calls 0))
(dolist (name names)
(multiple-value-bind (calls nested-calls time cons)
(monitor-info-values name nested :warn)
(declare (ignore nested-calls))
(incf total-calls calls)
(incf total-time time)
(incf total-cons cons)))
(setq *estimated-total-overhead*
(/ (* *monitor-time-overhead* total-calls)
time-units-per-second))
(if (zerop total-time)
(format *trace-output* "Not enough execution time to monitor.")
(progn
(setq *monitor-results* nil *no-calls* nil)
(dolist (name names)
(multiple-value-bind (calls nested-calls time cons)
(monitor-info-values name nested)
(declare (ignore nested-calls))
(when (minusp time) (setq time 0.0))
(when (minusp cons) (setq cons 0.0))
(if (zerop calls)
(push (if (symbolp name)
(symbol-name name)
(format nil "~S" name))
*no-calls*)
(push (make-monitoring-info
(format nil "~S" name) calls (/ time (float time-units-per-second)) (round cons) (/ time (float total-time)) (if (zerop total-cons) 0
(/ cons (float total-cons))) (/ (/ time (float calls)) time-units-per-second) (round (/ cons (float calls)))) *monitor-results*))))
(display-monitoring-results threshold key ignore-no-calls)))))
(defun display-monitoring-results (&optional (threshold 0.01)
(key :percent-time)
(ignore-no-calls t))
(let ((max-length 8) (max-cons-length 8)
(total-time 0.0)
(total-consed 0)
(total-calls 0)
(total-percent-time 0)
(total-percent-cons 0))
(sort-results key)
(dolist (result *monitor-results*)
(when (or (zerop threshold)
(> (m-info-percent-time result) threshold))
(setq max-length
(max max-length
(length (m-info-name result))))
(setq max-cons-length
(max max-cons-length
(m-info-cons-per-call result)))))
(incf max-length 2)
(setf max-cons-length (+ 2 (ceiling (log max-cons-length 10))))
(format *trace-output*
"~%~%~
~VT ~VA~
~% ~VT % % ~VA ~
Total Total~
~%Function~VT Time Cons Calls Sec/Call ~VA ~
Time Cons~
~%~V,,,'-A"
max-length
max-cons-length "Cons"
max-length
max-cons-length "Per"
max-length
max-cons-length "Call"
(+ max-length 62 (max 0 (- max-cons-length 5))) "-")
(dolist (result *monitor-results*)
(when (or (zerop threshold)
(> (m-info-percent-time result) threshold))
(format *trace-output*
"~%~A:~VT~6,2F ~6,2F ~7D ~,6F ~VD ~8,3F ~10D"
(m-info-name result)
max-length
(* 100 (m-info-percent-time result))
(* 100 (m-info-percent-cons result))
(m-info-calls result)
(m-info-time-per-call result)
max-cons-length
(m-info-cons-per-call result)
(m-info-time result)
(m-info-cons result))
(incf total-time (m-info-time result))
(incf total-consed (m-info-cons result))
(incf total-calls (m-info-calls result))
(incf total-percent-time (m-info-percent-time result))
(incf total-percent-cons (m-info-percent-cons result))))
(format *trace-output*
"~%~V,,,'-A~
~%TOTAL:~VT~6,2F ~6,2F ~7D ~9@T ~VA ~8,3F ~10D~
~%Estimated monitoring overhead: ~5,2F seconds~
~%Estimated total monitoring overhead: ~5,2F seconds"
(+ max-length 62 (max 0 (- max-cons-length 5))) "-"
max-length
(* 100 total-percent-time)
(* 100 total-percent-cons)
total-calls
max-cons-length " "
total-time total-consed
(/ (* *monitor-time-overhead* total-calls)
time-units-per-second)
*estimated-total-overhead*)
(when (and (not ignore-no-calls) *no-calls*)
(setq *no-calls* (sort *no-calls* #'string<))
(let ((num-no-calls (length *no-calls*)))
(if (> num-no-calls 20)
(format *trace-output*
"~%~@(~r~) monitored functions were not called. ~
~%See the variable swank-monitor::*no-calls* for a list."
num-no-calls)
(format *trace-output*
"~%The following monitored functions were not called:~
~%~{~<~%~:; ~A~>~}~%"
*no-calls*))))
(values)))
(defun sort-results (&optional (key :percent-time))
(setq *monitor-results*
(case key
(:function (sort *monitor-results* #'string>
:key #'m-info-name))
((:percent-time :time) (sort *monitor-results* #'>
:key #'m-info-time))
((:percent-cons :cons) (sort *monitor-results* #'>
:key #'m-info-cons))
(:calls (sort *monitor-results* #'>
:key #'m-info-calls))
(:time-per-call (sort *monitor-results* #'>
:key #'m-info-time-per-call))
(:cons-per-call (sort *monitor-results* #'>
:key #'m-info-cons-per-call)))))