diff --git a/Project.toml b/Project.toml index ad3cd52..a453257 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "LRUCache" uuid = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637" -version = "1.5.1" +version = "1.6.0" [compat] julia = "1" diff --git a/README.md b/README.md index 8265f31..365fe77 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,14 @@ storing this value in `lru`. Also comes in the following form: #### get(default::Callable, lru::LRU, key) +#### cache_info(lru::LRU) + +Returns an object holding a snapshot of information about hits, misses, current size, and total size in its properties. + +The caching functions `get` and `get!` each contribute a cache hit or miss on every function call (depending on whether or not the key was found). `empty!` resets the counts of hits and misses to 0. + +The other functions, e.g. `getindex` `iterate`, `haskey`, `setindex!`, `delete!`, and `pop!` do not contribute cache hits or misses, regardless of whether or not a key is retrieved. This is because it is not possible to use these functions for caching. + ## Example Commonly, you may have some long running function that sometimes gets called with the same @@ -130,3 +138,5 @@ function cached_foo(a::Float64, b::Float64) end end ``` + +If we want to see how our cache is performing, we can call `cache_info` to see the number of cache hits and misses. diff --git a/src/LRUCache.jl b/src/LRUCache.jl index 5bb9475..a9927b4 100644 --- a/src/LRUCache.jl +++ b/src/LRUCache.jl @@ -1,7 +1,7 @@ module LRUCache include("cyclicorderedset.jl") -export LRU +export LRU, cache_info using Base.Threads using Base: Callable @@ -14,6 +14,8 @@ mutable struct LRU{K,V} <: AbstractDict{K,V} keyset::CyclicOrderedSet{K} currentsize::Int maxsize::Int + hits::Int + misses::Int lock::SpinLock by::Any finalizer::Any @@ -21,7 +23,51 @@ mutable struct LRU{K,V} <: AbstractDict{K,V} function LRU{K, V}(; maxsize::Int, by = _constone, finalizer = nothing) where {K, V} dict = Dict{K, V}() keyset = CyclicOrderedSet{K}() - new{K, V}(dict, keyset , 0, maxsize, SpinLock(), by, finalizer) + new{K, V}(dict, keyset , 0, maxsize, 0, 0, SpinLock(), by, finalizer) + end +end + +Base.@kwdef struct CacheInfo + hits::Int + misses::Int + currentsize::Int + maxsize::Int +end + +function Base.show(io::IO, c::CacheInfo) + return print(io, "CacheInfo(; hits=$(c.hits), misses=$(c.misses), currentsize=$(c.currentsize), maxsize=$(c.maxsize))") +end + +""" + cache_info(lru::LRU) -> CacheInfo + +Returns a `CacheInfo` object holding a snapshot of information about the cache hits, misses, current size, and maximum size, current as of when the function was called. To access the values programmatically, use property access, e.g. `info.hits`. + +Note that only `get!` and `get` contribute to hits and misses, and `empty!` resets the counts of hits and misses to 0. + +## Example + +```jldoctest +lru = LRU{Int, Float64}(maxsize=10) + +get!(lru, 1, 1.0) # miss + +get!(lru, 1, 1.0) # hit + +get(lru, 2, 2) # miss + +get(lru, 2, 2) # miss + +info = cache_info(lru) + +# output + +CacheInfo(; hits=1, misses=3, currentsize=1, maxsize=10) +``` +""" +function cache_info(lru::LRU) + lock(lru.lock) do + return CacheInfo(; hits=lru.hits, misses=lru.misses, currentsize=lru.currentsize, maxsize=lru.maxsize) end end @@ -71,8 +117,10 @@ function Base.get(lru::LRU, key, default) lock(lru.lock) do if _unsafe_haskey(lru, key) v = _unsafe_getindex(lru, key) + lru.hits += 1 return v else + lru.misses += 1 return default end end @@ -81,8 +129,10 @@ function Base.get(default::Callable, lru::LRU, key) lock(lru.lock) try if _unsafe_haskey(lru, key) + lru.hits += 1 return _unsafe_getindex(lru, key) end + lru.misses += 1 finally unlock(lru.lock) end @@ -92,12 +142,14 @@ function Base.get!(lru::LRU{K, V}, key, default) where {K, V} evictions = Tuple{K, V}[] v = lock(lru.lock) do if _unsafe_haskey(lru, key) + lru.hits += 1 v = _unsafe_getindex(lru, key) return v end v = default _unsafe_addindex!(lru, v, key) _unsafe_resize!(lru, evictions) + lru.misses += 1 return v end _finalize_evictions!(lru.finalizer, evictions) @@ -108,6 +160,7 @@ function Base.get!(default::Callable, lru::LRU{K, V}, key) where {K, V} lock(lru.lock) try if _unsafe_haskey(lru, key) + lru.hits += 1 return _unsafe_getindex(lru, key) end finally @@ -117,11 +170,13 @@ function Base.get!(default::Callable, lru::LRU{K, V}, key) where {K, V} lock(lru.lock) try if _unsafe_haskey(lru, key) + lru.hits += 1 # should we test that this yields the same result as default() v = _unsafe_getindex(lru, key) else _unsafe_addindex!(lru, v, key) _unsafe_resize!(lru, evictions) + lru.misses += 1 end finally unlock(lru.lock) @@ -247,6 +302,8 @@ function Base.empty!(lru::LRU{K, V}) where {K, V} _unsafe_resize!(lru, evictions, 0) lru.maxsize = maxsize # restore `maxsize` end + lru.hits = 0 + lru.misses = 0 end _finalize_evictions!(lru.finalizer, evictions) return lru diff --git a/test/runtests.jl b/test/runtests.jl index 4cb742d..b30b5eb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,5 @@ using LRUCache +using LRUCache: CacheInfo using Test using Random using Base.Threads @@ -253,4 +254,46 @@ end @test lru[1] == 1:9 end +@testset "cache_info" begin + lru = LRU{Int, Float64}(; maxsize=10) + + get!(lru, 1, 1.0) # miss + @test cache_info(lru) == CacheInfo(; hits=0, misses=1, currentsize=1, maxsize=10) + + get!(lru, 1, 1.0) # hit + @test cache_info(lru) == CacheInfo(; hits=1, misses=1, currentsize=1, maxsize=10) + + get(lru, 1, 1.0) # hit + @test cache_info(lru) == CacheInfo(; hits=2, misses=1, currentsize=1, maxsize=10) + + get(lru, 2, 1.0) # miss + info = CacheInfo(; hits=2, misses=2, currentsize=1, maxsize=10) + @test cache_info(lru) == info + @test sprint(show, info) == "CacheInfo(; hits=2, misses=2, currentsize=1, maxsize=10)" + + # These don't change the hits and misses + @test haskey(lru, 1) + @test cache_info(lru) == info + @test lru[1] == 1.0 + @test cache_info(lru) == info + @test !haskey(lru, 2) + @test cache_info(lru) == info + + # This affects `currentsize` but not hits or misses + delete!(lru, 1) + @test cache_info(lru) == CacheInfo(; hits=2, misses=2, currentsize=0, maxsize=10) + + # Likewise setindex! does not affect hits or misses, just `currentsize` + lru[1] = 1.0 + @test cache_info(lru) == info + + # Only affects size + pop!(lru, 1) + @test cache_info(lru) == CacheInfo(; hits=2, misses=2, currentsize=0, maxsize=10) + + # Resets counts + empty!(lru) + @test cache_info(lru) == CacheInfo(; hits=0, misses=0, currentsize=0, maxsize=10) +end + include("originaltests.jl")