Coverage report: /home/ellis/comp/core/lib/obj/cache.lisp
Kind | Covered | All | % |
expression | 2 | 895 | 0.2 |
branch | 0 | 114 | 0.0 |
Key
Not instrumented
Conditionalized out
Executed
Not executed
Both branches taken
One branch taken
Neither branch taken
1
;;; cache.lisp --- Cache Objects
3
;; Cache Object Protocol
7
;; based on CACLE: https://github.com/jlahd/cacle
9
;; CACLE provides a similar object, CACHE which obtains a block of data from a
10
;; provider function, given the block's key. The key is a user-defined value
11
;; that is used in an EQL hash-table, which provides storage for the cache
14
;; CACLE supports a variety of replacement policies which we also support. The
15
;; only major interface change is that we use keywords to indicate the policy
16
;; instead of objects and the :LFUDA policy is inferred simply from fixnums.
19
- First In First Out (:fifo): Data that has been in the cache for the longest time is discarded
20
- Last In First Out (:lifo): Most recently added data is discarded
21
- Least Recently Used (:lru): Data that has gone unused for the longest time is discarded
22
- Most Recently Used (:mru): Most recently used data is discarded
23
- Random (:random): A randomly selected piece of data is discarded
24
- Least Frequently Used (:lfu): Data with the lowest number of fetches is discarded
25
- Least Frequently Used with Dynamic Aging (:lfuda): An aging variable is
26
introduced to LFU to prefer discarding data that has been used a lot in the
27
history but less often recently.
31
(in-package :obj/cache)
34
(defun make-cache-table (&rest args)
35
"Make a value-weak hashtable. When value gets collected so does the key."
36
(apply 'make-hash-table :weakness :value args))
38
(defun get-cache (key cache)
39
"Get a value from a cache-table."
40
(let ((val (gethash key cache)))
41
(if val (values (sb-ext:weak-pointer-value val) t)
44
(defsetf get-cache setf-cache)
46
(defun setf-cache (key cache value)
47
"Set a value in a cache-table."
48
(let ((w (sb-ext:make-weak-pointer value)))
49
(sb-ext:finalize value (make-finalizer key cache))
50
(setf (gethash key cache) w)
53
(defun make-finalizer (key cache)
54
(declare (ignorable key cache))
55
(lambda () (remhash key cache)))
57
(defun remcache (key cache)
60
(defun map-cache (fn cache)
61
(with-hash-table-iterator (nextfn cache)
63
(multiple-value-bind (valid? key value) (nextfn)
65
(return-from map-cache))
66
(funcall fn key (sb-ext:weak-pointer-value value))))))
68
(defun dump-cache (cache)
69
(format t "Dumping cache: ~A~%" cache)
70
(map-cache #'(lambda (k v)
71
(format t ":k ~A :v ~A~%" k v))
75
(defclass cache-entry ()
76
((key :accessor key :initarg :key)
78
(pending :initarg :pending)
79
(rc :accessor entry-rc :initform 0)
80
(expiry :reader entry-expiry)))
82
(defclass indexed-cache-entry (cache-entry)
83
((index :accessor index)))
85
(defclass linked-cache-entry (cache-entry)
89
(defmethod shared-initialize ((entry linked-cache-entry) slot-names &rest initargs)
90
(declare (ignore initargs))
92
(when (or (eq slot-names t)
93
(find 'next slot-names))
94
(setf (slot-value entry 'next) entry))
95
(when (or (eq slot-names t)
96
(find 'prev slot-names))
97
(setf (slot-value entry 'prev) entry)))
99
(defmethod unlink ((entry linked-cache-entry))
100
(let ((n (slot-value entry 'next))
101
(p (slot-value entry 'prev)))
102
(when (and (eq n entry)
104
(error "Attempt to unlink an already unlinked entry ~s" entry))
105
(setf (slot-value n 'prev) p
106
(slot-value p 'next) n
107
(slot-value entry 'next) entry
108
(slot-value entry 'prev) entry)
111
(defun ensure-unlinked (entry)
112
(with-slots (next prev)
114
(unless (and (eq next entry)
116
(error "Attempt to link an already linked entry ~s" entry))))
118
(defmethod link-before ((entry linked-cache-entry) (ref linked-cache-entry))
119
(ensure-unlinked entry)
121
(p (slot-value ref 'prev)))
122
(setf (slot-value p 'next) entry
123
(slot-value n 'prev) entry
124
(slot-value entry 'next) n
125
(slot-value entry 'prev) p)
128
(defmethod link-after ((entry linked-cache-entry) (ref linked-cache-entry))
129
(ensure-unlinked entry)
130
(let ((n (slot-value ref 'next))
132
(setf (slot-value p 'next) entry
133
(slot-value n 'prev) entry
134
(slot-value entry 'next) n
135
(slot-value entry 'prev) p)
138
(defclass heap-cache-entry (indexed-cache-entry)
139
((weight :accessor entry-weight :initform 0 :initarg :weight)))
141
(defun heap-parent-idx (idx)
144
(defun heap-left-idx (idx)
147
(defun heap-right-idx (idx)
150
(defun heap-parent (heap idx)
152
(aref heap (heap-parent-idx idx))))
154
(defun heap-left (heap idx)
155
(let ((left (heap-left-idx idx)))
156
(and (< left (length heap))
159
(defun heap-right (heap idx)
160
(let ((right (heap-right-idx idx)))
161
(and (< right (length heap))
164
(defun heap-swap (heap i1 i2)
165
(let ((e1 (aref heap i1))
173
(defun sink-down (heap idx &optional prefer-to-sink)
174
(let ((me (aref heap idx))
175
(left (heap-left heap idx))
176
(right (heap-right heap idx)))
177
(unless (and (or (null left)
180
(and (not prefer-to-sink)
182
(entry-weight left))))
185
(entry-weight right))
186
(and (not prefer-to-sink)
188
(entry-weight right)))))
189
;; heavier than (one of) children, do sink
190
(let ((lightest (if (and right
191
(< (entry-weight right)
192
(entry-weight left)))
194
(heap-left-idx idx))))
195
(heap-swap heap idx lightest)
196
(sink-down heap lightest prefer-to-sink)))))
198
(defun bubble-up (heap idx)
199
(let ((me (aref heap idx))
200
(parent (heap-parent heap idx)))
201
(unless (or (null parent)
202
(>= (entry-weight me)
203
(entry-weight parent)))
204
;; lighter than parent, do bubble
205
(let ((p (heap-parent-idx idx)))
206
(heap-swap heap idx p)
207
(bubble-up heap p)))))
210
(deftype cache-policy () '(or keyword fixnum))
212
(defgeneric entry-added (policy queue entry)
213
(:method (policy (queue cons-queue) (entry cache-entry))
214
(change-class entry 'linked-cache-entry))
215
(:method (policy (queue cons-queue) (entry cache-entry))
216
(link-after entry (next queue)))
217
(:method :before (policy (queue vector-queue) (entry cache-entry))
218
(change-class entry 'indexed-cache-entry))
219
(:method (policy (queue vector-queue) (entry cache-entry))
220
(setf (index entry) (push-queue* entry queue)))
221
(:method ((policy (eql :lfu)) (queue vector-queue) (entry cache-entry))
222
(change-class entry 'heap-cache-entry)
223
(setf (entry-weight entry) 1
224
(index entry) (push-queue* entry queue))
225
(bubble-up (data queue) (index entry)))
226
(:method ((policy fixnum) queue (entry cache-entry))
227
(entry-added :lfu queue entry)
228
(incf (entry-weight entry) policy)
229
(sink-down (data queue) (index entry))))
231
(defgeneric access-entry (policy queue entry)
232
(:method (policy (queue cons-queue) (entry cache-entry)) t)
233
(:method (policy (queue vector-queue) (entry cache-entry)) t)
234
(:method ((policy (eql :lru)) queue (entry cache-entry))
236
(link-after entry (next queue))
238
(:method ((policy (eql :mru)) queue (entry cache-entry))
240
(link-after entry (next queue))
242
(:method ((policy (eql :lfu)) (queue vector-queue) (entry heap-cache-entry))
243
(incf (entry-weight entry))
244
(sink-down (data queue) (index entry) t)
247
(defgeneric entry-removed (policy queue entry)
248
(:method (policy (queue cons-queue) (entry cache-entry))
250
(:method (policy (queue vector-queue) (entry cache-entry))
251
(let ((i (index entry)))
252
(setf (index entry) nil
253
(aref (data queue) i) nil)
255
(loop for i below (queue-count queue)
256
for e = (aref (data queue) i)
261
(aref (data queue) w) e
263
(setf (fill-pointer (data queue)) w))))
264
(:method ((policy (eql :lfu)) (queue vector-queue) (entry heap-cache-entry))
265
(let ((i (index entry)))
266
(setf (index entry) nil)
267
(unless (= i (1- (queue-count* queue)))
268
(setf (aref (data queue) i) (pop-queue* queue)
269
(index (aref (data queue) i)) i)
270
(sink-down (data queue) i)))))
272
(defgeneric evict-entry (policy queue)
273
(:method ((policy (eql :fifo)) queue)
274
(let* ((next (next queue))
276
(unless (eq last next)
279
(:method ((policy (eql :lifo)) queue)
280
(let* ((next (next queue))
282
(unless (eq first next)
285
(:method ((policy (eql :random)) queue)
286
(unless (queue-full-p queue)
287
(let ((e (loop for i = (random (raw-queue-capacity (queue queue)))
288
for e = (aref (data queue) i)
290
finally (return e))))
291
(entry-removed policy queue e)
293
(:method ((policy (eql :lfu)) (queue vector-queue))
294
(unless (queue-empty-p* queue)
295
(let ((light (aref (data queue) 0))
296
(heavy (pop-queue* queue)))
297
(unless (queue-empty-p* queue)
298
(setf (aref (data queue) 0) heavy
300
(sink-down (data queue) 0 t))
302
(:method ((policy fixnum) (queue vector-queue))
303
(when-let ((target (evict-entry :lfu queue)))
304
;; CACLE updates the policy object here, we return the weight
305
;; (entry-weight target)
310
((policy :initarg :policy :accessor cache-policy)
311
(kernel :initarg :kernel :accessor kernel)
312
(cleanup :initarg :cleanup :accessor cache-cleanup)
313
(table :initarg :table :accessor table)
314
(queue :initform (make-queue) :initarg :queue :accessor queue)))
316
(defmethod initialize-instance ((cache cache) &key policy kernel (test 'eql) capacity element-type
319
(unless kernel (required-argument :kernel))
320
(setf (slot-value cache 'table) (make-hash-table :test test)
321
(slot-value cache 'queue) (make-queue :capacity capacity :element-type element-type))
322
(cond ((and policy (not (typep (queue cache) 'vector-queue)))
323
(error "Policy defined, but queue is possibly infinite"))
325
(unless (not (typep (queue cache) 'vector-queue))
326
(error "Queue size is defined, but policy missing")))
327
((typep policy '(or keyword fixnum null))
328
(setf (slot-value cache 'policy) policy))
330
(error "Invalid policy ~s" policy))))
332
(defun make-cache (capacity provider &key (test 'eql) (policy :fifo) cleanup (element-type 'cache-entry))
333
"Create a new cache with the specified capacity, kernel function, and options."
334
(make-instance 'cache
340
:element-type element-type))
342
(defvar *cleanup-list*)
343
(defmacro with-collected-cleanups ((cache) &body body)
345
`(let* ((,fn (with-queue-lock (queue ,cache)
346
(slot-value ,cache 'cleanup)))
347
(*cleanup-list* (null ,fn)))
351
(dolist (,i *cleanup-list*)
352
(funcall ,fn ,i)))))))
354
(defun prepare-cleanup (entry hash)
355
(cond ((eq *cleanup-list* t)
356
(remhash (key entry) hash))
357
((zerop (entry-rc entry))
358
(remhash (key entry) hash)
359
(push (slot-value entry 'data) *cleanup-list*))
360
((< (entry-rc entry) 0)
361
(error "Internal error: double prepare-cleanup for ~s" entry))
363
(setf (entry-rc entry) (- (entry-rc entry))))))
365
;; REVIEW 2025-07-04:
366
(defun ensure-cache-size (cache)
367
(with-slots (policy table) cache
368
(loop while (not (queue-full-p* (queue cache)))
369
for old = (evict-entry policy (queue cache))
372
;; (decf size (slot-value old 'size))
373
(prepare-cleanup old table)))))
375
(defun cache-count (cache)
376
"Returns the current count of items in the cache."
377
(with-queue-lock (queue cache)
378
(hash-table-count (slot-value cache 'table))))
380
(defmethod get-val ((cache cache) key &key shallow force)
381
"Return the value associated with KEY in CACHE.
383
If the item is not currently in the cache, or has expired, it is fetched from
384
the provider and stored in the cache.
386
If FORCE is specified, a new value is fetched from the provider even if
387
it already exists in the cache.
389
If a cleanup function is defined for the cache, remember to call cache-release
390
with the second value returned by GET-VAL."
391
(with-slots (table policy kernel) cache
392
(let ((lock (lock (queue cache))))
393
(with-collected-cleanups (cache)
394
(multiple-value-bind (hit data entry)
397
(let ((entry (gethash key table)))
399
(prepare-cleanup entry table)
400
;; (decf (cache-size cache) (slot-value entry 'size))
402
(entry-removed policy (queue cache) entry)))))
404
(let ((entry (make-instance 'cache-entry :key key :pending (make-waitqueue))))
405
(setf (gethash key table) entry)
406
(values nil entry))))
408
(let ((entry (gethash key table)))
409
(cond ((and (null entry)
411
;; cache miss, and no waiting
412
(return (values t nil nil)))
415
;; cache miss - initialize fetch from source
418
((and (slot-boundp entry 'pending)
420
;; cache hit - but data not yet ready, and no waiting
421
(return (values t nil nil)))
423
((slot-boundp entry 'pending)
424
;; cache hit - but data not yet ready
425
(let ((pending (slot-value entry 'pending)))
426
(condition-wait pending lock)
427
;; note: the pending slot is no longer bound after the wait
428
(condition-notify pending)
429
;; data now available
430
(when (eq (gethash key table) entry)
431
;; ... and not immediately cleaned up
432
(if (cache-cleanup cache)
434
(if (>= (entry-rc entry) 0)
435
(incf (entry-rc entry))
436
(decf (entry-rc entry)))
437
(return (values t (slot-value entry 'data) entry)))
438
(return (values t (slot-value entry 'data)))))))
441
(or (and (slot-boundp entry 'expiry)
442
(<= (slot-value entry 'expiry)
443
(get-universal-time)))
444
(and (>= (entry-rc entry) 0)
445
(not (access-entry policy (queue cache) entry)))))
446
;; cached data has expired or been invalidated
448
(prepare-cleanup entry table)
449
(decf (slot-value cache 'size) (slot-value entry 'size))
450
(entry-removed policy (queue cache) entry)
452
(return (values t nil nil)) ; no waiting
455
((cache-cleanup cache)
456
(if (>= (entry-rc entry) 0)
457
(incf (entry-rc entry))
458
(decf (entry-rc entry)))
459
(return (values t (slot-value entry 'data) entry)))
462
(return (values t (slot-value entry 'data) nil))))))))
465
(multiple-value-bind (content size)
466
(handler-case (funcall kernel key)
470
(condition-notify (slot-value data 'pending))
471
(slot-makunbound data 'pending))
473
(with-collected-cleanups (cache)
474
(unless (typep size 'real)
475
(setf size (if content 1 0))
476
(warn "Cache provider did not return a proper size for the data - assuming size of ~d" size))
478
(setf (slot-value data 'data) content)
479
;; (slot-value data 'size) size)
480
;; (with-slots (lifetime) cache
482
;; (setf (slot-value data 'expiry)
483
;; (+ (get-universal-time) lifetime))))
484
(condition-notify (slot-value data 'pending))
485
(slot-makunbound data 'pending)
486
;; (incf (slot-value cache 'size) size)
488
(ensure-cache-size cache)
489
(entry-added policy (queue cache) data))
490
(if (cache-cleanup cache)
492
(incf (entry-rc data))
493
(values content data))
494
(values content nil)))))))))))
496
(defmethod cache-release ((cache cache) entry)
497
"Releases a reference for an item fetched earlier.
499
An item fetched from the cache with cache-fetch will not be cleaned up before
502
(with-slots (table cleanup) cache
504
(with-queue-lock (queue cache)
505
(let ((busy (entry-rc entry)))
507
(error "Double release for item with the key ~a" (key entry)))
509
(decf (entry-rc entry))
512
(when (zerop (incf (entry-rc entry)))
513
(when (eq (gethash (key entry) table) entry)
514
(remhash (key entry) table))
515
(slot-value entry 'data))))))))
516
(when (and cleanup to-clean)
517
(funcall cleanup to-clean)))))
520
(defmacro with-cache (var (cache key &key shallow) &body body)
521
"Combines a cache-fetch and cache-release in a form."
522
(with-gensyms (c-var tag)
523
`(let ((,c-var ,cache))
524
(multiple-value-bind (,var ,tag)
525
(cache-fetch ,c-var ,key ,@(and shallow '(:shallow t)))
528
(cache-release ,c-var ,tag))))))
530
(defmethod cache-remove ((cache cache) key)
531
"Remove the item with the specified key from the cache."
532
(with-slots (table policy) cache
533
(with-collected-cleanups (cache)
534
(with-queue-lock (queue cache)
535
(let ((entry (gethash key table)))
537
(prepare-cleanup entry table)
538
;; (decf size (slot-value entry 'size))
540
(entry-removed policy (queue cache) entry))
543
(defmethod cache-flush ((cache cache))
544
"Flush the cache, removing all items currently stored in it. If a cleanup
545
function is defined for the cache, it is called for every item."
546
(with-slots (table policy cleanup) cache
547
(with-collected-cleanups (cache)
548
(with-queue-lock (queue cache)
549
(maphash #'(lambda (k v)
551
(prepare-cleanup v table)
552
(entry-removed policy (queue cache) v))