This repository has been archived by the owner on Jun 3, 2022. It is now read-only.
forked from Shinmera/trivial-benchmark
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtimer.lisp
290 lines (244 loc) · 10.3 KB
/
timer.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
#|
This file is a part of Trivial-Benchmark
(c) 2015 Shirakumo http://tymoon.eu ([email protected])
Author: Nicolas Hafner <[email protected]>
|#
(in-package #:org.shirakumo.trivial-benchmark)
(defvar *default-metrics*
()
"The list of class-names used to populate a TIMER by default.")
(defvar *default-computations*
'(:samples :total :minimum :maximum :median :average :deviation)
"The list of computation-names used to print the REPORT table.")
(defclass metric ()
((running :initarg :running :initform NIL :accessor running))
(:documentation "A class container for sampling information."))
(defgeneric running (metric)
(:documentation "Returns T if the metric is currently sampling.
See START
See STOP"))
(defmethod print-object ((metric metric) stream)
(print-unreadable-object (metric stream :type T)
(format stream "~s ~a" :size (sample-size metric))))
(defgeneric start (metric)
(:documentation "Begin a sample for METRIC.
Sets RUNNING to T.")
(:method :around ((metric metric))
(unless (running metric)
(call-next-method)
(setf (running metric) T))))
(defgeneric stop (metric)
(:documentation "Stop the sample for METRIC.
Sets RUNNING to NIL.")
(:method :around ((metric metric))
(when (running metric)
(call-next-method)
(setf (running metric) NIL)))
(:method ((metric metric))
NIL))
(defgeneric discard (metric)
(:documentation "Discard the current sample of METRIC.
If the metric is running, call STOP first.")
(:method :before ((metric metric))
(when (running metric) (stop metric))))
(defgeneric commit (metric)
(:documentation "Commit the current sample of METRIC.
If the metric is running, call STOP first.")
(:method :before ((metric metric))
(when (running metric) (stop metric))))
(defgeneric take-sample (metric)
(:documentation "Return a current sampling value for METRIC.
Note that not all metrics must implement a method for this function.
It is perfectly plausible for a metric to skip this method if it
cannot provide a sample value at any point in time."))
(defgeneric samples (metric)
(:documentation "Return a sequence of committed samples stored in METRIC."))
(defgeneric sample-size (metric)
(:documentation "Return the number of samples stored in METRIC.")
(:method (metric)
(length (samples metric))))
(defgeneric condense (sample)
(:documentation "Turn the SAMPLE value into a usable number.")
(:method (thing)
thing))
(defgeneric reduce-samples (metric function)
(:documentation "Apply FUNCTION to the samples stored in METRIC in a REDUCE fashion.")
(:method (metric function)
(reduce function (samples metric) :key #'condense)))
(defgeneric compute (thing metric)
(:documentation "Compute a value of the statistical computation THING for METRIC based on its current samples.")
(:method ((x (eql :count)) (metric metric))
(sample-size metric))
(:method ((x (eql :samples)) (metric metric))
(sample-size metric))
(:method ((x (eql :total)) (metric metric))
(if (= 0 (sample-size metric))
:n/a
(reduce-samples metric #'+)))
(:method ((x (eql :minimum)) (metric metric))
(if (= 0 (sample-size metric))
:n/a
(reduce-samples metric #'min)))
(:method ((x (eql :maximum)) (metric metric))
(if (= 0 (sample-size metric))
:n/a
(reduce-samples metric #'max)))
(:method ((x (eql :median)) (metric metric))
(if (= 0 (sample-size metric))
:n/a
(elt (sort (copy-seq (samples metric)) #'<)
(1- (ceiling (/ (compute :samples metric) 2))))))
(:method ((x (eql :average)) (metric metric))
(if (= 0 (sample-size metric))
:n/a
(/ (compute :total metric)
(compute :samples metric))))
(:method ((x (eql :deviation)) (metric metric))
(if (= 0 (sample-size metric))
:n/a
(let ((metrics (samples metric))
(average (compute :average metric)))
(sqrt
(/ (reduce #'+ (map (class-of metrics)
(lambda (a) (expt (- a average) 2))
metrics))
(compute :samples metric))))))
(:method ((computations list) (metric metric))
(mapcar (lambda (thing) (compute thing metric)) computations)))
(defgeneric report-to (stream thing &key computations)
(:method ((stream (eql T)) thing &key computations)
(report-to *standard-output* thing :computations computations))
(:method ((string (eql NIL)) thing &key computations)
(with-output-to-string (stream)
(report-to stream thing :computations computations)))
(:method ((stream stream) (metric metric) &key computations)
(print-table
(cons (list :computation :value)
(loop for comp in computations
collect (list comp (compute comp metric))))
:stream stream)))
(defgeneric report (thing &key stream computations)
(:documentation "Print a report of all COMPUTATIONS for THING to STREAM
STREAM can be one of the following:
T --- Print to *standard-output*
NIL --- Print to a string and return it.
STREAM --- Print to the stream")
(:method (thing &key (stream T) (computations *default-computations*))
(report-to stream thing :computations computations)))
(defgeneric reset (metric)
(:documentation "Reset the METRIC and remove all its samples."))
(defclass timer ()
((metrics :initarg :metrics :accessor metrics))
(:documentation "Class container for a set of METRICS."))
(defgeneric metrics (timer)
(:documentation "Returns a list of metrics stored in TIMER."))
(defgeneric metric (type timer)
(:documentation "Returns the metric of TYPE in TIMER if any.
The metric must match the type by TYPE=")
(:method ((type symbol) (timer timer))
(find type (metrics timer) :test #'type=)))
(defgeneric (setf metric) (metric timer)
(:documentation "Sets the METRIC in TIMER.
The metric is replaced if it is found in the timer by TYPE= comparison.")
(:method ((metric metric) (timer timer))
(let ((pos (position metric (metrics timer) :test #'type=)))
(if pos
(setf (nth pos (metrics timer)) metric)
(push metric (metrics timer))))))
(defgeneric metric-types (timer)
(:documentation "Returns the types of metrics in TIMER.")
(:method ((timer timer))
(mapcar #'type-of (metrics timer))))
(defmethod initialize-instance :after ((timer timer) &key)
(unless (slot-boundp timer 'metrics)
(setf (metrics timer) ())
(loop for type in *default-metrics*
for metric = (make-instance type)
do (setf (metric timer) metric))))
(defmethod print-object ((timer timer) stream)
(print-unreadable-object (timer stream :type T)
(format stream "~{~a~^ ~}" (metric-types timer))))
(defun make-timer (&optional (metric-types *default-metrics*))
"Creates a TIMER object using the given METRIC-TYPES"
(let ((*default-metrics* metric-types))
(make-instance 'timer)))
(defun format-timer-stats (stream timer &optional (computations *default-computations*))
(print-table
(cons (cons :- computations)
(loop for metric in (metrics timer)
collect (list* (type-of metric)
(mapcar (lambda (a)
(typecase a
(symbol (format NIL "~a" a))
(fixnum (format NIL "~d" a))
(T (format NIL "~f" (round-to a 6)))))
(compute computations metric)))))
:stream stream))
(defmethod describe-object ((timer timer) stream)
(let ((*print-pretty* T))
(format stream "This is an object for keeping benchmarking data.")
(format stream "~&~%It tracks the following metric types:")
(pprint-indent :block 2 stream)
(format stream "~&~{~a~^, ~}" (metric-types timer))
(terpri stream)
(format stream "~&~%The statistics for the timer are:~&")
(report timer :stream stream)))
(defun map-metrics (timer function)
"Maps the metrics in TIMER, calling FUNCTION with each."
(dolist (metric (metrics timer))
(funcall function metric)))
(defmacro do-metrics ((metric-var timer &optional result-form) &body forms)
"Binds METRIC-VAR to each metric of TIMER and then evaluates FORMS.
Returns the value of RESULT-FORM after the loop."
`(block NIL
(map-metrics ,timer (lambda (,metric-var) ,@forms))
,result-form))
(defmethod take-sample ((timer timer))
(map-metrics timer #'take-sample))
(defmethod start ((timer timer))
(map-metrics timer #'start))
(defmethod stop ((timer timer))
(map-metrics timer #'stop))
(defmethod discard ((timer timer))
(map-metrics timer #'discard))
(defmethod commit ((timer timer))
(map-metrics timer #'commit))
(defmethod report-to ((stream stream) (timer timer) &key computations)
(if (< 0 (reduce #'max (mapcar #'sample-size (metrics timer))))
(format-timer-stats stream timer computations)
(format stream "No metric has any samples yet.")))
(defmethod reset ((timer timer))
(map-metrics timer #'reset))
(defmacro with-sampling ((timer-form) &body forms)
"Takes a sample for the evaluation time of FORMS and stores it in the timer given by TIMER-FORM.
Acts like a PROGN.
Specifically, START is called, then FORMS are evaluated. If an error occurs within the body,
DISCARD is called on the timer, otherwise COMMIT is called once the body exits.
See START
See DISCARD
See COMMIT"
(let ((timer (gensym "TIMER"))
(errord (gensym "ERRORD")))
`(let ((,timer ,timer-form)
(,errord NIL))
(start ,timer)
(unwind-protect
(handler-bind ((error (lambda (err)
(declare (ignore err))
(setf ,errord T))))
,@forms)
(unless ,errord
(stop ,timer)
(if ,errord
(discard ,timer)
(commit ,timer)))))))
(defmacro with-timing ((n &optional (timer-form '(make-timer)) (stream T) (computations '*default-computations*)) &body forms)
"Evaluates FORMS N times, using WITH-SAMPLING on the value of TIMER-FORM each iteration.
At the end, runs REPORT on the timer with STREAM and COMPUTATIONS.
See WITH-SAMPLING"
(let ((timer (gensym "TIMER")))
`(let ((,timer ,timer-form))
(loop repeat ,n
do (with-sampling (,timer)
,@forms))
(report ,timer :stream ,stream :computations ,computations))))