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
18 changes: 16 additions & 2 deletions docs/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ Settings are looked up per function, in this order (first match wins):
1. runtime calls: [`set_cache_size!`](@ref);
2. the function's own section, `[<Package>.MemoizationKit.<function>]`;
3. the section of the package that owns the function, `[<Package>.MemoizationKit]`;
4. MemoizationKit's own section, `[MemoizationKit]`;
5. the built-in defaults above.
4. defaults provided by the package's code, [`MemoizationKit.default_cache_settings`](@ref);
5. MemoizationKit's own section, `[MemoizationKit]`;
6. the built-in defaults above.

The package that owns a function is the package of the module that defines it, `parentmodule(typeof(f))`.
For a function extended by several packages, that is the package that defines the function, not the ones that add cached methods to it.
Expand All @@ -45,6 +46,19 @@ maxsize = 2_000_000_000

Unknown keys and invalid values are ignored with a warning.

## Package defaults

A package can ship defaults for its own functions by overloading [`MemoizationKit.default_cache_settings`](@ref), instead of writing preferences on load or calling [`set_cache_size!`](@ref) from `__init__`:

```julia
MemoizationKit.default_cache_settings(::typeof(device_kernel)) = (; measure = "bytes", maxsize = 2^28)
```

These defaults take precedence over the global `[MemoizationKit]` section, which is not specific to the package, so a generic limit does not override a smaller one the package chose, e.g. for values holding device memory.
The package's own sections, `[<Package>.MemoizationKit]` and `[<Package>.MemoizationKit.<function>]`, still override them.
For several functions at once, define the method for a union, e.g. `::Union{typeof(f), typeof(g)}`.
Like preferences, the defaults are read when a function's first cache is created, so define the method in the same package or extension as the cached methods.

## When changes take effect

- `disk` and `disk_path` are read when a function first uses its disk cache in the session.
Expand Down
1 change: 1 addition & 0 deletions docs/src/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ empty_caches!
set_cache_size!
set_cache_preferences!
MemoizationKit.cachesize
MemoizationKit.default_cache_settings
cache_dashboard
```

Expand Down
4 changes: 2 additions & 2 deletions ext/MemoizationKitSQLiteExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module MemoizationKitSQLiteExt
# give across nodes. Disk errors never fail a call: they warn once and the call goes on.

using MemoizationKit: MemoizationKit, DiskCache, _compute, _fname, _owner, _ispackage, _within, instrument,
_resolve_settings, _cached_section, _package_section, get_scratch!, sha256
_settings, get_scratch!, sha256
using SQLite: SQLite, DBInterface
using Serialization: serialize, deserialize, writeheader

Expand Down Expand Up @@ -104,7 +104,7 @@ function stores(f::F) where {F}
end

function open_stores(f)
settings = _resolve_settings(_fname(f), _cached_section(), _package_section(f))
settings = _settings(f)
settings.disk || return Stores(f, nothing, nothing)
dir = MemoizationKit.disk_artifact(f)
artifact = dir === nothing ? nothing : attempt(() -> Store(joinpath(dir, storename(f) * ".sqlite"); immutable = true), "open the disk artifact of `$(_fname(f))` in $dir")
Expand Down
2 changes: 1 addition & 1 deletion src/MemoizationKit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export disable_disk_caches!, enable_disk_caches!

# documented API that is used qualified, e.g. by overloading `MemoizationKit.cachesize`
@static if VERSION >= v"1.11.0-DEV.469"
eval(Meta.parse("public AbstractCache, cache_stats, disk_cache_stats, cachesize, implementation, instrument_label, diskversion, disk_artifact, cachekey"))
eval(Meta.parse("public AbstractCache, cache_stats, disk_cache_stats, cachesize, default_cache_settings, implementation, instrument_label, diskversion, disk_artifact, cachekey"))
end

using Base: @lock
Expand Down
47 changes: 40 additions & 7 deletions src/preferences.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# Settings are resolved once per function, when its first cache is created:
# runtime calls (`set_cache_size!`, ...) > [<Package>.MemoizationKit.<function>] > [<Package>.MemoizationKit]
# > [MemoizationKit] > built-in defaults
# > default_cache_settings(f) > [MemoizationKit] > built-in defaults
# where <Package> is the package that owns the function (`parentmodule(typeof(f))`).
# The default container is a compile-time preference of MemoizationKit only, since it selects the
# `CacheStyle` and so must be a constant.
Expand All @@ -24,18 +24,49 @@ cachesize(x) = Base.summarysize(x)

const MEASURES = Dict{String, Any}("count" => nothing, "bytes" => cachesize)

"""
MemoizationKit.default_cache_settings(f) -> NamedTuple

Default cache settings of `f` provided by the package that defines its cached methods, e.g. a
smaller limit for values that hold scarce memory. They take precedence over the built-in defaults
and the `[MemoizationKit]` preferences, but the preferences of the package that owns `f` and of
`f` itself override them. Defaults to `(;)`, no settings. Settings are `maxsize`, `measure`
(`"count"` or `"bytes"`), `disk` and `disk_path`, as for [`set_cache_preferences!`](@ref).

```julia
MemoizationKit.default_cache_settings(::typeof(device_kernel)) = (; measure = "bytes", maxsize = 2^28)
```

Settings are read when the first cache of `f` is created, so define this method in the same
package (or extension) as the cached methods of `f`. Unknown or invalid settings throw an
`ArgumentError` from that call.
"""
default_cache_settings(f) = (;)

const DEFAULT_CONTAINER = let name = @load_preference("container", "ClockCache")
containers = Dict("ClockCache" => ClockCache, "LRU" => LRU)
haskey(containers, name) ||
error("MemoizationKit: invalid preference `container = $(repr(name))`; use \"ClockCache\" or \"LRU\"")
containers[name]
end

# Resolve settings from the preference sections, most general first. `package` is the
# `[<Package>.MemoizationKit]` table (or `nothing`), whose sub-tables are per-function sections.
function _resolve_settings(fname::AbstractString, cached::AbstractDict, package)
# Resolve settings from the preference sections and the package defaults, most general first.
# `defaults` is `default_cache_settings(f)`, and `package` the `[<Package>.MemoizationKit]` table
# (or `nothing`), whose sub-tables are per-function sections. Invalid preferences are ignored with
# a warning, while invalid defaults are a bug of the package and throw.
function _resolve_settings(fname::AbstractString, cached::AbstractDict, defaults, package)
settings = Dict{String, Any}(string(k) => v for (k, v) in pairs(BUILTIN_SETTINGS))
_merge_settings!(settings, cached, "[MemoizationKit]")
for (k, v) in pairs(defaults)
k = string(k)
k in SETTING_KEYS && _isvalid(k, v) || throw(
ArgumentError(
"$(k in SETTING_KEYS ? "invalid" : "unknown") cache setting `$k = $(repr(v))` " *
"in `MemoizationKit.default_cache_settings(::typeof($fname))`"
)
)
settings[k] = v
end
if package isa AbstractDict
_merge_settings!(settings, package, "[<package>.MemoizationKit]")
section = get(package, fname, nothing)
Expand All @@ -53,9 +84,9 @@ function _merge_settings!(settings, section::AbstractDict, where)
for (k, v) in section
v isa AbstractDict && continue # per-function section
if !(k in SETTING_KEYS)
@warn "MemoizationKit: ignoring unknown preference `$k` in $where"
@warn "MemoizationKit: ignoring unknown setting `$k` in $where"
elseif !_isvalid(k, v)
@warn "MemoizationKit: ignoring invalid preference `$k = $(repr(v))` in $where"
@warn "MemoizationKit: ignoring invalid setting `$k = $(repr(v))` in $where"
else
settings[k] = v
end
Expand All @@ -82,8 +113,10 @@ end

_fname(f) = string(f isa Function ? nameof(f) : nameof(typeof(f)))

_settings(f) = _resolve_settings(_fname(f), _cached_section(), default_cache_settings(f), _package_section(f))

function FunctionCaches(f)
s = _resolve_settings(_fname(f), _cached_section(), _package_section(f))
s = _settings(f)
return FunctionCaches(s.maxsize, s.by, AbstractCache[])
end

Expand Down
4 changes: 2 additions & 2 deletions test/test_disk.jl
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ MemoizationKit.DiskCacheStyle(::Union{typeof(prefoff), typeof(prefpath), typeof(

@testset "preferences, and disk errors" begin
package = Dict{String, Any}("disk_path" => "/x", "f" => Dict{String, Any}("disk" => false))
@test MemoizationKit._resolve_settings("f", Dict{String, Any}("disk" => true), package)[(:disk, :disk_path)] == (; disk = false, disk_path = "/x")
@test (@test_logs (:warn, r"invalid preference `disk = 1`") MemoizationKit._resolve_settings("f", Dict{String, Any}("disk" => 1), nothing)).disk
@test MemoizationKit._resolve_settings("f", Dict{String, Any}("disk" => true), (;), package)[(:disk, :disk_path)] == (; disk = false, disk_path = "/x")
@test (@test_logs (:warn, r"invalid setting `disk = 1`") MemoizationKit._resolve_settings("f", Dict{String, Any}("disk" => 1), (;), nothing)).disk
@test_throws ArgumentError set_cache_preferences!(; disk = 1)

# in a private project, as in `test_preferences.jl`; it comes before `DIR` on the load path
Expand Down
58 changes: 44 additions & 14 deletions test/test_preferences.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,38 @@ resolve(args...) = MemoizationKit._resolve_settings(args...)[(:maxsize, :by)] #
caches(f) = last.(cache_info(f))

@testset "resolution order" begin
@test resolve("f", Dict(), nothing) == (; maxsize = 10_000, by = nothing)
@test resolve("f", Dict(), (;), nothing) == (; maxsize = 10_000, by = nothing)
cached = Dict{String, Any}("maxsize" => 500)
@test resolve("f", cached, nothing) == (; maxsize = 500, by = nothing)
package = Dict{String, Any}("maxsize" => 50, "measure" => "bytes", "g" => Dict{String, Any}("maxsize" => 5))
@test resolve("f", cached, package) == (; maxsize = 50, by = MemoizationKit.cachesize)
@test resolve("g", cached, package) == (; maxsize = 5, by = MemoizationKit.cachesize)
@test resolve("f", cached, (;), nothing) == (; maxsize = 500, by = nothing)
# package defaults override MemoizationKit's section, but not the package's sections
defaults = (; maxsize = 100, measure = "bytes")
@test resolve("f", cached, defaults, nothing) == (; maxsize = 100, by = MemoizationKit.cachesize)
package = Dict{String, Any}("maxsize" => 50, "g" => Dict{String, Any}("maxsize" => 5, "measure" => "count"))
@test resolve("f", cached, defaults, package) == (; maxsize = 50, by = MemoizationKit.cachesize)
@test resolve("g", cached, defaults, package) == (; maxsize = 5, by = nothing)
end

@testset "invalid preferences are ignored with a warning" begin
@testset "invalid settings are ignored with a warning" begin
for (section, msg) in (
Dict{String, Any}("maxsise" => 1) => r"unknown preference `maxsise`",
Dict{String, Any}("maxsize" => -1) => r"invalid preference `maxsize = -1`",
Dict{String, Any}("maxsize" => true) => r"invalid preference `maxsize = true`",
Dict{String, Any}("maxsubcaches" => 2) => r"unknown preference `maxsubcaches`", # removed setting
Dict{String, Any}("measure" => "kilos") => r"invalid preference `measure = \"kilos\"`",
Dict{String, Any}("maxsise" => 1) => r"unknown setting `maxsise`",
Dict{String, Any}("maxsize" => -1) => r"invalid setting `maxsize = -1`",
Dict{String, Any}("maxsize" => true) => r"invalid setting `maxsize = true`",
Dict{String, Any}("maxsubcaches" => 2) => r"unknown setting `maxsubcaches`", # removed setting
Dict{String, Any}("measure" => "kilos") => r"invalid setting `measure = \"kilos\"`",
)
s = @test_logs (:warn, msg) resolve("f", section, nothing)
@test s == resolve("f", Dict(), nothing)
s = @test_logs (:warn, msg) resolve("f", section, (;), nothing)
@test s == resolve("f", Dict(), (;), nothing)
end
end

@testset "invalid package defaults throw" begin
@test_throws r"invalid cache setting `measure = :bytes`.*default_cache_settings\(::typeof\(f\)\)" resolve(
"f", Dict(), (; measure = :bytes), nothing
)
@test_throws r"unknown cache setting `maxsise = 1`" resolve("f", Dict(), (; maxsise = 1), nothing)
@test_throws ArgumentError resolve("f", Dict(), (; container = "LRU"), nothing) # global only
end

struct Key
x::Int
end
Expand All @@ -36,6 +47,10 @@ end
@cached fromcached(x) = x
@cached Aqua.test_all(k::Key) = k.x
@cached Aqua.test_ambiguities(k::Key) = k.x
@cached Aqua.test_unbound_args(k::Key) = k.x
MemoizationKit.default_cache_settings(::typeof(Aqua.test_unbound_args)) = (; maxsize = 6)
@cached Aqua.test_undefined_exports(k::Key) = k.x
MemoizationKit.default_cache_settings(::typeof(Aqua.test_undefined_exports)) = (; maxsize = -1)

# Preferences are written next to the active project. Point it at a private temporary project
# while writing them, so that test files running in parallel never see them.
Expand Down Expand Up @@ -79,13 +94,28 @@ with_private_preferences() do
Aqua.test_ambiguities(Key(1))
@test MemoizationKit.cache_stats(only(cache_info(Aqua.test_all)).second).maxsize == 3
@test MemoizationKit.cache_stats(only(cache_info(Aqua.test_ambiguities)).second).maxsize == 2
# package defaults override MemoizationKit's section, the package's section overrides them
@test MemoizationKit._settings(Aqua.test_unbound_args).maxsize == 3
# runtime settings still override preferences
set_cache_size!(Aqua.test_all, 11)
@test MemoizationKit.cache_stats(only(cache_info(Aqua.test_all)).second).maxsize == 11
finally
delete_preferences!(Aqua, "MemoizationKit"; force = true)
end

# package defaults apply without preferences, and take precedence over MemoizationKit's section
set_preferences!(MemoizationKit, "maxsize" => 7; force = true)
try
Aqua.test_unbound_args(Key(1))
@test MemoizationKit.cache_stats(only(cache_info(Aqua.test_unbound_args)).second).maxsize == 6
finally
delete_preferences!(MemoizationKit, "maxsize"; force = true)
end

# invalid package defaults fail the call, every time
@test_throws ArgumentError Aqua.test_undefined_exports(Key(1))
@test_throws ArgumentError Aqua.test_undefined_exports(Key(1))

# functions outside packages only see MemoizationKit's section
@test MemoizationKit._package_section(fromcached) === nothing
end
Expand All @@ -109,7 +139,7 @@ with_private_preferences() do
"measure" => "bytes", "test_all" => Dict("maxsize" => 4),
"test_ambiguities" => Dict("measure" => "count")
)
@test resolve("test_all", Dict(), section) ==
@test resolve("test_all", Dict(), (;), section) ==
(; maxsize = 4, by = MemoizationKit.cachesize)
set_cache_preferences!(Aqua.test_all; maxsize = nothing) # empty sections are removed
@test !haskey(load_preference(Aqua, "MemoizationKit"), "test_all")
Expand Down
Loading