• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pkryger / basic-stats.el / 13112944620

03 Feb 2025 11:58AM UTC coverage: 98.611% (+1.3%) from 97.297%
13112944620

push

github

pkryger
feat: Add benchmark

Model usage on Google Benchmark with support for simultaneous running of
multiple forms. Collect mutliple samples of each from (time limitted,
leveraging built-in `benchmark-call` resultiing with - likely - many
repetitions of each form). Compute statistcs over average run times in each
sample. Support interleaving, as Google Benchmark does.

106 of 107 new or added lines in 1 file covered. (99.07%)

142 of 144 relevant lines covered (98.61%)

325.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.61
/basic-stats.el
1
;;; basic-stats.el --- Basic statistic functions     -*- lexical-binding: t -*-
2

3

4
;; Author: Przemyslaw Kryger <pkryger@gmail.com>
5
;; Keywords: tools statistics
6
;; Homepage: https://github.com/pkryger/basic-stats.el
7
;; Package-Requires: ((emacs "28.1"))
8
;; Version: 0.0.0
9

10
;;; This file is not a part of GNU Emacs.
11

12
;;; Commentary:
13

14
;; Provide a few basic functions for statistics.
15
;; Quartiles are implemented as per https://en.wikipedia.org/wiki/Quartile.
16

17
;;; License:
18

19
;;; MIT License
20

21
;;; Copyright (c) 2020 Przemysław Kryger
22

23
;;; Permission is hereby granted, free of charge, to any person obtaining a
24
;;; copy of this software and associated documentation files (the "Software"),
25
;;; to deal in the Software without restriction, including without limitation
26
;;; the rights to use, copy, modify, merge, publish, distribute, sublicense,
27
;;; and/or sell copies of the Software, and to permit persons to whom the
28
;;; Software is furnished to do so, subject to the following conditions:
29

30
;;; The above copyright notice and this permission notice shall be included in
31
;;; all copies or substantial portions of the Software.
32

33
;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34
;;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35
;;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36
;;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37
;;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
38
;;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
39
;;; DEALINGS IN THE SOFTWARE.
40

41
;;; Code:
42

43
(require 'cl-lib)
44
(require 'seq)
45

46
(defun basic-stats-quartile--internal (sequence quartile &optional method)
47
  "Return a given QUARTILE of a sorted SEQUENCE.
48
The optional METHOD is the same as in `basic-stats-quartile'."
49
  (let* ((length (length sequence))
1,220✔
50
         (offset (if (and (cl-oddp length)
1,220✔
51
                          (eq method :include))
740✔
52
                     1 0)))
1,220✔
53
    (cl-flet ((median (sequence)
×
54
                      (let* ((length (length sequence))
880✔
55
                             (mid (/ length 2)))
880✔
56
                        (if (cl-oddp length)
880✔
57
                            (seq-elt sequence mid)
480✔
58
                          (/ (+ (seq-elt sequence (1- mid))
400✔
59
                                (seq-elt sequence mid))
400✔
60
                             2.0)))))
400✔
61
        (cond
1,220✔
62
         ((and (< 1 length) (eq quartile 2))
920✔
63
          (median sequence))
320✔
64
         ((and (< 2 length) (eq quartile 1))
560✔
65
          (median (seq-subseq sequence
280✔
66
                              0
67
                              (+ (/ length 2)
280✔
68
                                 offset))))
280✔
69
         ((and (< 2 length) (eq quartile 3))
280✔
70
          (median (seq-subseq sequence
280✔
71
                              (- (1+ (/ length 2))
280✔
72
                                 offset))))))))
280✔
73

74
;;;###autoload
75
(defun basic-stats-quartile (sequence quartile &optional method sorted)
76
  "Return a given QUARTILE for the specified SEQUENCE.
77

78
When the optional METHOD is nil or `:exclude', the value is returned according
79
to Method 1 from Wiki: `https://en.wikipedia.org/wiki/Quartile'.
80

81
The METHOD can be `:include' to use Method 2 instead.
82

83
When SORTED is t it indicates the sequence is already sorted.
84

85
Return nil unless one of:
86
- QUARTILE is one of 1, 2, or 3,
87
- SEQUENCE length is >=1 and QUARTILE is 2,
88
- SEQUENCE length is >=2 and QUARTILE is one of 1 or 3."
89
  (basic-stats-quartile--internal (if sorted
1,220✔
90
                             sequence
910✔
91
                           (cl-sort sequence '<))
310✔
92
                         quartile method))
1,220✔
93

94
;;;###autoload
95
(defun basic-stats-median (sequence &optional sorted)
96
  "Return a median for the specified SEQUENCE.
97
The optional SORTED is the same as in `basic-stats-quartile'."
98
  (basic-stats-quartile sequence 2 nil sorted))
300✔
99

100
;;;###autoload
101
(defun basic-stats-five-nums (sequence &optional method sorted)
102
  "Return a list consisting of (min q1 med q3 max) for the specified SEQUENCE.
103
The optional METHOD and SORTED are the same as in `basic-stats-quartile'.
104
When some values cannot be calculated they are set to nil."
105
  (if (and (not sorted) sequence)
240✔
106
      (setq sequence (cl-sort sequence '<)))
240✔
107
  (list (when sequence (seq-min sequence))
280✔
108
        (basic-stats-quartile sequence 1 method t)
280✔
109
        (basic-stats-median sequence t)
280✔
110
        (basic-stats-quartile sequence 3 method t)
280✔
111
        (when sequence (seq-max sequence))))
280✔
112

113
;;;###autoload
114
(defun basic-stats-five-nums-with-header (sequence &optional method sorted)
115
  "Return`basic-stats-five-nums' for the specified SEQUENCE with a header.
116
This is meant as a convenience function for `org-mode' code block to be used
117
with ':output table'.  The optional METHOD and SORTED are the same as in
118
`basic-stats-quartile'."
119
  (list (list "min" "q1" "med" "q3" "max")
40✔
120
        'hline
121
        (basic-stats-five-nums sequence method sorted)))
40✔
122

123
(defun basic-stats--convert-time (time)
124
  "Convert TIME to a human readable format."
125
  (cond
1,130✔
126
   ((null time) nil)
1,130✔
127
   ((= 0 time) "0")
970✔
128
   ((<= 3600 time)
903✔
129
    (let* ((hours (floor (/ time 3600)))
20✔
130
           (fseconds (- time (* hours 3600)))
20✔
131
           (seconds (floor fseconds)))
20✔
132
      (format "%dh %dmin %d.%ds"
20✔
133
              hours
20✔
134
              (/ seconds 60)
20✔
135
              (% seconds 60)
20✔
136
              (* 1000 (- fseconds seconds)))))
20✔
137
   ((<= 60 time)
883✔
138
    (let ((seconds (floor time)))
10✔
139
      (format "%dmin %d.%ds"
10✔
140
              (/ seconds 60)
10✔
141
              (% seconds 60)
10✔
142
              (* 1000 (- time seconds)))))
10✔
143
   ((<= 1 time)
873✔
144
    (format "%.3fs" time))
90✔
145
   ((<= 1e-3 time)
783✔
146
    (format "%.3fms" (* 1000 time)))
64✔
147
   ((<= 1e-6 time)
719✔
148
    (format "%.3fµs" (* 1000 1000 time)))
140✔
149
   ((<= 1e-9 time)
579✔
150
    (format "%.3fns" (* 1000 1000 1000 time)))
549✔
151
   (t
152
    (format "%.3fps" (* 1000 1000 1000 1000 time)))))
30✔
153

154
(defun basic-stats--convert-times (times human-readable)
155
  "Calculate five numbers for  TIMES.
156
TIMES is a list of numbers to calculate `basic-stats-five-nums'
157
on.  When HUMAN-READABLE is non-nil, duration are reported as
158
strings with time units."
159
  (cl-remove-if (lambda (elt)
200✔
160
                  (null (cdr elt)))
1,000✔
161
                (cl-mapcar (lambda (label num)
200✔
162
                             (cons label
1,000✔
163
                                   (if human-readable
1,000✔
164
                                       (basic-stats--convert-time num)
700✔
165
                                     num)))
300✔
166
                           '(min q1 med q3 max)
167
                           (basic-stats-five-nums times))))
200✔
168

169
(defun basic-stats--convert-result (result human-readable)
170
  "Convert RESULT to a reportable form.
171
RESULT is a list where each element is in a from (REPETITIONS
172
TIME GC GC-TIME), like a value returned from `benchmark-call',
173
which see.  When HUMAN-READABLE is non-nil, duration are reported
174
as strings with time units."
175
  (let ((total (cl-reduce
80✔
176
                (lambda (acc res)
177
                  (list (+ (nth 0 acc) (nth 0 res))
660✔
178
                        (+ (nth 1 acc) (nth 1 res))
660✔
179
                        (+ (nth 2 acc) (nth 2 res))
660✔
180
                        (+ (nth 3 acc) (nth 3 res))))
660✔
181
                result))
80✔
182
        (times  (basic-stats--convert-times
80✔
183
                 (mapcar (lambda (res)
80✔
184
                           (/ (nth 1 res)
740✔
185
                              (nth 0 res)))
740✔
186
                         result)
80✔
187
                 human-readable))
80✔
188
        (times-sans-gc (basic-stats--convert-times
80✔
189
                        (mapcar (lambda (res)
80✔
190
                                  (/ (- (nth 1 res) (nth 3 res))
740✔
191
                                     (nth 0 res)))
740✔
192
                                result)
80✔
193
                        human-readable)))
80✔
194
    (list
80✔
195
     (cons 'repetitions (nth 0 total))
80✔
196
     (cons 'total-time
80✔
197
           (let ((total-time (nth 1 total)))
80✔
198
             (if human-readable
80✔
199
                 (basic-stats--convert-time total-time)
60✔
200
               total-time)))
20✔
201
     (cons 'mean-time
80✔
202
           (let ((mean-time (/ (nth 1 total)
80✔
203
                               (nth 0 total))))
80✔
204
           (if human-readable
80✔
205
               (basic-stats--convert-time mean-time)
60✔
206
             mean-time)))
20✔
207
     (cons 'gc (nth 2 total))
80✔
208
     (cons 'gc-time
80✔
209
           (let ((gc-time (nth 3 total)))
80✔
210
             (if human-readable
80✔
211
                 (basic-stats--convert-time gc-time)
60✔
212
               gc-time)))
20✔
213
     (cons 'mean-time-sans-gc
80✔
214
           (let ((mean-time-sans-gc (/ (- (nth 1 total) (nth 3 total))
80✔
215
                                       (nth 0 total))))
80✔
216
             (if human-readable
80✔
217
                 (basic-stats--convert-time mean-time-sans-gc)
60✔
218
               mean-time-sans-gc)))
20✔
219
     (cons 'times times)
80✔
220
     (cons 'times-sans-gc times-sans-gc))))
80✔
221

222
;;;###autoload
223
(cl-defmacro basic-stats-benchmark
224
    (specs &key (samples 12) (time .5) (interleave t) (human-readable t))
225
  "Perform benchmark of specified SPECS.
226
Each element of SPECS is a list (SYMBOL FORM) where symbol is
227
a user specified name and FORM is the form to be benchmarked.  If
228
first element of FORM is `eq' to `progn' it will be skipped in
229
benchmarked code.  For example the following FORM:
230
  (progn
231
    (foo)
232
    (bar))
233
will result with the following code being benchmarked:
234
  (foo)
235
  (bar)
236

237
The argument SAMPLES is the number of times each FORM is
238
sampled (note, there may be many repetitions of FORM in a single
239
sample).  The argument TIME is for how long to keep collecting
240
samples (in seconds).  In other words each sample being measured
241
for approximately TIME/REPETITIONS seconds and the total
242
execution time being approximately (length SPECS)*TIME seconds.
243

244
When INTERLEAVE is non-nil benchmarking of consecutive samples of
245
a given FORM is randomly interleaved with benchmarking of other
246
FORMs.  For more rationale, please see
247
https://github.com/google/benchmark/issues/1051.
248

249
When HUMAN-READABLE is non-nil duration values in the returned
250
value are strings with unit suffixes, for example h, min, s, ms,
251
µs, ns, ps.
252

253
The value returned is an alist where each element is in a form
254
of (SYMBOL . REPORT).  The REPORT is an alist with the following
255
keys:
256
 - repetitions: how many times the FORM has been repeated (total
257
   fol all samples),
258
 - total-time: how long it took to execute FORM  (total for all
259
   samples),
260
 - mean-time: the mean repetition time (across all samples),
261
 - gc: how many times garbage collection was run (total for all
262
   samples),
263
 - gc-time: how long it took to execute garbage collection (total
264
   for all samples),
265
 - times and times-sans-gc: an alist with statistics computed for
266
   average times of collected samples, where times include total
267
   run time, while times-sans-gc substracts garbage collection
268
   running time for a given sample from a time of a given sample."
269
  (declare (indent 1))
270
  (let ((max-time (/ (float time) samples))
50✔
271
        (plan (apply #'vconcat
50✔
272
                     (mapcar (lambda (binding)
50✔
273
                               (make-vector samples (car binding)))
100✔
274
                             specs)))
50✔
275
        (resultsvar (make-symbol "results")))
50✔
276
    (when (and interleave
50✔
277
               (< 1 (length specs)))
40✔
278
      ;; https://en.wikipedia.org/wiki/Fisher–Yates_shuffle#The_modern_algorithm
279
      (dotimes (n (1- (length plan)))
40✔
280
        (let* ((i (- (length plan) n 1))
40✔
281
               (j (random (1+ i)))
40✔
282
               (tmp (aref plan j)))
40✔
283
          (aset plan j (aref plan i))
40✔
284
          (aset plan i tmp))))
40✔
285
    (append `(let (,resultsvar))
50✔
286
            (mapcar (lambda (elt)
50✔
287
                      `(push
740✔
288
                        (benchmark-call
289
                         (lambda ()
290
                           ,@(let ((func (cadr (assq elt specs))))
740✔
291
                               (if (and (listp func) (eq 'progn (car func)))
740✔
NEW
292
                                   (cdr func)
×
293
                                 (list func))))
740✔
294
                         ,max-time)
740✔
295
                        (alist-get ',elt ,resultsvar)))
740✔
296
                    plan)
50✔
297
            `((mapcar (lambda (result)
50✔
298
                        (cons (car result)
299
                              (basic-stats--convert-result (cdr result)
300
                                                           ,human-readable)))
50✔
301
                      ,resultsvar)))))
50✔
302

303
;; LocalWords: quartile el
304

305
(provide 'basic-stats)
306

307
;;; basic-stats.el ends here
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc