Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ projects = ["test", "docs", "benchmark"]

[deps]
ExprTools = "e2ba6199-217a-4e67-a87a-7c52f15ade04"
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
Preferences = "21216c6a-2e73-6563-6e65-726566657250"
SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"
Scratch = "6c6a2e73-6563-6170-7368-637461726353"
Expand All @@ -25,6 +26,7 @@ CachedTimerOutputsExt = "TimerOutputs"

[compat]
ExprTools = "0.1"
PrecompileTools = "1"
Preferences = "1.4"
SHA = "0.7, 1"
SQLite = "1.6"
Expand Down
3 changes: 3 additions & 0 deletions src/Cached.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ end

using Base: @lock
using ExprTools: ExprTools
using PrecompileTools: @setup_workload
using Preferences: @load_preference, load_preference, has_preference, set_preferences!, delete_preferences!
using Serialization: AbstractSerializer, Serializer
using Scratch: Scratch, get_scratch! # with SHA, for the SQLite extension
Expand All @@ -34,6 +35,8 @@ include("keys.jl")
include("call.jl")
include("api.jl")
include("macro.jl")
# Source loading should not introduce the workload's module or helper methods.
@setup_workload include("precompile.jl")

function __init__()
isdefined(Base.Experimental, :register_error_hint) &&
Expand Down
112 changes: 112 additions & 0 deletions src/precompile.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Keep the example functions private and separate from the public API.
module _Precompile

using ..Cached
using PrecompileTools: @setup_workload, @compile_workload

@setup_workload begin
struct Workload{S, D}
offset::Int
end

Cached.CacheStyle(::Workload{S}, args...) where {S} = S()
Cached.DiskCacheStyle(::Workload{S, D}, args...) where {S, D} = D()

@cached (f::Workload)(x) = f.offset + x
@cached (f::Workload)(x, rest...; scale = 1)::Int = scale * (f.offset + x + length(rest))
@cached basic(x) = x
@cached unstable(x) = x > 0 ? x : "negative"
@cached keyed(x) = length(x)
Cached.cachekey(::typeof(keyed), x) = (Hashed(x),)

let
# Preserve task-local state, and remove only this workload's registry entries.
tls = task_local_storage()
had_local = haskey(tls, :__Cached_tasklocal__)
old_local = get(tls, :__Cached_tasklocal__, nothing)
tls[:__Cached_tasklocal__] = IdDict{Any, Any}()
io = IOBuffer()
try
@compile_workload begin
# Compile the macro itself as well as the wrappers generated above.
for ex in (
:(@cached example(x) = x),
:(
@cached function example(x::T, y = 1, rest...; scale = 1, kw...)::T where {T}
return x
end
),
:(@cached (::Workload)(::Int) = 1),
)
macroexpand(@__MODULE__, ex)
end

for S in (GlobalCache{ClockCache}, GlobalCache{LRU}, TaskLocalCache{ClockCache}, TaskLocalCache{LRU}, TaskLocalCache{Dict}, NoCache)
# Disk calls compile their RAM/compute fallback without touching files.
for D in (NoCache, DiskCache{Cached.Serializer})
f = Workload{S, D}(1)
set_cache_size!(f, 2)
for x in (1, 2, 3, 3)
f(x)
f(x, 2; scale = 2)
end
f(1.0)
uncached(f, 1)
uncached(f, 1, 2; scale = 2)
Cached.instrument_label(f, Val(:lookup))
cache_info(f)
set_cache_size!(f, 1)
empty_caches!(f)
set_cache_size!(f, 32; by = Cached.cachesize)
f(1)
end
end

basic(1)
basic(1)
unstable(1)
unstable(-1)
keyed([1, 2])
keyed([1, 2])
cache_info()
cache_info(@__MODULE__)
empty_caches!(@__MODULE__)

# Exercise both eviction policies and the shared dictionary interface,
# including typed/untyped storage and byte-based size accounting.
for C in (LRU, ClockCache)
for c in (C{Any, Any}(; maxsize = 2), C{Int, Int}(; maxsize = 2), C{Int, Int}(; maxsize = 16, by = Cached.cachesize))
for x in (1, 2, 3, 3)
get!(Returns(x), c, x)
end
c[3]
c[3] = 4
get(c, 0, nothing)
haskey(c, 3)
length(c)
isempty(c)
collect(c)
Cached.cache_stats(c)
show(io, c)
show(io, MIME"text/plain"(), c)
delete!(c, 3)
c[4] = 4
resize!(c; maxsize = 0)
empty!(c)
end
end
end
finally
had_local ? (tls[:__Cached_tasklocal__] = old_local) : pop!(tls, :__Cached_tasklocal__, nothing)
# Emptying a cache alone keeps its registration and size settings alive.
Base.@lock Cached.REGISTRY.lock begin
for f in collect(keys(Cached.REGISTRY.functions))
parentmodule(typeof(f)) === (@__MODULE__) && delete!(Cached.REGISTRY.functions, f)
end
Cached._publish!()
end
end
end
end

end # module _Precompile
20 changes: 20 additions & 0 deletions test/test_precompile.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Test
using Cached

@testset "precompile workload leaves no cache state" begin
if isdefined(Cached, :_Precompile)
workload = Cached._Precompile
@test isempty(cache_info(workload))
@test all(f -> parentmodule(typeof(f)) !== workload, keys(Cached.REGISTRY.functions))
@test all(k -> parentmodule(typeof(first(k))) !== workload, keys(get(task_local_storage(), :__Cached_tasklocal__, IdDict())))
else
@test isempty(cache_info(Cached))
end
@test Cached.DISK_ENABLED[]
end

if Base.JLOptions().use_compiled_modules == 0
@testset "source loading skips precompile helpers" begin
@test !isdefined(Cached, :_Precompile)
end
end
Loading