From 4ace3441a69c8dc8f807e8e3be1332eaa56be51b Mon Sep 17 00:00:00 2001 From: lkdvos Date: Mon, 5 Oct 2026 15:08:23 -0400 Subject: [PATCH] Add representative PrecompileTools workload --- Project.toml | 2 + src/Cached.jl | 3 ++ src/precompile.jl | 112 ++++++++++++++++++++++++++++++++++++++++ test/test_precompile.jl | 20 +++++++ 4 files changed, 137 insertions(+) create mode 100644 src/precompile.jl create mode 100644 test/test_precompile.jl diff --git a/Project.toml b/Project.toml index fb293ef..11d69de 100644 --- a/Project.toml +++ b/Project.toml @@ -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" @@ -25,6 +26,7 @@ CachedTimerOutputsExt = "TimerOutputs" [compat] ExprTools = "0.1" +PrecompileTools = "1" Preferences = "1.4" SHA = "0.7, 1" SQLite = "1.6" diff --git a/src/Cached.jl b/src/Cached.jl index e55793d..243ab17 100644 --- a/src/Cached.jl +++ b/src/Cached.jl @@ -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 @@ -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) && diff --git a/src/precompile.jl b/src/precompile.jl new file mode 100644 index 0000000..bb0afb4 --- /dev/null +++ b/src/precompile.jl @@ -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 diff --git a/test/test_precompile.jl b/test/test_precompile.jl new file mode 100644 index 0000000..5484f59 --- /dev/null +++ b/test/test_precompile.jl @@ -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