Merge pull request #783 from urbit/ford-turbo-clock

Improvements to +clock / +capped-queue
This commit is contained in:
Elliot Glaysher 2018-08-28 10:11:03 -07:00 committed by GitHub
commit e3e5ae6d30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 533 additions and 211 deletions

View File

@ -126,6 +126,7 @@
::
|= pit=vase
::
=, contain
=, ford
:: ford internal data structures
::
@ -184,216 +185,6 @@
care-paths=(set [care=care:clay =path])
== == == ==
--
::
|%
:: +clock: polymorphic cache type for use with the clock replacement algorithm
::
:: The +by-clock core wraps interface arms for manipulating a mapping from
:: :key-type to :val-type. Detailed docs for this type can be found there.
::
++ clock
|* $: :: key-type: mold of keys
::
key-type=mold
:: val-type: mold of values
::
val-type=mold
==
$: lookup=(map key-type [val=val-type fresh=@ud])
queue=(qeu key-type)
size=@ud
max-size=_2.048
depth=_1
==
:: +capped-queue: a +qeu with a maximum number of entries
::
++ capped-queue
|* item-type=mold
$: queue=(qeu item-type)
size=@ud
max-size=_64
==
--
|%
:: +by-clock: interface core for a cache using the clock replacement algorithm
::
:: Presents an interface for a mapping, but somewhat specialized, and with
:: stateful accessors. The clock's :depth parameter is used as the maximum
:: freshness that an entry can have. The standard clock algorithm has a depth
:: of 1, meaning that a single sweep of the arm will delete the entry. For
:: more scan resistance, :depth can be set to a higher number.
::
:: Internally, :clock maintains a :lookup of type
:: `(map key-type [val=val-type fresh=@ud])`, where :depth.clock is the
:: maximum value of :fresh. Looking up a key increments its freshness, and a
:: sweep of the clock arm decrements its freshness.
::
:: The clock arm is stored as :queue, which is a `(qeu key-type)`. The head
:: of the queue represents the position of the clock arm. New entries are
:: inserted at the tail of the queue. When the clock arm sweeps, it
:: pops the head off the queue. If the :fresh of the head's entry in :lookup
:: is 0, remove the entry from the mapping and replace it with the new entry.
:: Otherwise, decrement the entry's freshness, put it back at the tail of
:: the queue, and pop the next head off the queue and try again.
::
:: Cache entries must be immutable: a key cannot be overwritten with a new
:: value. This property is enforced for entries currently stored in the
:: cache, but it is not enforced for previously deleted entries, since we
:: no longer remember what that key's value was supposed to be.
::
++ by-clock
|* [key-type=mold val-type=mold]
|_ clock=(clock key-type val-type)
:: +get: looks up a key, marking it as fresh
::
++ get
|= key=key-type
^- [(unit val-type) _clock]
::
=+ maybe-got=(~(get by lookup.clock) key)
?~ maybe-got
[~ clock]
::
=. clock (freshen key)
::
[`val.u.maybe-got clock]
:: +put: add a new cache entry, possibly removing an old one
::
++ put
|= [key=key-type val=val-type]
^+ clock
:: no overwrite allowed, but allow duplicate puts
::
?^ existing=(~(get by lookup.clock) key)
:: val must not change
::
?> =(val val.u.existing)
::
(freshen key)
::
=? clock =(max-size.clock +(size.clock))
evict
::
%_ clock
size +(size.clock)
lookup (~(put by lookup.clock) key [val 1])
queue (~(put to queue.clock) key)
==
:: +freshen: increment the protection level on an entry
::
++ freshen
|= key=key-type
^+ clock
%_ clock
lookup
%+ ~(jab by lookup.clock) key
|= entry=[val=val-type fresh=@ud]
entry(fresh (max +(fresh.entry) depth.clock))
==
:: +resize: changes the maximum size, removing entries if needed
::
++ resize
|= new-max=@ud
^+ clock
::
=. max-size.clock new-max
::
?: (gte new-max size.clock)
clock
::
(trim (sub size.clock new-max))
:: +evict: remove an entry from the cache
::
++ evict
^+ clock
::
=. size.clock (dec size.clock)
::
|-
^+ clock
::
=^ old-key queue.clock ~(get to queue.clock)
=/ old-entry (~(got by lookup.clock) old-key)
::
?: =(0 fresh.old-entry)
clock(lookup (~(del by lookup.clock) old-key))
::
%_ $
lookup.clock
(~(put by lookup.clock) old-key old-entry(fresh (dec fresh.old-entry)))
::
queue.clock
(~(put to queue.clock) old-key)
==
:: +trim: remove :count entries from the cache
::
++ trim
|= count=@ud
^+ clock
?: =(0 count)
clock
$(count (dec count), clock evict)
:: +purge: removes all cache entries
::
++ purge
^+ clock
%_ clock
lookup ~
queue ~
size 0
==
--
:: +to-capped-queue: interface door for +capped-queue
::
++ to-capped-queue
|* item-type=mold
|_ queue=(capped-queue item-type)
:: +put: enqueue :item, possibly popping and producing an old item
::
++ put
|= item=item-type
^- [(unit item-type) _queue]
:: are we already at max capacity?
::
?. =(size.queue max-size.queue)
:: we're below max capacity, so push and increment size
::
=. queue.queue (~(put to queue.queue) item)
=. size.queue +(size.queue)
::
[~ queue]
:: we're at max capacity, so pop before pushing; size is unchanged
::
=^ oldest queue.queue ~(get to queue.queue)
=. queue.queue (~(put to queue.queue) item)
::
[`oldest queue]
:: +get: pop an item off the queue, adjusting size
::
++ get
^- [item-type _queue]
::
=. size.queue (dec size.queue)
=^ oldest queue.queue ~(get to queue.queue)
::
[oldest queue]
:: change the :max-size of the queue, popping items if necessary
::
++ resize
=| pops=(list item-type)
|= new-max=@ud
^+ [pops queue]
:: we're not overfull, so no need to pop off more items
::
?: (gte new-max size.queue)
[(flop pops) queue(max-size new-max)]
:: we're above capacity; pop an item off and recurse
::
=^ oldest queue get
::
$(pops [oldest pops])
--
--
|%
:: +axle: overall ford state
::
@ -6084,7 +5875,9 @@
?: ?=(%pin -.schematic.build)
~
::
=/ subs ~(tap in ~(key by subs:(~(got by builds.state) build)))
=/ subs
~| [%collect-live-resource (build-to-tape build)]
~(tap in ~(key by subs:(~(got by builds.state) build)))
=| resources=(jug disc resource)
|-
?~ subs

View File

@ -40,6 +40,34 @@
:: miscellaneous systems types
::+|
++ ares (unit {p/term q/(list tank)}) :: possible error
:: +capped-queue: a +qeu with a maximum number of entries
::
++ capped-queue
|* item-type=mold
$: queue=(qeu item-type)
size=@ud
max-size=_64
==
:: +clock: polymorphic cache type for use with the clock replacement algorithm
::
:: The +by-clock core wraps interface arms for manipulating a mapping from
:: :key-type to :val-type. Detailed docs for this type can be found there.
::
++ clock
|* $: :: key-type: mold of keys
::
key-type=mold
:: val-type: mold of values
::
val-type=mold
==
$: lookup=(map key-type [val=val-type fresh=@ud])
queue=(qeu key-type)
size=@ud
max-size=_2.048
depth=_1
==
::
++ coop (unit ares) :: possible error
++ json :: normal json value
$@ ~ :: null
@ -5675,6 +5703,197 @@
^- @tas
?~(une %no (mill u.une))
--
::
::::
::
++ contain ^?
|%
:: +by-clock: interface core for a cache using the clock replacement algorithm
::
:: Presents an interface for a mapping, but somewhat specialized, and with
:: stateful accessors. The clock's :depth parameter is used as the maximum
:: freshness that an entry can have. The standard clock algorithm has a depth
:: of 1, meaning that a single sweep of the arm will delete the entry. For
:: more scan resistance, :depth can be set to a higher number.
::
:: Internally, :clock maintains a :lookup of type
:: `(map key-type [val=val-type fresh=@ud])`, where :depth.clock is the
:: maximum value of :fresh. Looking up a key increments its freshness, and a
:: sweep of the clock arm decrements its freshness.
::
:: The clock arm is stored as :queue, which is a `(qeu key-type)`. The head
:: of the queue represents the position of the clock arm. New entries are
:: inserted at the tail of the queue. When the clock arm sweeps, it
:: pops the head off the queue. If the :fresh of the head's entry in :lookup
:: is 0, remove the entry from the mapping and replace it with the new entry.
:: Otherwise, decrement the entry's freshness, put it back at the tail of
:: the queue, and pop the next head off the queue and try again.
::
:: Cache entries must be immutable: a key cannot be overwritten with a new
:: value. This property is enforced for entries currently stored in the
:: cache, but it is not enforced for previously deleted entries, since we
:: no longer remember what that key's value was supposed to be.
::
++ by-clock
|* [key-type=mold val-type=mold]
|_ clock=(clock key-type val-type)
:: +get: looks up a key, marking it as fresh
::
++ get
|= key=key-type
^- [(unit val-type) _clock]
::
=+ maybe-got=(~(get by lookup.clock) key)
?~ maybe-got
[~ clock]
::
=. clock (freshen key)
::
[`val.u.maybe-got clock]
:: +put: add a new cache entry, possibly removing an old one
::
++ put
|= [key=key-type val=val-type]
^+ clock
:: do nothing if our size is 0 so we don't decrement-underflow
::
?: =(0 max-size.clock)
clock
:: no overwrite allowed, but allow duplicate puts
::
?^ existing=(~(get by lookup.clock) key)
:: val must not change
::
?> =(val val.u.existing)
::
(freshen key)
::
=? clock =(max-size.clock size.clock)
evict
::
%_ clock
size +(size.clock)
lookup (~(put by lookup.clock) key [val 1])
queue (~(put to queue.clock) key)
==
:: +freshen: increment the protection level on an entry
::
++ freshen
|= key=key-type
^+ clock
%_ clock
lookup
%+ ~(jab by lookup.clock) key
|= entry=[val=val-type fresh=@ud]
entry(fresh (min +(fresh.entry) depth.clock))
==
:: +resize: changes the maximum size, removing entries if needed
::
++ resize
|= new-max=@ud
^+ clock
::
=. max-size.clock new-max
::
?: (gte new-max size.clock)
clock
::
(trim (sub size.clock new-max))
:: +evict: remove an entry from the cache
::
++ evict
^+ clock
::
=. size.clock (dec size.clock)
::
|-
^+ clock
::
=^ old-key queue.clock ~(get to queue.clock)
=/ old-entry (~(got by lookup.clock) old-key)
::
?: =(0 fresh.old-entry)
clock(lookup (~(del by lookup.clock) old-key))
::
%_ $
lookup.clock
(~(put by lookup.clock) old-key old-entry(fresh (dec fresh.old-entry)))
::
queue.clock
(~(put to queue.clock) old-key)
==
:: +trim: remove :count entries from the cache
::
++ trim
|= count=@ud
^+ clock
?: =(0 count)
clock
$(count (dec count), clock evict)
:: +purge: removes all cache entries
::
++ purge
^+ clock
%_ clock
lookup ~
queue ~
size 0
==
--
:: +to-capped-queue: interface door for +capped-queue
::
:: Provides a queue of a limited size where pushing additional items will
:: force pop the items at the front of the queue.
::
++ to-capped-queue
|* item-type=mold
|_ queue=(capped-queue item-type)
:: +put: enqueue :item, possibly popping and producing an old item
::
++ put
|= item=item-type
^- [(unit item-type) _queue]
:: are we already at max capacity?
::
?. =(size.queue max-size.queue)
:: we're below max capacity, so push and increment size
::
=. queue.queue (~(put to queue.queue) item)
=. size.queue +(size.queue)
::
[~ queue]
:: we're at max capacity, so pop before pushing; size is unchanged
::
=^ oldest queue.queue ~(get to queue.queue)
=. queue.queue (~(put to queue.queue) item)
::
[`oldest queue]
:: +get: pop an item off the queue, adjusting size
::
++ get
^- [item-type _queue]
::
=. size.queue (dec size.queue)
=^ oldest queue.queue ~(get to queue.queue)
::
[oldest queue]
:: change the :max-size of the queue, popping items if necessary
::
++ resize
=| pops=(list item-type)
|= new-max=@ud
^+ [pops queue]
:: we're not overfull, so no need to pop off more items
::
?: (gte new-max size.queue)
[(flop pops) queue(max-size new-max)]
:: we're above capacity; pop an item off and recurse
::
=^ oldest queue get
::
$(pops [oldest pops])
--
--
:: ::
:::: ++userlib :: (2u) non-vane utils
:: ::::

View File

@ -0,0 +1,128 @@
/+ tester
::
=, contain
::
|_ _tester:tester
++ test-basic-capped-queue
::
=| q=(capped-queue @u)
=. max-size.q 3
:: specialize type
::
=+ to-capped-queue=(to-capped-queue @u)
:: push a single element
::
=^ maybe1 q (~(put to-capped-queue q) 5)
::
=/ results1
%- expect-eq !>
:- ~
maybe1
=/ results2
%- expect-eq !>
:- 1
size.q
=/ results3
%- expect-eq !>
:- [~ 5]
~(top to queue.q)
:: remove the single element
::
=^ maybe2 q ~(get to-capped-queue q)
::
=/ results4
%- expect-eq !>
:- 5
maybe2
=/ results5
%- expect-eq !>
:- 0
size.q
::
;: weld
results1
results2
results3
results4
results5
==
::
++ test-put-returns-evicted-value
::
=| q=(capped-queue @u)
=. max-size.q 2
:: specialize type
::
=+ to-capped-queue=(to-capped-queue @u)
:: push enough values to evict one
::
=^ maybe1 q (~(put to-capped-queue q) 5)
=/ results1
%- expect-eq !>
:- ~
maybe1
=/ results2
%- expect-eq !>
:- 1
size.q
::
=^ maybe2 q (~(put to-capped-queue q) 6)
=/ results3
%- expect-eq !>
:- ~
maybe2
=/ results4
%- expect-eq !>
:- 2
size.q
::
=^ maybe3 q (~(put to-capped-queue q) 7)
=/ results5
%- expect-eq !>
:- [~ 5]
maybe3
=/ results6
%- expect-eq !>
:- 2
size.q
::
;: weld
results1
results2
results3
results4
results5
results6
==
::
++ test-resize-evicts-on-shrink
::
=| q=(capped-queue @u)
=. max-size.q 5
:: specialize type
::
=+ to-capped-queue=(to-capped-queue @u)
::
=^ maybe1 q (~(put to-capped-queue q) 1)
=^ maybe2 q (~(put to-capped-queue q) 2)
=^ maybe3 q (~(put to-capped-queue q) 3)
=^ maybe4 q (~(put to-capped-queue q) 4)
=^ maybe5 q (~(put to-capped-queue q) 5)
:: resize the size to 3; this should pop two items
::
=^ pops q (~(resize to-capped-queue q) 3)
::
=/ results1
%- expect-eq !>
:- [1 2 ~]
pops
=/ results2
%- expect-eq !>
:- 3
size.q
::
;: weld
results1
results2
==
--

View File

@ -0,0 +1,182 @@
/+ tester
::
=, contain
::
|_ _tester:tester
++ test-basic-clock
::
=| c=(clock @u tape)
:: make max-size reasonable for testing
::
=. max-size.c 3
:: specialize type
::
=+ by-clock=(by-clock @u tape)
:: ensure we get a single key we put in
::
=. c (~(put by-clock c) 1 "one")
=^ maybe1 c (~(get by-clock c) 1)
=/ results1
%- expect-eq !>
:- [~ "one"]
maybe1
::
=/ results2
%- expect-eq !>
:- 1
size.c
:: push that key out of the cache
::
=. c (~(put by-clock c) 2 "two")
=. c (~(put by-clock c) 3 "three")
=. c (~(put by-clock c) 4 "four")
::
=/ results3
%- expect-eq !>
:- 3
size.c
::
=^ maybe2 c (~(get by-clock c) 1)
=/ results4
%- expect-eq !>
:- ~
maybe2
::
;: weld
results1
results2
results3
results4
==
::
++ test-clock-purge
::
=| c=(clock @u tape)
:: make max-size reasonable for testing
::
=. max-size.c 3
:: specialize type
::
=+ by-clock=(by-clock @u tape)
:: fill the clock
::
=. c (~(put by-clock c) 1 "one")
=. c (~(put by-clock c) 2 "two")
=. c (~(put by-clock c) 3 "three")
:: purge the entire clock
::
=. c ~(purge by-clock c)
::
;: weld
%- expect-eq !>
:- 0
size.c
::
%- expect-eq !>
:- 3
max-size.c
::
%- expect-eq !>
:- ~
lookup.c
::
%- expect-eq !>
:- ~
queue.c
==
::
++ test-clock-trim
::
=| c=(clock @u tape)
:: make max-size reasonable for testing
::
=. max-size.c 3
:: specialize type
::
=+ by-clock=(by-clock @u tape)
:: fill the clock
::
=. c (~(put by-clock c) 1 "one")
=. c (~(put by-clock c) 2 "two")
=. c (~(put by-clock c) 3 "three")
:: trim 2/3 of the clock
::
=. c (~(trim by-clock c) 2)
::
;: weld
%- expect-eq !>
:- 1
size.c
::
=^ results1 c (~(get by-clock c) 3)
%- expect-eq !>
:- [~ "three"]
results1
::
%- expect-eq !>
:- 1
~(wyt by lookup.c)
==
::
++ test-clock-resized-to-zero
::
=| c=(clock @u tape)
:: make max-size reasonable for testing
::
=. max-size.c 3
:: specialize type
::
=+ by-clock=(by-clock @u tape)
:: fill the clock
::
=. c (~(put by-clock c) 1 "one")
=. c (~(put by-clock c) 2 "two")
=. c (~(put by-clock c) 3 "three")
:: resize the clock so it has zero elements
::
=. c (~(resize by-clock c) 0)
::
=/ results1
%- expect-eq !>
:- 0
size.c
::
=/ results2
%- expect-eq !>
:- ~
lookup.c
::
=/ results3
%- expect-eq !>
:- ~
queue.c
::
=/ results4
%- expect-eq !>
:- 0
max-size.c
:: trying to get an element just returns ~
::
=^ maybe1 c (~(get by-clock c) 3)
=/ results5
%- expect-eq !>
:- ~
maybe1
:: trying to put an element in doesn't mutate the clock
::
=. c (~(put by-clock c) 4 "four")
::
=/ results6
%- expect-eq !>
:- 0
size.c
::
;: weld
results1
results2
results3
results4
results5
results6
==
--