summaryrefslogtreecommitdiff
path: root/lisp/emacs-lisp/erb-task.el
blob: e5041e58617f194454e9acbb742693a791e6f5a9 (plain)
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
291
292
293
294
295
296
297
298
299
;;; erb-task.el --- Emacs Regression Benchmarking -*- lexical-binding: t -*-

;; Copyright (C) 2018 Free Software Foundation, Inc.

;; Author: Gemini Lasswell
;; Keywords: lisp, tools
;; Version: 1.0

;; This file is part of GNU Emacs.

;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; ERB is a tool for automated benchmarking in Emacs Lisp.  This file
;; implements defining and running benchmark tasks within an Emacs
;; instance.

;; See the file erb.el for the rest of ERB, which implements a user
;; interface for building older versions of Emacs, running the
;; benchmark tasks in them, managing a database of results, and
;; presenting them.

;; For usage information, see ERB's info manual.

;; Significant changes to benchmark.el over the years:
;;   In Emacs 21 500ae43022, benchmark.el was added.
;;   In Emacs 23 e2bac5f625, benchmark-elapse was changed to use
;;     float-time and time-subtract.
;;   In Emacs 26 c7d2a0dd76, repetitions is allowed to be a symbol.

;;; Code:

;; Since it is necessary to load this file into older versions of
;; Emacs in order to define benchmark tasks for them to run, the code
;; in this file must avoid using features or libraries which are not
;; present in those older versions.
(require 'benchmark)

;;; Define benchmark tasks

(defmacro erb-deftask (name _arglist &rest docstring-keys-and-body)
  "Define NAME (a symbol) as a benchmark task.

BODY is evaluated as a `progn' when the task is run.  It should
contain a `erb-task-time' form wrapping the code to be
benchmarked.  Any setup or cleanup work done outside of the
`erb-task-time' form will not be benchmarked.

DOCSTRING-KEYS-AND-BODY may begin with an optional docstring and
an optional plist.  Valid keywords for use as properties in the
plist are:

:version

  A version number for this task, which should be increased if the
  task is changed sufficiently to invalidate previous measurements.

:rev-list

  A list of strings to use as arguments to git-rev-list(1) to get
  the list of commits for which this task should be run.

:discard-first-sample

  If non-nil, discard the result of the first run of the task.
  Use this if you notice the first sample is consistently much
  larger than the following samples.

:special

  If this exists and the value is `startup' a body for the task
  is not required, and the benchmark runner will instead time the
  startup and shutdown of Emacs.  If the value is `own-process',
  run this task in its own process instead of a process shared
  with other tasks."

  (declare (indent 2)
           (doc-string 3)
           (debug (&define :name task
                           name sexp [&optional stringp]
                           [&optional (&rest keywordp sexp)]
                           def-body)))
  (let ((documentation nil)
        (keys nil))
    (when (stringp (car-safe docstring-keys-and-body))
      (setq documentation (car docstring-keys-and-body))
      (pop docstring-keys-and-body))
    (when (keywordp (car-safe (car-safe docstring-keys-and-body)))
      (setq keys (car docstring-keys-and-body))
      (pop docstring-keys-and-body))
    `(progn
       (erb-task--set ',name
                      (erb-task--create-task ',name ,documentation ',keys
                                             (lambda ()
                                               ,@docstring-keys-and-body)))
       ',name)))

(defun erb-task--key-plist-p (list)
  "Return non-nil if LIST is a plist using keywords valid in ERB.
Those are :version, :rev-list, :discard-first-sample, and
:special."
  (while (consp list)
    (setq list (if (and (consp (cdr list))
                        (or (and (eq (car list) :version)
                                 (stringp (cadr list)))
                            (and (eq (car list) :rev-list)
                                 (listp (cadr list)))
                            (and (eq (car list) :special) (symbolp (cadr list)))
                            (eq (car list) :discard-first-sample)))
                   (cddr list)
                 'not-plist)))
  (null list))

(defvar erb-task--result nil)

(defmacro erb-task-time (&rest body)
  "Save timing results for BODY.
Use this macro inside of a benchmark task defined by
`benchmark-deftask' to define the code to be benchmarked.  Only
use it once per task."
  ;; TODO should this collect gc statistics?
  ;; as in (memory-use-counts) before and after,
  ;; do subtraction and sum
  `(progn
     (garbage-collect)
     (setq erb-task--result (benchmark-run ,@body))))

;;;  Internal representation of tasks

;; Use an alist so as not to have to worry about what
;; cl-defstruct was called in old versions of Emacs.
(defun erb-task--create-task (name doc keys body)
  (unless (erb-task--key-plist-p keys)
    (error "Keyword plist for %s contains unexpected keys"
           name))
  `((:name . ,name)
    (:documentation . ,doc)
    (:key-plist . ,keys)
    (:body . ,body)
    ,(cons :results nil)
    ,(cons :messages nil)))

(defsubst erb-task--name (task)
  (alist-get :name task))
(defsubst erb-task--documentation (task)
  (alist-get :documentation task))
(defsubst erb-task--body (task)
  (alist-get :body task))
(defsubst erb-task--key-plist (task)
  (alist-get :key-plist task))
(defsubst erb-task--results (task)
  (alist-get :results task))
(defsubst erb-task--add-result (result task)
  (push result (alist-get :results task)))
(defsubst erb-task--discard-result (task)
  (pop (alist-get :results task)))
(defsubst erb-task--messages (task)
  (alist-get :messages task))
(defsubst erb-task--add-message (message task)
  (push message (alist-get :messages task)))

(defun erb-task--boundp (symbol)
  "Return non-nil if SYMBOL names a task."
  (and (get symbol 'erb-task) t))

(defun erb-task--get-task (symbol)
  "If SYMBOL names a task, return that.  Signal an error otherwise."
  (unless (erb-task--boundp symbol)
    (error "No task named `%S'" symbol))
  (get symbol 'erb-task))

(defun erb-task--all-symbols ()
  (apropos-internal "" #'erb-task--boundp))

(defun erb-task--version (task)
  (plist-get (erb-task--key-plist task) :version))

(defun erb-task--rev-list (task)
  (plist-get (erb-task--key-plist task) :rev-list))

(defun erb-task--set (symbol definition)
  "Make SYMBOL name the task DEFINITION, and return DEFINITION."
  (when (eq symbol 'nil)
    (error "Attempt to define a task named nil"))
  (put symbol 'erb-task definition)
  definition)

(defun erb-task--make-unbound (symbol)
  "Make SYMBOL name no task.
Return SYMBOL."
  (put symbol 'erb-task nil)
  symbol)

(defun erb-delete-all-tasks ()
  "Make all symbols in `obarray' name no task."
  (interactive)
  (when (called-interactively-p 'any)
    (unless (y-or-n-p "Delete all tasks? ")
      (user-error "Aborted")))
  (mapc #'erb-task--make-unbound (erb-task--all-symbols)))

;;; Running tasks

(defvar erb-task-repetitions 10
  "Number of times to run each task.")

(defun erb-task-run-batch (symbols output-file)
  "Run defined benchmark tasks in batch mode.
SYMBOLS is a list of the names of the tasks.  Run each one
`erb-repetitions' times.  Write to OUTPUT-FILE an list of
results.  Each entry of the list will be of the form:

   ((name . NAME)
    (version . VERSION)
    (samples . SAMPLES-LIST)
    (messages . MESSAGES))

where NAME is the name of the task, VERSION is its version as
defined in the optional plist given to `erb-deftask',
SAMPLES-LIST is a list of the return values of benchmark-run, and
MESSAGES is a list of strings containing the messages issued
while the task was running.

If there were errors while running the task,
elements of SAMPLES-LIST will be of the form (error ERROR-INFO)
instead.  This function is used as a command-line entry point
into the target Emacs by `erb-run-start'."
  (let ((print-level nil)
        (print-length nil))
    (dolist (symbol symbols)
      (let* ((task (erb-task--get-task symbol))
             (key-plist (erb-task--key-plist task))
             (discard-first (plist-get key-plist :discard-first-sample)))
        (unless noninteractive
          (message "Running %s" symbol))
        (dotimes (i (+ erb-task-repetitions (if discard-first 1 0)))
          (erb-task--run symbol)
          (when (and discard-first (zerop i))
            (erb-task--discard-result task)))))

    (with-temp-file output-file
      (let ((results
             (mapcar (lambda (symbol)
                       (let ((task (erb-task--get-task symbol)))
                         `((name . ,symbol)
                           (version . ,(erb-task--version task))
                           (samples ,@(reverse (erb-task--results task)))
                           (messages ,@(reverse (erb-task--messages task))))))
                     symbols)))

        (insert (with-temp-buffer
                 (prin1 results (current-buffer))
                 (pp-buffer)
                 (buffer-string)))))))

(defun erb-task-run-all (&optional repetitions)
  "Run all defined benchmark tasks REPETITIONS times and message the results.
REPETITIONS defaults to 1."
  (interactive "p")
  (unless (natnump repetitions) (setq repetitions 1))
  (dotimes (_i repetitions)
    (mapc #'erb-task--run (erb-task--all-symbols)))
  (message "Results:")
  (mapc #'erb-task--message-results (erb-task--all-symbols)))

(defun erb-task--run (symbol)
  "Run the benchmark task associated with SYMBOL."
  (let ((task (erb-task--get-task symbol))
        (message-marker (with-current-buffer (messages-buffer)
                                (point-max-marker))))
    (condition-case err
        (progn
          (setq erb-task--result nil)
          (funcall (erb-task--body task)))
      (error (setq erb-task--result err)))
    (erb-task--add-result erb-task--result task)
    (erb-task--add-message (with-current-buffer (messages-buffer)
                             (buffer-substring message-marker (point-max)))
                           task)))

(defun erb-task--message-results (symbol)
  (message "%s: " symbol)
  (dolist (item (reverse (erb-task--results (erb-task--get-task symbol))))
    (message "  %s" item)))

(provide 'erb-task)
;;; erb-task.el ends here