Skip to content

Commit 7e0934a

Browse files
authored
Merge pull request #20 from JuliaImageRecon/nh/resize_cache
Allow cache resizing
2 parents 9a574ae + 23a877c commit 7e0934a

File tree

4 files changed

+232
-4
lines changed

4 files changed

+232
-4
lines changed

docs/src/API/api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ AbstractImageReconstruction.savePlan
3535
AbstractImageReconstruction.loadPlan
3636
AbstractImageReconstruction.loadListener!
3737
AbstractImageReconstruction.parent(::RecoPlan)
38-
AbstractImageReconstruction.parent!(::RecoPlan, ::RecoPlan)
38+
AbstractImageReconstruction.parent!(::RecoPlan, ::AbstractRecoPlan)
3939
AbstractImageReconstruction.parentproperty
4040
AbstractImageReconstruction.parentproperties
4141
```

docs/src/literate/howto/caching.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,7 @@ reconstruct(algo, sinograms);
6868

6969
# Caches support serialization like other `RecoPlans`:
7070
clear!(plan)
71-
toTOML(stdout, plan)
71+
toTOML(stdout, plan)
72+
73+
# Caches can also be resized. You can either set the maxsize property of the RecoPlan or use `resize!` on the `ProcessResultCache`. Resizing a cache affects all algorithms build from the same plan.
74+
setAll!(plan, maxsize, 0)

src/RecoPlans/Cache.jl

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,20 @@ The cache is transparent for properties of the underlying parameter. Cache can b
77
"""
88
mutable struct ProcessResultCache{P} <: AbstractUtilityReconstructionParameters{P}
99
param::P
10-
const maxsize::Int64
11-
cache::LRU{UInt64, Any}
10+
maxsize::Int64
11+
const cache::LRU{UInt64, Any}
1212
function ProcessResultCache(; param::Union{P, AbstractUtilityReconstructionParameters{P}}, maxsize::Int64 = 1, cache::LRU{UInt64, Any} = LRU{UInt64, Any}(maxsize = maxsize)) where P
13+
if maxsize != cache.maxsize
14+
@warn "Incosistent cache size detected. Found maxsize $maxsize and cache size $(cache.maxsize). This can happen when a cache is resized. Cache will use $(cache.maxsize)"
15+
end
16+
return ProcessResultCache(cache; param)
17+
end
18+
function ProcessResultCache(maxsize::Int64; param::Union{P, AbstractUtilityReconstructionParameters{P}}) where P
19+
cache::LRU{UInt64, Any} = LRU{UInt64, Any}(maxsize = maxsize)
20+
return new{P}(param, maxsize, cache)
21+
end
22+
function ProcessResultCache(cache::LRU{UInt64, Any}; param::Union{P, AbstractUtilityReconstructionParameters{P}}) where P
23+
maxsize = cache.maxsize
1324
return new{P}(param, maxsize, cache)
1425
end
1526
end
@@ -45,6 +56,10 @@ function Base.setproperty!(plan::RecoPlan{<:ProcessResultCache}, name::Symbol, v
4556
if in(name, [:param, :cache, :maxsize])
4657
t = type(plan, name)
4758
getfield(plan, :values)[name][] = validvalue(plan, t, value) ? value : convert(t, x)
59+
if name == :maxsize && !ismissing(plan.cache)
60+
@warn "Resizing cache will affect all algorithms constructed from this plan" maxlog = 3
61+
resize!(plan.cache; maxsize = plan.maxsize)
62+
end
4863
else
4964
setproperty!(plan.param, name, value)
5065
end
@@ -104,7 +119,17 @@ end
104119
Empty the cache of the `ProcessResultCache`
105120
"""
106121
Base.empty!(cache::ProcessResultCache) = empty!(cache.cache)
122+
"""
123+
resize!(cache::ProcessResultCache)
107124
125+
Resize the cache. This will affect all algorithms sharing the cache, i.e. all algorithms constructed from the same RecoPlan.
126+
"""
127+
function Base.resize!(cache::ProcessResultCache, n)
128+
@warn "Resizing cache will affect all algorithms sharing the cache. Resizing will not update maxsize in RecoPlan" maxlog = 3
129+
cache.maxsize = n
130+
resize!(cache.cache; maxsize = n)
131+
return cache
132+
end
108133
"""
109134
hash(parameter::AbstractImageReconstructionParameters, h)
110135
@@ -117,6 +142,9 @@ function Base.hash(parameter::T, h::UInt64) where T <: AbstractImageReconstructi
117142
end
118143
return h
119144
end
145+
function Base.hash(parameter::ProcessResultCache, h::UInt64)
146+
return hash(typeof(parameter), hash(parameter.maxsize, hash(parameter.param, h)))
147+
end
120148

121149

122150
function showproperty(io::IO, name, property::RecoPlan{ProcessResultCache}, indent, islast, depth)

test/caching.jl

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
@testset "Caching" begin
2+
Base.@kwdef mutable struct CacheableParameter <: AbstractImageReconstructionParameters
3+
factor::Int64
4+
cache_misses::Ref{Int64}
5+
end
6+
Base.@kwdef mutable struct PureCacheableParameter <: AbstractImageReconstructionParameters
7+
factor::Int64
8+
cache_misses::Ref{Int64}
9+
end
10+
mutable struct CacheableAlgorithm{P} <: AbstractImageReconstructionAlgorithm
11+
const parameter::Union{P, ProcessResultCache{P}}
12+
value::Int64
13+
output::Channel{Int64}
14+
CacheableAlgorithm(parameter::ProcessResultCache{P}) where P = new{P}(parameter, 1, Channel{Int64}(Inf))
15+
CacheableAlgorithm(parameter::P) where P = new{P}(parameter, 1, Channel{Int64}(Inf))
16+
end
17+
AbstractImageReconstruction.parameter(algo::CacheableAlgorithm) = algo.parameter
18+
Base.lock(algo::CacheableAlgorithm) = lock(algo.output)
19+
Base.unlock(algo::CacheableAlgorithm) = unlock(algo.output)
20+
Base.take!(algo::CacheableAlgorithm) = Base.take!(algo.output)
21+
function Base.put!(algo::CacheableAlgorithm, value)
22+
lock(algo) do
23+
put!(algo.output, process(algo, algo.parameter, value))
24+
end
25+
end
26+
# Implement proper hashing for algorithm, otherwise hash will ignore changes to value
27+
function Base.hash(algo::CacheableAlgorithm, h::UInt64)
28+
return hash(typeof(algo), hash(algo.output, hash(algo.value, hash(algo.parameter, h))))
29+
end
30+
31+
function AbstractImageReconstruction.process(algo::CacheableAlgorithm, parameter::CacheableParameter, value)
32+
parameter.cache_misses[] += 1
33+
return algo.value + parameter.factor * value
34+
end
35+
function AbstractImageReconstruction.process(algo::CacheableAlgorithm, parameter::Union{PureCacheableParameter, ProcessResultCache{PureCacheableParameter}}, value)
36+
return algo.value + process(typeof(algo), parameter, value)
37+
end
38+
function AbstractImageReconstruction.process(algo, parameter::PureCacheableParameter, value)
39+
parameter.cache_misses[] += 1
40+
return parameter.factor * value
41+
end
42+
43+
@testset "Constructor" begin
44+
cached_parameter = PureCacheableParameter(3, Ref(0))
45+
cache = ProcessResultCache(; param = cached_parameter, maxsize = 42)
46+
cache2 = ProcessResultCache(cache.cache; param = cache.param)
47+
cache3 = ProcessResultCache(42; param = cache.param)
48+
@test cache.cache == cache2.cache
49+
@test cache.maxsize == cache2.maxsize
50+
@test cache.cache.maxsize == cache2.cache.maxsize
51+
@test cache2.maxsize == cache3.maxsize
52+
@test cache2.cache.maxsize == cache3.cache.maxsize
53+
end
54+
55+
@testset "Stateful Process" begin
56+
uncached_parameter = CacheableParameter(3, Ref(0))
57+
algo = CacheableAlgorithm(uncached_parameter)
58+
59+
cache_misses = Ref(0)
60+
cached_parameter = CacheableParameter(3, cache_misses)
61+
cache = ProcessResultCache(; param = cached_parameter, maxsize = 1)
62+
cached_algo = CacheableAlgorithm(cache)
63+
# Inital reco misses cache
64+
@test reconstruct(algo, 42) == reconstruct(cached_algo, 42)
65+
@test cache_misses[] == 1
66+
cache_misses[] = 0
67+
# Other value misses cache
68+
@test reconstruct(algo, 3) == reconstruct(cached_algo, 3)
69+
@test cache_misses[] == 1
70+
cache_misses[] = 0
71+
# Repeated value hits cache
72+
@test reconstruct(algo, 3) == reconstruct(cached_algo, 3)
73+
@test cache_misses[] == 0
74+
cache_misses[] = 0
75+
# Changing parameter results in cache miss
76+
uncached_parameter.factor = 5
77+
cached_parameter.factor = 5
78+
@test reconstruct(algo, 3) == reconstruct(cached_algo, 3)
79+
@test cache_misses[] == 1
80+
cache_misses[] = 0
81+
@test reconstruct(algo, 3) == reconstruct(cached_algo, 3)
82+
@test cache_misses[] == 0
83+
cache_misses[] = 0
84+
# Changing algorithm results in cache miss
85+
algo.value = 2
86+
cached_algo.value = 2
87+
@test reconstruct(algo, 3) == reconstruct(cached_algo, 3)
88+
@test cache_misses[] == 1
89+
cache_misses[] = 0
90+
@test reconstruct(algo, 3) == reconstruct(cached_algo, 3)
91+
@test cache_misses[] == 0
92+
cache_misses[] = 0
93+
94+
@testset "Resize" begin
95+
resize!(cache, 3)
96+
for i in 1:3
97+
@test reconstruct(algo, i) == reconstruct(cached_algo, i)
98+
end
99+
old_misses = cache_misses[]
100+
for i in 1:3
101+
@test reconstruct(algo, i) == reconstruct(cached_algo, i)
102+
@test cache_misses[] == old_misses
103+
end
104+
resize!(cache, 0)
105+
for i in 1:3
106+
@test reconstruct(algo, i) == reconstruct(cached_algo, i)
107+
end
108+
@test cache_misses[] == old_misses + 3
109+
end
110+
111+
112+
end
113+
114+
@testset "Pure Process" begin
115+
uncached_parameter = CacheableParameter(3, Ref(0))
116+
algo = CacheableAlgorithm(uncached_parameter)
117+
118+
cache_misses = Ref(0)
119+
cached_parameter = PureCacheableParameter(3, cache_misses)
120+
cache = ProcessResultCache(; param = cached_parameter, maxsize = 1)
121+
cached_algo = CacheableAlgorithm(cache)
122+
# Inital reco misses cache
123+
@test reconstruct(algo, 42) == reconstruct(cached_algo, 42)
124+
@test cache_misses[] == 1
125+
cache_misses[] = 0
126+
# Other value misses cache
127+
@test reconstruct(algo, 3) == reconstruct(cached_algo, 3)
128+
@test cache_misses[] == 1
129+
cache_misses[] = 0
130+
# Repeated value hits cache
131+
@test reconstruct(algo, 3) == reconstruct(cached_algo, 3)
132+
@test cache_misses[] == 0
133+
cache_misses[] = 0
134+
# Changing parameter results in cache miss
135+
uncached_parameter.factor = 5
136+
cached_parameter.factor = 5
137+
@test reconstruct(algo, 3) == reconstruct(cached_algo, 3)
138+
@test cache_misses[] == 1
139+
cache_misses[] = 0
140+
@test reconstruct(algo, 3) == reconstruct(cached_algo, 3)
141+
@test cache_misses[] == 0
142+
cache_misses[] = 0
143+
# Changing algorithm results in no cache miss
144+
algo.value = 2
145+
cached_algo.value = 2
146+
@test reconstruct(algo, 3) == reconstruct(cached_algo, 3)
147+
@test cache_misses[] == 0
148+
cache_misses[] = 0
149+
@test reconstruct(algo, 3) == reconstruct(cached_algo, 3)
150+
@test cache_misses[] == 0
151+
cache_misses[] = 0
152+
153+
@testset "Resize" begin
154+
resize!(cache, 3)
155+
for i in 1:3
156+
@test reconstruct(algo, i) == reconstruct(cached_algo, i)
157+
end
158+
old_misses = cache_misses[]
159+
for i in 1:3
160+
@test reconstruct(algo, i) == reconstruct(cached_algo, i)
161+
@test cache_misses[] == old_misses
162+
end
163+
resize!(cache, 0)
164+
for i in 1:3
165+
@test reconstruct(algo, i) == reconstruct(cached_algo, i)
166+
end
167+
@test cache_misses[] == old_misses + 3
168+
end
169+
170+
end
171+
172+
@testset "RecoPlan" begin
173+
cache_misses = Ref(0)
174+
cached_parameter = CacheableParameter(3, cache_misses)
175+
cache = ProcessResultCache(; param = cached_parameter, maxsize = 1)
176+
177+
plan = toPlan(cache)
178+
setAll!(plan, :maxsize, 3)
179+
@test plan.cache.maxsize == 3
180+
181+
clear!(plan.param)
182+
io = IOBuffer()
183+
toTOML(io, plan)
184+
seekstart(io)
185+
planCopy = loadPlan(io, [Main, AbstractImageReconstruction])
186+
187+
setAll!(planCopy, :factor, 3)
188+
setAll!(planCopy, :cache_misses, Ref(0))
189+
copy1 = build(planCopy)
190+
copy2 = build(planCopy)
191+
@test copy1.cache == copy2.cache
192+
resize!(copy1, 42)
193+
194+
@test copy2.cache.maxsize == 42
195+
end
196+
197+
end

0 commit comments

Comments
 (0)