summaryrefslogtreecommitdiff
path: root/src/cuirass/metrics.scm
blob: 2c61555561db6032a842bd222fb6cfb1c590f230 (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
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
;;; metrics.scm -- Compute and store metrics.
;;; This file is part of Cuirass.
;;;
;;; Cuirass 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.
;;;
;;; Cuirass 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 Cuirass.  If not, see <http://www.gnu.org/licenses/>.

(define-module (cuirass metrics)
  #:use-module (cuirass database)
  #:use-module (cuirass logging)
  #:use-module (guix records)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-19)
  #:use-module (srfi srfi-26)
  #:use-module (ice-9 match)
  #:export (metric
            metric?
            metric-id
            metric-proc

            %metrics
            metric->type
            compute-metric

            db-get-metric
            db-get-metrics-with-id
            db-update-metric
            db-update-metrics))


;;;
;;; Metric record.
;;;

(define-record-type* <metric> metric make-metric
  metric?
  (id              metric-id)
  (compute-proc    metric-compute-proc)
  (field-proc      metric-field-proc
                   (default #f)))


;;;
;;; Database procedures.
;;;

(define* (db-average-eval-duration-per-spec spec #:key limit)
  "Return the average evaluation duration for SPEC.  Limit the average
computation to the most recent LIMIT records if this argument is set."
  (with-db-worker-thread db
    (let ((rows (sqlite-exec db "SELECT AVG(duration) FROM
(SELECT (evaltime - timestamp) as duration
FROM Evaluations WHERE specification = " spec
" AND evaltime != 0 ORDER BY rowid DESC
LIMIT " (or limit -1) ");")))
      (and=> (expect-one-row rows) (cut vector-ref <> 0)))))

(define (db-builds-previous-day _)
  "Return the builds count of the previous day."
  (with-db-worker-thread db
    (let ((rows (sqlite-exec db "SELECT COUNT(*) from Builds
WHERE date(timestamp, 'unixepoch') = date('now', '-1 day') AND
date(stoptime, 'unixepoch') = date('now', '-1 day');")))
      (and=> (expect-one-row rows) (cut vector-ref <> 0)))))

(define (db-new-derivations-previous-day _)
  "Return the new derivations count of the previous day."
  (with-db-worker-thread db
    (let ((rows (sqlite-exec db "SELECT COUNT(*) from Builds
WHERE date(timestamp, 'unixepoch') = date('now', '-1 day');")))
      (and=> (expect-one-row rows) (cut vector-ref <> 0)))))

(define (db-pending-builds _)
  "Return the current pending builds count."
  (with-db-worker-thread db
    (let ((rows (sqlite-exec db "SELECT COUNT(*) from Builds
WHERE status < 0;")))
      (and=> (expect-one-row rows) (cut vector-ref <> 0)))))

(define* (db-percentage-failed-eval-per-spec spec #:key limit)
  "Return the failed evaluation percentage for SPEC.  If LIMIT is set, limit
the percentage computation to the most recent LIMIT records."
  (with-db-worker-thread db
    (let ((rows (sqlite-exec db "\
SELECT 100 * CAST(SUM(status > 0) as float) / COUNT(*) FROM
(SELECT status from Evaluations WHERE specification = " spec
" ORDER BY rowid DESC LIMIT " (or limit -1) ");")))
      (and=> (expect-one-row rows) (cut vector-ref <> 0)))))

(define* (db-average-build-start-time-per-eval eval)
  "Return the average build start time for the given EVAL."
  (with-db-worker-thread db
    (let ((rows (sqlite-exec db "\
SELECT AVG(B.starttime - E.evaltime) FROM
(SELECT id, evaltime
FROM Evaluations WHERE id = " eval ") E
LEFT JOIN Builds as B
ON E.id = B.evaluation and B.starttime > 0
GROUP BY E.id;")))
      (and=> (expect-one-row rows) (cut vector-ref <> 0)))))

(define* (db-average-build-complete-time-per-eval eval)
  "Return the average build complete time for the given EVAL."
  (with-db-worker-thread db
    (let ((rows (sqlite-exec db "\
SELECT AVG(B.stoptime - E.evaltime) FROM
(SELECT id, evaltime
FROM Evaluations WHERE id = " eval ") E
LEFT JOIN Builds as B
ON E.id = B.evaluation and B.stoptime > 0
GROUP BY E.id;")))
      (and=> (expect-one-row rows) (cut vector-ref <> 0)))))

(define* (db-evaluation-completion-speed eval)
  "Return the evaluation completion speed of the given EVAL. The speed is
expressed in builds per hour."
  ;; completion_speed = 60 * completed_builds / eval_duration.
  ;;
  ;; evaluation_duration (seconds) = current_time - eval_start_time
  ;; If some evaluations builds are not completed.
  ;;
  ;; evaluation_duration (seconds) = max(build_stop_time) - eval_start_time
  ;; If the evaluation builds are all completed.
  (with-db-worker-thread db
    (let ((rows (sqlite-exec db "\
SELECT
3600.0 * SUM(B.status = 0) /
(CASE SUM(status < 0)
   WHEN 0 THEN MAX(stoptime)
   ELSE strftime('%s', 'now')
END - E.evaltime) FROM
(SELECT id, evaltime
FROM Evaluations WHERE id = " eval ") E
LEFT JOIN Builds as B
ON E.id = B.evaluation and B.stoptime > 0
GROUP BY E.id;")))
      (and=> (expect-one-row rows) (cut vector-ref <> 0)))))

(define (db-previous-day-timestamp)
  "Return the timestamp of the previous day."
  (with-db-worker-thread db
    (let ((rows (sqlite-exec db "SELECT strftime('%s',
date('now', '-1 day'));")))
      (and=> (expect-one-row rows) (cut vector-ref <> 0)))))

(define (db-current-day-timestamp)
  "Return the timestamp of the current day."
  (with-db-worker-thread db
    (let ((rows (sqlite-exec db "SELECT strftime('%s',
date('now'));")))
      (and=> (expect-one-row rows) (cut vector-ref <> 0)))))

(define* (db-latest-evaluations #:key (days 3))
  "Return the successful evaluations added during the previous DAYS."
  (with-db-worker-thread db
    (let ((query (format #f "SELECT id from Evaluations
WHERE date(timestamp, 'unixepoch') > date('now', '-~a day') AND
status = 0 ORDER BY rowid DESC" days)))
      (let loop ((rows (sqlite-exec db query))
                 (evaluations '()))
        (match rows
          (() (reverse evaluations))
          ((#(id) . rest)
           (loop rest
                 (cons id evaluations))))))))


;;;
;;; Definitions.
;;;

;; XXX: Make sure to add new metrics at the *end of the list* only, as they
;; are indexed by position in database.
(define %metrics
  (list
   ;; Average evaluation duration per specification.
   (metric
    (id 'average-10-last-eval-duration-per-spec)
    (compute-proc
     (cut db-average-eval-duration-per-spec <> #:limit 10)))

   (metric
    (id 'average-100-last-eval-duration-per-spec)
    (compute-proc
     (cut db-average-eval-duration-per-spec <> #:limit 100)))

   (metric
    (id 'average-eval-duration-per-spec)
    (compute-proc db-average-eval-duration-per-spec))

   ;; Builds count per day.
   (metric
    (id 'builds-per-day)
    (compute-proc db-builds-previous-day)
    (field-proc db-previous-day-timestamp))

   ;; Pending builds count.
   (metric
    (id 'pending-builds)
    (compute-proc db-pending-builds)
    (field-proc db-current-day-timestamp))

   ;; New derivations per day.
   (metric
    (id 'new-derivations-per-day)
    (compute-proc db-new-derivations-previous-day)
    (field-proc db-previous-day-timestamp))

   ;; Percentage of failed evaluations per specification.
   (metric
    (id 'percentage-failure-10-last-eval-per-spec)
    (compute-proc
     (cut db-percentage-failed-eval-per-spec <> #:limit 10)))

   (metric
    (id 'percentage-failure-100-last-eval-per-spec)
    (compute-proc
     (cut db-percentage-failed-eval-per-spec <> #:limit 100)))

   (metric
    (id 'percentage-failed-eval-per-spec)
    (compute-proc db-percentage-failed-eval-per-spec))

   ;; Average time to start a build for an evaluation.
   (metric
    (id 'average-eval-build-start-time)
    (compute-proc db-average-build-start-time-per-eval))

   ;; Average time to complete a build for an evaluation.
   (metric
    (id 'average-eval-build-complete-time)
    (compute-proc db-average-build-complete-time-per-eval))

   ;; Evaluation completion speed in builds/hour.
   (metric
    (id 'evaluation-completion-speed)
    (compute-proc db-evaluation-completion-speed))))

(define (metric->type metric)
  "Return the index of the given METRIC in %metrics list.  This index is used
to identify the metric type in database."
  (list-index
   (lambda (cur-metric)
     (eq? (metric-id cur-metric) (metric-id metric)))
   %metrics))

(define (find-metric id)
  "Find the metric with the given ID."
  (find (lambda (metric)
          (eq? (metric-id metric) id))
        %metrics))

(define* (compute-metric metric field)
  "Compute the given METRIC on FIELD and return the associated value."
  (let ((compute (metric-compute-proc metric)))
    (compute field)))

(define* (db-get-metric id field)
  "Return the metric with the given ID and FIELD."
  (let* ((metric (find-metric id))
         (type (metric->type metric)))
    (with-db-worker-thread db
    (let ((rows (sqlite-exec db "SELECT value from Metrics
WHERE type = " type " AND field = " field ";")))
      (and=> (expect-one-row rows) (cut vector-ref <> 0))))))

(define* (db-get-metrics-with-id id
                                 #:key
                                 limit
                                 (order "rowid DESC"))
  "Return the metrics with the given ID.  If LIMIT is set, the resulting list
if restricted to LIMIT records."
  (let* ((metric (find-metric id))
         (type (metric->type metric))
         (limit (or limit -1)))
    (with-db-worker-thread db
      (let ((query (format #f "SELECT field, value from Metrics
WHERE type = ? ORDER BY ~a LIMIT ~a" order limit)))
        (let loop ((rows (%sqlite-exec db query type))
                   (metrics '()))
          (match rows
            (() (reverse metrics))
            ((#(field value) . rest)
             (loop rest
                   `((,field . ,value)
                     ,@metrics)))))))))

(define* (db-update-metric id #:optional field)
  "Compute and update the value of the metric ID in database.

  FIELD is optional and can be the id of a database object such as an
evaluation or a specification that the METRIC applies to.  If FIELD is not
passed then the METRIC may provide a FIELD-PROC to compute it.  It is useful
for periodical metrics for instance."
  (define now
    (time-second (current-time time-utc)))

  (let* ((metric (find-metric id))
         (field-proc (metric-field-proc metric))
         (field (or field (field-proc)))
         (value (compute-metric metric field)))
    (if value
        (begin
          (log-message "Updating metric ~a (~a) to ~a."
                       (symbol->string id) field value)
          (with-db-worker-thread db
            (sqlite-exec db "\
INSERT OR REPLACE INTO Metrics (field, type, value,
timestamp) VALUES ("
                         field ", "
                         (metric->type metric) ", "
                         value ", "
                         now ");")
            (last-insert-rowid db)))
        (log-message "Failed to compute metric ~a (~a)."
                     (symbol->string id) field))))

(define (db-update-metrics)
  "Compute and update all available metrics in database."
  (define specifications
    (map (cut assq-ref <> #:name) (db-get-specifications)))

  ;; We can not update all evaluations metrics for performance reasons. Limit
  ;; to the evaluations that were added during the past three days.
  (define evaluations
    (db-latest-evaluations))

  (db-update-metric 'builds-per-day)
  (db-update-metric 'new-derivations-per-day)
  (db-update-metric 'pending-builds)

  ;; Update specification related metrics.
  (for-each (lambda (spec)
              (db-update-metric
               'average-10-last-eval-duration-per-spec spec)
              (db-update-metric
               'average-100-last-eval-duration-per-spec spec)
              (db-update-metric
               'average-eval-duration-per-spec spec)

              (db-update-metric
               'percentage-failure-10-last-eval-per-spec spec)
              (db-update-metric
               'percentage-failure-100-last-eval-per-spec spec)
              (db-update-metric
               'percentage-failed-eval-per-spec spec))
            specifications)

  ;; Update evaluation related metrics.
  (for-each (lambda (evaluation)
              (db-update-metric
               'average-eval-build-start-time evaluation)
              (db-update-metric
               'average-eval-build-complete-time evaluation)
              (db-update-metric
               'evaluation-completion-speed evaluation))
            evaluations))