From f435ccf88ddb0dc81d84aa58100db9474c4fc3fa Mon Sep 17 00:00:00 2001 From: lkdvos Date: Thu, 8 Oct 2026 08:59:02 -0400 Subject: [PATCH 1/3] rename to MemoizationKit.jl --- .github/workflows/Documentation.yml | 4 +- CLAUDE.md | 6 +- Project.toml | 8 +- README.md | 28 +++---- benchmark/Project.toml | 4 +- benchmark/containers.jl | 8 +- benchmark/disk_stress.jl | 10 +-- docs/Project.toml | 4 +- docs/examples/package.jl | 6 +- docs/make.jl | 12 +-- docs/src/configuration.md | 38 ++++----- docs/src/dashboard.md | 26 +++---- docs/src/disk.md | 44 +++++------ docs/src/eviction.md | 14 ++-- docs/src/index.md | 48 ++++++------ docs/src/interface.md | 18 ++--- docs/src/keys.md | 28 +++---- docs/src/reference.md | 22 +++--- docs/src/timing.md | 14 ++-- ext/CachedTimerOutputsExt.jl | 27 ------- ...QLiteExt.jl => MemoizationKitSQLiteExt.jl} | 28 +++---- ...maExt.jl => MemoizationKitTachikomaExt.jl} | 22 +++--- ext/MemoizationKitTimerOutputsExt.jl | 27 +++++++ src/{Cached.jl => MemoizationKit.jl} | 6 +- src/api.jl | 20 ++--- src/cachestyle.jl | 4 +- src/call.jl | 8 +- src/containers/interface.jl | 2 +- src/disk.jl | 12 +-- src/keys.jl | 12 +-- src/macro.jl | 6 +- src/precompile.jl | 34 ++++---- src/preferences.jl | 40 +++++----- test/Project.toml | 4 +- test/runtests.jl | 4 +- test/test_aqua.jl | 6 +- test/test_containers.jl | 4 +- test/test_disk.jl | 78 +++++++++---------- test/test_instrument.jl | 42 +++++----- test/test_keys.jl | 2 +- test/test_macro.jl | 38 ++++----- test/test_precompile.jl | 16 ++-- test/test_preferences.jl | 68 ++++++++-------- test/test_tachikoma.jl | 52 ++++++------- 44 files changed, 452 insertions(+), 452 deletions(-) delete mode 100644 ext/CachedTimerOutputsExt.jl rename ext/{CachedSQLiteExt.jl => MemoizationKitSQLiteExt.jl} (88%) rename ext/{CachedTachikomaExt.jl => MemoizationKitTachikomaExt.jl} (95%) create mode 100644 ext/MemoizationKitTimerOutputsExt.jl rename src/{Cached.jl => MemoizationKit.jl} (91%) diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml index 2d76984..542d133 100644 --- a/.github/workflows/Documentation.yml +++ b/.github/workflows/Documentation.yml @@ -32,9 +32,9 @@ jobs: uses: actions/github-script@v9 with: script: | - const marker = ''; + const marker = ''; const issue_number = context.issue.number; - const url = `https://lkdvos.github.io/Cached.jl/previews/PR${issue_number}/`; + const url = `https://lkdvos.github.io/MemoizationKit.jl/previews/PR${issue_number}/`; const body = `${marker}\n[Preview the documentation](${url})`; const comments = await github.paginate(github.rest.issues.listComments, { ...context.repo, diff --git a/CLAUDE.md b/CLAUDE.md index 474778d..6496343 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ ## Layout -This repository uses sibling worktrees with the bare Git repository at `../Cached.jl`. +This repository uses sibling worktrees with a shared bare Git repository in the parent directory. Create additional worktrees beside the current checkout, never inside it. - `src/`: memoization macro, strategy dispatch, cache containers, registry, and configuration. @@ -37,11 +37,11 @@ Keep Markdown prose to one sentence per line. ## Implementation constraints -- `@cached` preserves the method signature and moves the body to `Cached.implementation`. +- `@cached` preserves the method signature and moves the body to `MemoizationKit.implementation`. - The default global cache shares one size limit across a function's cached methods, with a separate cache per container type. - Preserve inferred return types and allocation-free RAM hits for concrete keys with the built-in containers. - Built-in RAM containers distinguish key types and use `hash` and `isequal`. -- `Cached.cachekey` may merge equivalent inputs; merged calls must accept the same result and return type. +- `MemoizationKit.cachekey` may merge equivalent inputs; merged calls must accept the same result and return type. - Compute misses outside cache locks so recursion works; concurrent misses may compute twice. - Keep the call path safe for precompiled packages, without runtime method definitions. - Disk lookup order is RAM, read-only artifact, local database, then computation. diff --git a/Project.toml b/Project.toml index 11d69de..dcd620c 100644 --- a/Project.toml +++ b/Project.toml @@ -1,4 +1,4 @@ -name = "Cached" +name = "MemoizationKit" uuid = "1b238080-9255-4fe9-b224-89eb24efe93b" version = "0.1.0" authors = ["Lukas Devos and contributors"] @@ -20,9 +20,9 @@ Tachikoma = "468859d6-42d8-48b7-8ad9-1d312e0e3b0a" TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" [extensions] -CachedSQLiteExt = "SQLite" -CachedTachikomaExt = "Tachikoma" -CachedTimerOutputsExt = "TimerOutputs" +MemoizationKitSQLiteExt = "SQLite" +MemoizationKitTachikomaExt = "Tachikoma" +MemoizationKitTimerOutputsExt = "TimerOutputs" [compat] ExprTools = "0.1" diff --git a/README.md b/README.md index 3ccd46f..6793b60 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# Cached.jl +# MemoizationKit.jl -[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://lkdvos.github.io/Cached.jl/stable/) -[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://lkdvos.github.io/Cached.jl/dev/) -[![Build Status](https://github.com/lkdvos/Cached.jl/actions/workflows/Tests.yml/badge.svg?branch=main)](https://github.com/lkdvos/Cached.jl/actions/workflows/Tests.yml?query=branch%3Amain) -[![Coverage](https://codecov.io/gh/lkdvos/Cached.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/lkdvos/Cached.jl) +[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://lkdvos.github.io/MemoizationKit.jl/stable/) +[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://lkdvos.github.io/MemoizationKit.jl/dev/) +[![Build Status](https://github.com/lkdvos/MemoizationKit.jl/actions/workflows/Tests.yml/badge.svg?branch=main)](https://github.com/lkdvos/MemoizationKit.jl/actions/workflows/Tests.yml?query=branch%3Amain) +[![Coverage](https://codecov.io/gh/lkdvos/MemoizationKit.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/lkdvos/MemoizationKit.jl) [![Code Style: Runic](https://img.shields.io/badge/code_style-%F0%9F%AA%A8_Runic-9558B2)](https://github.com/fredrikekre/Runic.jl) [![Aqua](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl) @@ -13,12 +13,12 @@ Memoization for Julia, with bounded memory, persistent results, and a live view - **Live dashboard:** watch hit rates and sizes, browse RAM and disk caches, and clear or resize RAM caches from your terminal. - **Fast RAM hits:** preserve return-type inference and avoid allocations with the built-in caches and concrete keys. - **Flexible strategies:** share results across tasks, keep them local to a task, or skip caching for selected argument types. -- **Custom keys:** reuse results for equivalent inputs with `Cached.cachekey`, or customize hashing and equality with `Hashed`. +- **Custom keys:** reuse results for equivalent inputs with `MemoizationKit.cachekey`, or customize hashing and equality with `Hashed`. - **Bounded storage:** Clock and LRU eviction, with limits in entries or bytes across a function's methods. - **Package controls:** set defaults per package or function, inspect caches programmatically, and profile lookups and computations with TimerOutputs.jl. ```julia -using Cached +using MemoizationKit @cached function fib(n::Int)::BigInt n < 2 ? BigInt(n) : fib(n - 1) + fib(n - 2) @@ -36,13 +36,13 @@ Load SQLite.jl and opt a function into disk caching: ```julia using SQLite -Cached.DiskCacheStyle(::typeof(fib), ::Int) = DiskCache() +MemoizationKit.DiskCacheStyle(::typeof(fib), ::Int) = DiskCache() fib(200) # new results are kept in RAM and on disk ``` Calls look in RAM, then on disk, before computing. Processes on the same machine share the database. -You control result versions and disk cleanup; see [disk caching](https://lkdvos.github.io/Cached.jl/dev/disk/). +You control result versions and disk cleanup; see [disk caching](https://lkdvos.github.io/MemoizationKit.jl/dev/disk/). ## See what your caches are doing @@ -63,14 +63,14 @@ The dashboard shows recent hit rates, storage use, and activity, with separate R ⇥ tab ↑↓ select ⏎ resize e empty s sort / filter q quit ``` -See the [dashboard guide](https://lkdvos.github.io/Cached.jl/dev/dashboard/) for controls and statistics. +See the [dashboard guide](https://lkdvos.github.io/MemoizationKit.jl/dev/dashboard/) for controls and statistics. -## Why Cached? +## Why MemoizationKit? -Choose Cached when memoization needs ongoing management: memory limits, reuse across runs, or visibility into a running workload. +Choose MemoizationKit when memoization needs ongoing management: memory limits, reuse across runs, or visibility into a running workload. It brings these tools together with strategies chosen by function and argument type. For a single in-memory cache, Memoize.jl or Memoization.jl with an LRU container may already cover your needs. -See [the comparison and tradeoffs](https://lkdvos.github.io/Cached.jl/dev/#why-cached). +See [the comparison and tradeoffs](https://lkdvos.github.io/MemoizationKit.jl/dev/#why-memoizationkit). Requires Julia 1.10 or later. -Start with [usage](https://lkdvos.github.io/Cached.jl/dev/#usage), [configuration](https://lkdvos.github.io/Cached.jl/dev/configuration/), or [timing](https://lkdvos.github.io/Cached.jl/dev/timing/) in the [documentation](https://lkdvos.github.io/Cached.jl/dev/). +Start with [usage](https://lkdvos.github.io/MemoizationKit.jl/dev/#usage), [configuration](https://lkdvos.github.io/MemoizationKit.jl/dev/configuration/), or [timing](https://lkdvos.github.io/MemoizationKit.jl/dev/timing/) in the [documentation](https://lkdvos.github.io/MemoizationKit.jl/dev/). diff --git a/benchmark/Project.toml b/benchmark/Project.toml index 05191f6..66ef505 100644 --- a/benchmark/Project.toml +++ b/benchmark/Project.toml @@ -1,9 +1,9 @@ [deps] BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" -Cached = "1b238080-9255-4fe9-b224-89eb24efe93b" +MemoizationKit = "1b238080-9255-4fe9-b224-89eb24efe93b" LRUCache = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" [sources] -Cached = {path = ".."} +MemoizationKit = {path = ".."} diff --git a/benchmark/containers.jl b/benchmark/containers.jl index df554e9..d622013 100644 --- a/benchmark/containers.jl +++ b/benchmark/containers.jl @@ -6,12 +6,12 @@ # The value computation is trivial, so the numbers measure cache overhead only. using BenchmarkTools, Random, Printf -using Cached: Cached +using MemoizationKit: MemoizationKit using LRUCache: LRUCache const CONTAINERS = [ - "Cached.LRU" => (K, V, n) -> Cached.LRU{K, V}(; maxsize = n), - "Cached.ClockCache" => (K, V, n) -> Cached.ClockCache{K, V}(; maxsize = n), + "MemoizationKit.LRU" => (K, V, n) -> MemoizationKit.LRU{K, V}(; maxsize = n), + "MemoizationKit.ClockCache" => (K, V, n) -> MemoizationKit.ClockCache{K, V}(; maxsize = n), "LRUCache.LRU" => (K, V, n) -> LRUCache.LRU{K, V}(; maxsize = n), ] @@ -31,7 +31,7 @@ function run!(c, seq) end hitrate(c::LRUCache.LRU) = (i = LRUCache.cache_info(c); i.hits / (i.hits + i.misses)) -hitrate(c) = (s = Cached.cache_stats(c); s.hits / (s.hits + s.misses)) +hitrate(c) = (s = MemoizationKit.cache_stats(c); s.hits / (s.hits + s.misses)) function threaded!(c, seq, ntasks) chunks = Iterators.partition(seq, cld(length(seq), ntasks)) diff --git a/benchmark/disk_stress.jl b/benchmark/disk_stress.jl index e08684a..98499e7 100644 --- a/benchmark/disk_stress.jl +++ b/benchmark/disk_stress.jl @@ -12,7 +12,7 @@ # # `test/test_disk.jl` includes this file and calls `stress` on a temporary directory. -using Cached +using MemoizationKit using SQLite: SQLite const COMPUTED = Ref(0) @@ -21,8 +21,8 @@ const COMPUTED = Ref(0) # preference, added to the load path. function use_disk_path(dir) env = mktempdir() - write(joinpath(env, "Project.toml"), "[deps]\nCached = \"$(Base.PkgId(Cached).uuid)\"\n") - write(joinpath(env, "LocalPreferences.toml"), "[Cached]\ndisk_path = $(repr(dir))\n") + write(joinpath(env, "Project.toml"), "[deps]\nMemoizationKit = \"$(Base.PkgId(MemoizationKit).uuid)\"\n") + write(joinpath(env, "LocalPreferences.toml"), "[MemoizationKit]\ndisk_path = $(repr(dir))\n") push!(LOAD_PATH, env) return env end @@ -31,8 +31,8 @@ end stress_expected(k) = [sin(k * i) for i in 1:(64 + 37k % 512)] @cached stress_value(k::Int)::Vector{Float64} = (COMPUTED[] += 1; stress_expected(k)) -Cached.CacheStyle(::typeof(stress_value), ::Int) = NoCache() -Cached.DiskCacheStyle(::typeof(stress_value), ::Int) = DiskCache() +MemoizationKit.CacheStyle(::typeof(stress_value), ::Int) = NoCache() +MemoizationKit.DiskCacheStyle(::typeof(stress_value), ::Int) = DiskCache() # Every key three times, in an order that depends on `seed`. function stress_worker(dir, nkeys, seed) diff --git a/docs/Project.toml b/docs/Project.toml index 8ec86fa..9023a08 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,5 +1,5 @@ [deps] -Cached = "1b238080-9255-4fe9-b224-89eb24efe93b" +MemoizationKit = "1b238080-9255-4fe9-b224-89eb24efe93b" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" SQLite = "0aa819cd-b072-5ff4-a722-6bc24af294d9" Tachikoma = "468859d6-42d8-48b7-8ad9-1d312e0e3b0a" @@ -12,4 +12,4 @@ Tachikoma = "2.7" TimerOutputs = "0.5, 1" [sources] -Cached = {path = ".."} +MemoizationKit = {path = ".."} diff --git a/docs/examples/package.jl b/docs/examples/package.jl index daadb84..7b387ec 100644 --- a/docs/examples/package.jl +++ b/docs/examples/package.jl @@ -5,11 +5,11 @@ let env = mktempdir(), uid = Base.UUID(rand(UInt128)) mkpath(src) write( joinpath(dirname(src), "Project.toml"), - "name = \"MyPackage\"\nuuid = \"$uid\"\n[deps]\nCached = \"$(Base.PkgId(Cached).uuid)\"\n", + "name = \"MyPackage\"\nuuid = \"$uid\"\n[deps]\nMemoizationKit = \"$(Base.PkgId(MemoizationKit).uuid)\"\n", ) write( joinpath(src, "MyPackage.jl"), - "__precompile__(false)\nmodule MyPackage\nusing Cached\n@cached expensive(x) = x^2\nend\n", + "__precompile__(false)\nmodule MyPackage\nusing MemoizationKit\n@cached expensive(x) = x^2\nend\n", ) pushfirst!(LOAD_PATH, env) package = try @@ -19,7 +19,7 @@ let env = mktempdir(), uid = Base.UUID(rand(UInt128)) end write( joinpath(env, "Project.toml"), - "[deps]\nCached = \"$(Base.PkgId(Cached).uuid)\"\nMyPackage = \"$uid\"\n", + "[deps]\nMemoizationKit = \"$(Base.PkgId(MemoizationKit).uuid)\"\nMyPackage = \"$uid\"\n", ) with_preferences = function (f) original = Base.ACTIVE_PROJECT[] diff --git a/docs/make.jl b/docs/make.jl index b60d64c..4e7f936 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,14 +1,14 @@ -using Cached: Cached +using MemoizationKit: MemoizationKit using Documenter: Documenter, DocMeta, deploydocs, makedocs -DocMeta.setdocmeta!(Cached, :DocTestSetup, :(using Cached); recursive = true) +DocMeta.setdocmeta!(MemoizationKit, :DocTestSetup, :(using MemoizationKit); recursive = true) makedocs(; - modules = [Cached], + modules = [MemoizationKit], authors = "Lukas Devos", - sitename = "Cached.jl", + sitename = "MemoizationKit.jl", format = Documenter.HTML(; - canonical = "https://lkdvos.github.io/Cached.jl", + canonical = "https://lkdvos.github.io/MemoizationKit.jl", edit_link = "main", assets = String[], ), @@ -31,4 +31,4 @@ makedocs(; ], ) -deploydocs(; repo = "github.com/lkdvos/Cached.jl", devbranch = "main", push_preview = true) +deploydocs(; repo = "github.com/lkdvos/MemoizationKit.jl", devbranch = "main", push_preview = true) diff --git a/docs/src/configuration.md b/docs/src/configuration.md index 819925a..b3f36dc 100644 --- a/docs/src/configuration.md +++ b/docs/src/configuration.md @@ -1,7 +1,7 @@ # Configuration ```@meta -CurrentModule = Cached +CurrentModule = MemoizationKit ``` Default cache settings are read with [Preferences.jl](https://github.com/JuliaPackaging/Preferences.jl) from `LocalPreferences.toml`, next to the active project. @@ -11,8 +11,8 @@ Default cache settings are read with [Preferences.jl](https://github.com/JuliaPa | Key | Values | Default | Meaning | | :--- | :--- | :--- | :--- | | `maxsize` | integer ≥ 0 | `10000` | limit of a function's cache, all signatures together, in entries or bytes (see `measure`) | -| `measure` | `"count"` or `"bytes"` | `"count"` | count entries, or measure values with [`Cached.cachesize`](@ref) | -| `container` | `"ClockCache"` or `"LRU"` | `"ClockCache"` | container of the default `CacheStyle`; only in the `[Cached]` section | +| `measure` | `"count"` or `"bytes"` | `"count"` | count entries, or measure values with [`MemoizationKit.cachesize`](@ref) | +| `container` | `"ClockCache"` or `"LRU"` | `"ClockCache"` | container of the default `CacheStyle`; only in the `[MemoizationKit]` section | | `disk` | `true` or `false` | `true` | whether the [disk cache](disk.md) of a function with a `DiskCacheStyle` is used | | `disk_path` | a directory | `""` (a scratch space) | where the [disk caches](disk.md) are stored | @@ -21,9 +21,9 @@ Default cache settings are read with [Preferences.jl](https://github.com/JuliaPa 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, `[.Cached.]`; -3. the section of the package that owns the function, `[.Cached]`; -4. Cached's own section, `[Cached]`; +2. the function's own section, `[.MemoizationKit.]`; +3. the section of the package that owns the function, `[.MemoizationKit]`; +4. MemoizationKit's own section, `[MemoizationKit]`; 5. the built-in defaults above. The package that owns a function is the package of the module that defines it, `parentmodule(typeof(f))`. @@ -31,14 +31,14 @@ For a function extended by several packages, that is the package that defines th ```toml # LocalPreferences.toml -[Cached] +[MemoizationKit] maxsize = 10000 container = "LRU" -[MyPackage.Cached] +[MyPackage.MemoizationKit] maxsize = 50000 -[MyPackage.Cached.expensive] +[MyPackage.MemoizationKit.expensive] measure = "bytes" maxsize = 2_000_000_000 ``` @@ -50,18 +50,18 @@ Unknown keys and invalid values are ignored with a warning. - `disk` and `disk_path` are read when a function first uses its disk cache in the session. - `maxsize` and `measure` are read when a function's first cache is created, so a change applies to functions that have not been called yet in the current session, and to every function after a restart. - `container` is a compile-time preference, because it selects the default `CacheStyle`. - Changing it recompiles Cached on the next start. + Changing it recompiles MemoizationKit on the next start. `GlobalCache()` and `TaskLocalCache()` use this container, so `CacheStyle` methods that return them follow the preference. Use [`set_cache_preferences!`](@ref) to write the sections, which merges with what is already there: ```@example preferences -using Cached -sample = include(joinpath(pkgdir(Cached), "docs", "examples", "package.jl")) # hide +using MemoizationKit +sample = include(joinpath(pkgdir(MemoizationKit), "docs", "examples", "package.jl")) # hide MyPackage = sample.package # hide with_preferences = sample.with_preferences # hide with_preferences() do # hide -set_cache_preferences!(; maxsize = 50_000) # [Cached] +set_cache_preferences!(; maxsize = 50_000) # [MemoizationKit] println(read("LocalPreferences.toml", String)) end # hide ``` @@ -70,8 +70,8 @@ For functions defined in your package, use package or function settings: ```@example preferences with_preferences() do # hide -set_cache_preferences!(MyPackage; measure = "bytes") # [MyPackage.Cached] -set_cache_preferences!(MyPackage.expensive; maxsize = 1000) # [MyPackage.Cached.expensive] +set_cache_preferences!(MyPackage; measure = "bytes") # [MyPackage.MemoizationKit] +set_cache_preferences!(MyPackage.expensive; maxsize = 1000) # [MyPackage.MemoizationKit.expensive] println(read("LocalPreferences.toml", String)) end # hide ``` @@ -87,20 +87,20 @@ end # hide ## Measuring sizes -With `measure = "bytes"`, every cached value is measured once, on insertion, by [`Cached.cachesize`](@ref), which defaults to `Base.summarysize`. +With `measure = "bytes"`, every cached value is measured once, on insertion, by [`MemoizationKit.cachesize`](@ref), which defaults to `Base.summarysize`. That traverses the whole value, which can be slow for large nested values, and counts memory shared between values once per value. Keys and container overhead are excluded; this is a value budget, not a limit on total process memory. Overload it for your own types: ```jldoctest -using Cached +using MemoizationKit struct MyTensor data::Vector{Float64} end -Cached.cachesize(t::MyTensor) = sizeof(t.data); +MemoizationKit.cachesize(t::MyTensor) = sizeof(t.data); -Cached.cachesize(MyTensor(zeros(10))) +MemoizationKit.cachesize(MyTensor(zeros(10))) # output diff --git a/docs/src/dashboard.md b/docs/src/dashboard.md index ee10afe..3c7bb11 100644 --- a/docs/src/dashboard.md +++ b/docs/src/dashboard.md @@ -1,14 +1,14 @@ # Dashboard ```@meta -CurrentModule = Cached +CurrentModule = MemoizationKit ``` [`cache_dashboard`](@ref) opens a terminal dashboard of the global caches, to browse them, watch their hit rates live, and empty or resize them while your program runs. It is a package extension on [Tachikoma.jl](https://github.com/kahliburke/Tachikoma.jl), so load Tachikoma first: ```julia -using Cached, Tachikoma +using MemoizationKit, Tachikoma cache_dashboard() # refreshes every second cache_dashboard(; interval = 0.2, filter = "fs") # faster, only functions whose name contains "fs" ``` @@ -36,25 +36,25 @@ Statistics are copied under brief locks, so the dashboard can run while other ta With a few example caches, the RAM tab looks like this (rendered while building these docs): ```@example dashboard -using Cached, Tachikoma, SQLite # hide +using MemoizationKit, Tachikoma, SQLite # hide env = mktempdir() # the disk caches go to a temporary directory # hide -write(joinpath(env, "Project.toml"), "[deps]\nCached = \"$(Base.PkgId(Cached).uuid)\"\n") # hide -write(joinpath(env, "LocalPreferences.toml"), "[Cached]\ndisk_path = $(repr(mktempdir()))\n") # hide +write(joinpath(env, "Project.toml"), "[deps]\nMemoizationKit = \"$(Base.PkgId(MemoizationKit).uuid)\"\n") # hide +write(joinpath(env, "LocalPreferences.toml"), "[MemoizationKit]\ndisk_path = $(repr(mktempdir()))\n") # hide push!(LOAD_PATH, env) # hide -Core.eval(Main, :(using Cached)) # hide +Core.eval(Main, :(using MemoizationKit)) # hide Core.eval(Main, quote # hide @cached fib(n::Int)::BigInt = n < 2 ? BigInt(n) : fib(n - 1) + fib(n - 2) # hide @cached weights(a, b, c) = a + b * c # hide @cached matrix(n::Int)::Matrix{Float64} = zeros(n, n) # hide @cached label(s::Symbol; upper::Bool = false)::String = upper ? uppercase(string(s)) : string(s) # hide @cached kernel(j::Int)::Matrix{Float64} = ones(j, j) # hide - Cached.CacheStyle(::typeof(fib), ::Int) = GlobalLRUCache() # hide - Cached.DiskCacheStyle(::typeof(fib), ::Int) = DiskCache() # hide - Cached.CacheStyle(::typeof(kernel), ::Int) = NoCache() # hide - Cached.DiskCacheStyle(::typeof(kernel), ::Int) = DiskCache() # hide - set_cache_size!(matrix, 2^20; by = Cached.cachesize) # hide + MemoizationKit.CacheStyle(::typeof(fib), ::Int) = GlobalLRUCache() # hide + MemoizationKit.DiskCacheStyle(::typeof(fib), ::Int) = DiskCache() # hide + MemoizationKit.CacheStyle(::typeof(kernel), ::Int) = NoCache() # hide + MemoizationKit.DiskCacheStyle(::typeof(kernel), ::Int) = DiskCache() # hide + set_cache_size!(matrix, 2^20; by = MemoizationKit.cachesize) # hide end) # hide -const Ext = Base.get_extension(Cached, :CachedTachikomaExt) # hide +const Ext = Base.get_extension(MemoizationKit, :MemoizationKitTachikomaExt) # hide m = Ext.Dashboard(; interval = Inf) # hide for step in 1:40 # hide for i in 1:(20 + (37step) % 60) # hide @@ -96,7 +96,7 @@ The Disk tab lists the disk caches opened in this process, including functions w m.tab = 2 # hide wait(Ext.read_disk!(m).disktask) # the entries and sizes on disk # hide preview(m, 8) # hide -Base.get_extension(Cached, :CachedSQLiteExt).close_all() # hide +Base.get_extension(MemoizationKit, :MemoizationKitSQLiteExt).close_all() # hide filter!(!=(env), LOAD_PATH) # hide nothing # hide ``` diff --git a/docs/src/disk.md b/docs/src/disk.md index e29bdcb..8ab0f3c 100644 --- a/docs/src/disk.md +++ b/docs/src/disk.md @@ -1,7 +1,7 @@ # Disk caching ```@meta -CurrentModule = Cached +CurrentModule = MemoizationKit ``` Results that are expensive to compute can also be kept on disk, so that they survive the process. @@ -10,11 +10,11 @@ The disk is a second level below the cache in memory: a call looks in RAM first Disk caching is a package extension on [SQLite.jl](https://github.com/JuliaDatabases/SQLite.jl): load it with `using SQLite`, or, in a package, add SQLite to its dependencies and `import SQLite`. ```@example disk -using Cached, SQLite +using MemoizationKit, SQLite using Pkg.Artifacts # hide diskenv = mktempdir() # hide -write(joinpath(diskenv, "Project.toml"), "[deps]\nCached = \"$(Base.PkgId(Cached).uuid)\"\n") # hide -write(joinpath(diskenv, "LocalPreferences.toml"), "[Cached]\ndisk_path = $(repr(mktempdir()))\n") # hide +write(joinpath(diskenv, "Project.toml"), "[deps]\nMemoizationKit = \"$(Base.PkgId(MemoizationKit).uuid)\"\n") # hide +write(joinpath(diskenv, "LocalPreferences.toml"), "[MemoizationKit]\ndisk_path = $(repr(mktempdir()))\n") # hide push!(LOAD_PATH, diskenv) # hide computations = Ref(0) @@ -23,7 +23,7 @@ computations = Ref(0) [Float64(i == j) for i in 1:n, j in 1:n] end -Cached.DiskCacheStyle(::typeof(expensive), args...) = DiskCache() +MemoizationKit.DiskCacheStyle(::typeof(expensive), args...) = DiskCache() expensive(2) ``` @@ -50,12 +50,12 @@ Both resolve at compile time, so functions without a disk cache are not affected ## Where the data goes Each function has one SQLite database per machine, `.-v-.sqlite`. -The directory is the `disk_path` preference of the function, its package, or Cached (see [Configuration](configuration.md)), and by default a [scratch space](https://github.com/JuliaPackaging/Scratch.jl) of the package that owns `f`. -Functions outside packages (in scripts or the REPL) use a scratch space of Cached. +The directory is the `disk_path` preference of the function, its package, or MemoizationKit (see [Configuration](configuration.md)), and by default a [scratch space](https://github.com/JuliaPackaging/Scratch.jl) of the package that owns `f`. +Functions outside packages (in scripts or the REPL) use a scratch space of MemoizationKit. ```@example diskprefs -using Cached # hide -sample = include(joinpath(pkgdir(Cached), "docs", "examples", "package.jl")) # hide +using MemoizationKit # hide +sample = include(joinpath(pkgdir(MemoizationKit), "docs", "examples", "package.jl")) # hide MyPackage = sample.package # hide with_preferences = sample.with_preferences # hide with_preferences() do # hide @@ -76,14 +76,14 @@ Disk storage has no automatic size limit or eviction; use [`empty_disk_caches!`] ## Versions -The file name holds a version, [`Cached.diskversion(f)`](@ref), `"1"` by default. +The file name holds a version, [`MemoizationKit.diskversion(f)`](@ref), `"1"` by default. Keeping it current is up to you: bump it when the results of `f` change, and the old file is no longer read. ```@example version -using Cached # hide +using MemoizationKit # hide @cached expensive(n) = n^2 # hide -Cached.diskversion(::typeof(expensive)) = "2" -Cached.diskversion(expensive) +MemoizationKit.diskversion(::typeof(expensive)) = "2" +MemoizationKit.diskversion(expensive) ``` The default format is that of `Serialization`, which is not guaranteed to be readable by other Julia versions, nor after the definition of a stored type changes. @@ -96,7 +96,7 @@ Errors of the disk itself, such as a full disk, never fail a call: they are repo Keys and values are written with `Serialization`, indexed by the SHA-256 hash of the serialized key; the key is stored too, to guard against hash collisions. -[`Cached.cachekey`](@ref) selects the key for both RAM and disk caching. +[`MemoizationKit.cachekey`](@ref) selects the key for both RAM and disk caching. To share disk entries between equivalent inputs, return a canonical representation that serializes to the same bytes. [`Hashed`](@ref) changes RAM hashing and equality, but its wrapped value is still serialized, so custom equality alone does not merge disk entries. See [Custom cache keys](keys.md) for examples and the equality contract. @@ -134,11 +134,11 @@ function Serialization.deserialize(s::MySerializer, ::Type{Point}) end @cached point(x::Int) = Point(x, 2x) -Cached.DiskCacheStyle(::typeof(point), ::Int) = DiskCache(; serializer = MySerializer) +MemoizationKit.DiskCacheStyle(::typeof(point), ::Int) = DiskCache(; serializer = MySerializer) point(4) empty_caches!(point) p = point(4) # read with the custom serializer -@assert only(filter(p -> p.first === point, Cached.disk_cache_stats())).second.hits == 1 # hide +@assert only(filter(p -> p.first === point, MemoizationKit.disk_cache_stats())).second.hits == 1 # hide (p.x, p.y) ``` @@ -174,14 +174,14 @@ only(disk_cache_info(expensive)).second.entries ``` `disk_cache_info(MyPackage)` lists disk caches of functions owned by a module and its submodules. -[`Cached.disk_cache_stats`](@ref) reports hit/miss counters for this process. +[`MemoizationKit.disk_cache_stats`](@ref) reports hit/miss counters for this process. [`disk_cache_info`](@ref) and [`empty_disk_caches!`](@ref) act on this machine's current database; files of older versions or other machines are left alone. The Disk tab of the [dashboard](dashboard.md) lists the open disk caches with these numbers. ## Precomputed results as an artifact -A package can ship precomputed results, read-only, as a [Pkg artifact](https://pkgdocs.julialang.org/v1/artifacts/): fill the disk cache, export it with [`export_disk_cache`](@ref), and point [`Cached.disk_artifact`](@ref) at the artifact. +A package can ship precomputed results, read-only, as a [Pkg artifact](https://pkgdocs.julialang.org/v1/artifacts/): fill the disk cache, export it with [`export_disk_cache`](@ref), and point [`MemoizationKit.disk_artifact`](@ref) at the artifact. ```@example disk using Pkg.Artifacts @@ -196,24 +196,24 @@ readdir(artifact_path(artifact_hash)) # the exported database Point the function at the exported artifact, then empty RAM and the node database: ```@example disk -Cached.disk_artifact(::typeof(expensive)) = artifact_path(artifact_hash) +MemoizationKit.disk_artifact(::typeof(expensive)) = artifact_path(artifact_hash) try # hide empty_caches!(expensive) empty_disk_caches!(expensive) -Base.get_extension(Cached, :CachedSQLiteExt).close_all() # reopen with the artifact # hide +Base.get_extension(MemoizationKit, :MemoizationKitSQLiteExt).close_all() # reopen with the artifact # hide before = computations[] result = expensive(2) @assert computations[] == before # hide println((; result, new_computations = computations[] - before)) finally # hide -Base.get_extension(Cached, :CachedSQLiteExt).close_all() # hide +Base.get_extension(MemoizationKit, :MemoizationKitSQLiteExt).close_all() # hide filter!(!=(diskenv), LOAD_PATH) # hide end # hide nothing # hide ``` To ship the artifact, archive, upload, and bind it in your package's `Artifacts.toml`. -Its package definition can then return `artifact"expensive"` from `Cached.disk_artifact`. +Its package definition can then return `artifact"expensive"` from `MemoizationKit.disk_artifact`. A call then looks in RAM, the artifact, and this machine's database, in that order, and only then computes. New results go to the database; the artifact is never written, and holds the results of one version of `f`. diff --git a/docs/src/eviction.md b/docs/src/eviction.md index 6308b3f..c548435 100644 --- a/docs/src/eviction.md +++ b/docs/src/eviction.md @@ -1,11 +1,11 @@ # Eviction policies ```@meta -CurrentModule = Cached +CurrentModule = MemoizationKit ``` When a RAM cache reaches its size limit, it removes entries to make room for new results. -Cached provides two policies: Clock (the default) and least recently used (LRU). +MemoizationKit provides two policies: Clock (the default) and least recently used (LRU). Both work with shared and task-local caches, and with limits in entries or measured value size. ## Clock: second chances @@ -19,7 +19,7 @@ New entries start with a clear bit: they get a second chance only after a cache For example, fill a three-entry cache with A, B, and C, then hit A. Inserting D starts the hand at A, clears A's bit, and evicts B, whose bit is clear. ```@example clockcache -using Cached +using MemoizationKit cache = ClockCache{Symbol, Int}(; maxsize = 3) for (key, value) in zip((:A, :B, :C), 1:3) @@ -45,7 +45,7 @@ When space is needed, the least recently used entry is removed. For example, fill a three-entry cache with A, B, and C, then hit A. The order from least to most recently used becomes B, C, A, so inserting D evicts B. ```@example lru -using Cached +using MemoizationKit cache = LRU{Symbol, Int}(; maxsize = 3) for (key, value) in zip((:A, :B, :C), 1:3) @@ -65,12 +65,12 @@ Neither policy guarantees a better hit rate for every workload; use the [dashboa ## Selecting a policy ```@example policy -using Cached +using MemoizationKit @cached expensive(x) = x^2 @cached other(x) = 2x -Cached.CacheStyle(::typeof(expensive), args...) = GlobalCache{LRU}() -Cached.CacheStyle(::typeof(other), args...) = TaskLocalCache{ClockCache}() +MemoizationKit.CacheStyle(::typeof(expensive), args...) = GlobalCache{LRU}() +MemoizationKit.CacheStyle(::typeof(other), args...) = TaskLocalCache{ClockCache}() (CacheStyle(expensive, 3), CacheStyle(other, 3)) ``` diff --git a/docs/src/index.md b/docs/src/index.md index acac4ea..65d76bc 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,32 +1,32 @@ -# Cached.jl +# MemoizationKit.jl ```@meta -CurrentModule = Cached +CurrentModule = MemoizationKit ``` -Cached reuses the results of Julia functions called with the same arguments. +MemoizationKit reuses the results of Julia functions called with the same arguments. It is useful when repeated computations are expensive and their caches need limits, monitoring, or persistence. - [`@cached`](@ref) keeps ordinary Julia method signatures, including defaults, keywords, varargs, and `where` parameters. - [`CacheStyle`](@ref) selects shared, task-local, or uncached execution per function and argument type. - Built-in [`ClockCache`](@ref) and [`LRU`](@ref) containers bound storage in entries or bytes. With concrete keys and an inferred concrete return type, RAM hits do not allocate. -- [`Cached.cachekey`](@ref) maps equivalent inputs to a shared result; see [Custom cache keys](keys.md). +- [`MemoizationKit.cachekey`](@ref) maps equivalent inputs to a shared result; see [Custom cache keys](keys.md). - [Preferences](configuration.md) set defaults per package or function; runtime tools inspect, clear, and resize global caches. - Optional extensions add [disk persistence](disk.md), a [terminal dashboard](dashboard.md), and [timing](timing.md). ## Quick start Requires Julia 1.10 or later. -Add Cached to your environment with Julia's package manager: +Add MemoizationKit to your environment with Julia's package manager: ```julia using Pkg -Pkg.add("Cached") +Pkg.add("MemoizationKit") ``` ```@example fib -using Cached +using MemoizationKit @cached function fib(n::Int)::BigInt n < 2 ? BigInt(n) : fib(n - 1) + fib(n - 2) @@ -38,7 +38,7 @@ fib(100) Calling `fib(100)` again reuses the stored result. By default, each function gets a shared Clock cache with room for 10,000 entries across all its cached methods. -## [Using Cached](@id usage) +## [Using MemoizationKit](@id usage) ### Memoizing a method @@ -46,7 +46,7 @@ Put [`@cached`](@ref) before a method definition. By default, its positional and keyword arguments form the cache key, and its body runs only on a miss. ```@example powers -using Cached +using MemoizationKit @cached function powers(x::T, n::Int = 3; offset::T = zero(T))::Vector{T} where {T} [x^i + offset for i in 1:n] @@ -60,7 +60,7 @@ Anonymous functions are not supported. Methods without `@cached` keep their usual behavior. A return annotation is optional and may depend on `where` parameters. -Without one, Cached uses the inferred return type, falling back to `Any` if it is not concrete. +Without one, MemoizationKit uses the inferred return type, falling back to `Any` if it is not concrete. A type-stable body keeps calls inferred even though the shared cache holds results of different types. Use [`uncached`](@ref) to call the body without reading or filling RAM or disk caches. @@ -85,10 +85,10 @@ A strategy based on types can be resolved by the compiler. For example, give each task its own cache for a recursive computation: ```@example tasklocal -using Cached +using MemoizationKit @cached fib(n::Int)::BigInt = n < 2 ? BigInt(n) : fib(n - 1) + fib(n - 2); -Cached.CacheStyle(::typeof(fib), ::Int) = TaskLocalCache{LRU}(); +MemoizationKit.CacheStyle(::typeof(fib), ::Int) = TaskLocalCache{LRU}(); fib(10) ``` @@ -96,7 +96,7 @@ fib(10) To skip caching floating-point calls to `powers`: ```@example powers -Cached.CacheStyle(::typeof(powers), ::AbstractFloat, ::Int) = NoCache(); +MemoizationKit.CacheStyle(::typeof(powers), ::AbstractFloat, ::Int) = NoCache(); powers(2.0; offset = 1.0) ``` @@ -119,10 +119,10 @@ Normally, one cache holds all cached methods and argument types of a function. If its strategy selects several container types, each gets a separate cache with the function's size limit. ```@example management -using Cached +using MemoizationKit module MyPackage - using Cached + using MemoizationKit @cached powers(x) = [x^i for i in 1:3] end @@ -141,7 +141,7 @@ cache_info(MyPackage.powers) Switch to a byte limit, or clear a module's caches: ```@example management -set_cache_size!(MyPackage.powers, 2^20; by = Cached.cachesize) +set_cache_size!(MyPackage.powers, 2^20; by = MemoizationKit.cachesize) MyPackage.powers(2) empty_caches!(MyPackage) # keep hit/miss counters cache_info(MyPackage) @@ -165,17 +165,17 @@ See [Configuration](configuration.md) for persistent defaults and custom size me Copy a mutable result before changing it. - Use caching for computations determined by their arguments. Changes to external state or method definitions (including through Revise) do not automatically invalidate results. - Clear the global cache with `empty_caches!`; for disk results, update [`Cached.diskversion`](@ref). + Clear the global cache with `empty_caches!`; for disk results, update [`MemoizationKit.diskversion`](@ref). - Built-in shared caches are thread-safe. Computations run outside the cache lock, allowing recursion; simultaneous misses may compute the same key more than once. Exceptions are propagated without storing a result. -Use [`Cached.cachekey`](@ref) to map equivalent inputs to a common key, or [`Hashed`](@ref) to customize hashing and equality. +Use [`MemoizationKit.cachekey`](@ref) to map equivalent inputs to a common key, or [`Hashed`](@ref) to customize hashing and equality. See [Custom cache keys](keys.md) for examples and the requirements for sharing results. -## [Why Cached?](@id why-cached) +## [Why MemoizationKit?](@id why-memoizationkit) -Cached is for memoization that needs ongoing management: bounded RAM caches, reuse across runs, and tools to inspect and adjust caches while a program runs. +MemoizationKit is for memoization that needs ongoing management: bounded RAM caches, reuse across runs, and tools to inspect and adjust caches while a program runs. Strategies, settings, persistence, and monitoring work together under one function-level API. | Package | Choose it when | Tradeoff | @@ -183,17 +183,17 @@ Strategies, settings, persistence, and monitoring work together under one functi | [Memoize.jl](https://github.com/JuliaCollections/Memoize.jl#usage) | You want a small `@memoize` API with a dictionary of your choice. | Limits and statistics depend on the container; persistence, a dashboard, and package settings require additional integration. | | [Memoization.jl](https://github.com/marius311/Memoization.jl#readme) | You need to memoize individual calls or closures, as well as method definitions. | Its default cache is unbounded and not thread-safe; custom containers can address those needs, but management and persistence tools require additional integration. | | [LRUCache.jl](https://github.com/JuliaCollections/LRUCache.jl#readme) | You want a thread-safe cache dictionary with limits, statistics, resizing, and eviction callbacks. | You write the function wrapper and management code, or combine it with a memoization macro. | -| Cached.jl | You want bounded caches, per-type strategies, disk persistence, and live monitoring under one API. | The macro requires a method definition; shared caches use locks, and disk results need explicit versioning and cleanup. | +| MemoizationKit.jl | You want bounded caches, per-type strategies, disk persistence, and live monitoring under one API. | The macro requires a method definition; shared caches use locks, and disk results need explicit versioning and cleanup. | -Memoize.jl and Memoization.jl both accept LRUCache.jl containers, so bounded memory and statistics are available without Cached. +Memoize.jl and Memoization.jl both accept LRUCache.jl containers, so bounded memory and statistics are available without MemoizationKit. Memoization.jl also preserves return-type inference. For a single in-memory cache, these packages may already cover your needs. **Key matching:** Memoize.jl and Memoization.jl default to identity-based `IdDict` caches and allow other dictionaries. -Cached defaults to value-based matching with distinct entries for different argument types; [custom keys](keys.md) can merge equivalent inputs. +MemoizationKit defaults to value-based matching with distinct entries for different argument types; [custom keys](keys.md) can merge equivalent inputs. Hashing large keys can cost more than recomputing. -**Invalidation:** Cached requires manual clearing after code or external state changes. +**Invalidation:** MemoizationKit requires manual clearing after code or external state changes. Memoization.jl handles method redefinition for memoized definitions, though memoized individual calls require clearing; see its [limitations](https://github.com/marius311/Memoization.jl#limitations). Those limitations also describe thread safety for top-level functions with a thread-safe container, but not closures or callable objects. diff --git a/docs/src/interface.md b/docs/src/interface.md index c58bb72..39f7988 100644 --- a/docs/src/interface.md +++ b/docs/src/interface.md @@ -1,19 +1,19 @@ # Implementing a cache ```@meta -CurrentModule = Cached +CurrentModule = MemoizationKit ``` -A [`Cached.AbstractCache`](@ref) is the container behind [`GlobalCache`](@ref) and [`TaskLocalCache`](@ref). -Besides [`LRU`](@ref) and [`ClockCache`](@ref), any subtype `C{K, V} <: Cached.AbstractCache{K, V}` implementing these methods can be used: +A [`MemoizationKit.AbstractCache`](@ref) is the container behind [`GlobalCache`](@ref) and [`TaskLocalCache`](@ref). +Besides [`LRU`](@ref) and [`ClockCache`](@ref), any subtype `C{K, V} <: MemoizationKit.AbstractCache{K, V}` implementing these methods can be used: | Method | Contract | | :----- | :------- | | `C{K, V}(; maxsize, by)` | An empty cache. `maxsize` bounds the number of entries, or the sum of `by(value)` if `by !== nothing`. | | `get!(default, c, key)` | The value stored for `key`, or else `default()`, stored if it fits. Keys of different types are different entries. | | `empty!(c)` | Remove all entries, keeping the statistics. | -| [`resize!(c; maxsize)`](@ref resize!(::Cached.AbstractCache)) | Set the size limit, evicting entries until they fit. | -| [`Cached.cache_stats(c)`](@ref) | `(; hits, misses, length, currentsize, maxsize, by)` | +| [`resize!(c; maxsize)`](@ref resize!(::MemoizationKit.AbstractCache)) | Set the size limit, evicting entries until they fit. | +| [`MemoizationKit.cache_stats(c)`](@ref) | `(; hits, misses, length, currentsize, maxsize, by)` | They may be called from any task, so they must be thread-safe, and `get!` must not hold a lock while it calls `default`, which may recurse into the same cache or throw. `show` is derived from `cache_stats`; the other `AbstractDict` methods are optional. @@ -21,9 +21,9 @@ They may be called from any task, so they must be thread-safe, and `get!` must n ## Example: first in, first out ```jldoctest fifo -using Cached +using MemoizationKit -mutable struct FIFO{K, V} <: Cached.AbstractCache{K, V} +mutable struct FIFO{K, V} <: MemoizationKit.AbstractCache{K, V} const entries::Dict{Any, Tuple{V, Int}} # (typeof(key), key) => (value, size) const order::Vector{Any} # keys of `entries`, oldest first const lock::ReentrantLock @@ -64,11 +64,11 @@ end Base.empty!(c::FIFO) = @lock c.lock (empty!(c.entries); empty!(c.order); c.currentsize = 0; c) Base.resize!(c::FIFO; maxsize::Integer) = @lock c.lock (c.maxsize = maxsize; evict!(c)) -Cached.cache_stats(c::FIFO) = +MemoizationKit.cache_stats(c::FIFO) = @lock c.lock (; c.hits, c.misses, length = length(c.entries), c.currentsize, c.maxsize, c.by) @cached square(x) = x^2 -Cached.CacheStyle(::typeof(square), x) = GlobalCache{FIFO}() +MemoizationKit.CacheStyle(::typeof(square), x) = GlobalCache{FIFO}() square.(1:3); square(3); square(3.0) set_cache_size!(square, 2) # evicts square(1) and square(2) diff --git a/docs/src/keys.md b/docs/src/keys.md index 029156f..d854c8a 100644 --- a/docs/src/keys.md +++ b/docs/src/keys.md @@ -1,11 +1,11 @@ # Custom cache keys ```@meta -CurrentModule = Cached +CurrentModule = MemoizationKit ``` A computation may depend on only part of its input: an array's shape, a tensor space's sector structure, or a normalized name. -Specialize [`Cached.cachekey`](@ref) to let equivalent inputs share a cached result. +Specialize [`MemoizationKit.cachekey`](@ref) to let equivalent inputs share a cached result. The function body, return-type inference, [`CacheStyle`](@ref), and [`DiskCacheStyle`](@ref) still use the original arguments. ## The default key @@ -13,9 +13,9 @@ The function body, return-type inference, [`CacheStyle`](@ref), and [`DiskCacheS Without an overload, the key contains every positional argument and, when present, the keyword `NamedTuple`: ```jldoctest -using Cached +using MemoizationKit -(Cached.cachekey(identity, 1, 2), Cached.cachekey(identity, 1, 2; scale = 3)) +(MemoizationKit.cachekey(identity, 1, 2), MemoizationKit.cachekey(identity, 1, 2; scale = 3)) # output @@ -32,10 +32,10 @@ A canonical key contains the properties that determine the result. This example computes the same allocation shape for any input of a given length: ```jldoctest canonical-key -using Cached +using MemoizationKit @cached allocation_shape(x; copies = 1) = (length(x), copies) -Cached.cachekey(::typeof(allocation_shape), x; copies = 1) = (length(x), copies) +MemoizationKit.cachekey(::typeof(allocation_shape), x; copies = 1) = (length(x), copies) a = allocation_shape([1, 2]) b = allocation_shape((3, 4)) @@ -47,7 +47,7 @@ b = allocation_shape((3, 4)) ``` The vector and tuple share an entry because both calls produce the key `(2, 1)`. -Cached does not retain the original argument types alongside a custom key. +MemoizationKit does not retain the original argument types alongside a custom key. Keywords that affect the result belong in the key too: ```jldoctest canonical-key @@ -68,7 +68,7 @@ Returning a compact tuple is often sufficient; no wrapper is required. The same mapping can serve several cached methods of one function: ```jldoctest shared-methods -using Cached +using MemoizationKit computations = Ref(0) @@ -80,7 +80,7 @@ end computations[] += 1 return length(x) end -Cached.cachekey(::typeof(nitems), x) = length(x) +MemoizationKit.cachekey(::typeof(nitems), x) = length(x) a = nitems([1, 2]) b = nitems((3, 4)) @@ -106,13 +106,13 @@ When forming a canonical representation is expensive, [`Hashed`](@ref) keeps the The hash callable takes `(value, seed::UInt)`; the equality callable takes two wrapped values and returns a `Bool`. ```jldoctest hashed-key -using Cached +using MemoizationKit lengthhash(x, seed::UInt) = hash(length(x), seed) lengthequal(x, y) = isequal(length(x), length(y)) @cached wrapped_shape(x; copies = 1) = (length(x), copies) -Cached.cachekey(::typeof(wrapped_shape), x; copies = 1) = +MemoizationKit.cachekey(::typeof(wrapped_shape), x; copies = 1) = (Hashed(x, lengthhash, lengthequal), copies) a = wrapped_shape([1, 2]) @@ -147,7 +147,7 @@ Two wrappers compare equal only when both callables are identical under `===`. Closures with different captured settings therefore remain separate even if they have the same function type. The wrapper's type includes the wrapped value type. -Consequently, wrappers around a vector and a tuple do not share an entry in Cached's RAM containers. +Consequently, wrappers around a vector and a tuple do not share an entry in MemoizationKit's RAM containers. Return a canonical key of a common type when sharing across those inputs is needed. ## Choosing valid equality @@ -162,7 +162,7 @@ For tolerance-based grouping, define explicit buckets and use the bucket identif Keep every property used by hashing or equality stable while its key is stored. A `Hashed` wrapper retains its input rather than copying it, so mutating relevant fields can invalidate the cache's lookup assumptions. A canonical key made from immutable properties avoids that dependency. -Cached results themselves are shared objects; mutations of a cached result are visible to other callers sharing its key. +MemoizationKit results themselves are shared objects; mutations of a cached result are visible to other callers sharing its key. ## RAM and disk caching @@ -178,5 +178,5 @@ To share them, return a canonical key that serializes to the same bytes for equi Disk caching still needs SQLite.jl and a [`DiskCacheStyle`](@ref) overload; see [Disk caching](disk.md). After changing a key mapping, call [`empty_caches!`](@ref) for the function. -If it uses disk caching, also change [`Cached.diskversion`](@ref) or empty its disk cache so entries created under the previous mapping are no longer used. +If it uses disk caching, also change [`MemoizationKit.diskversion`](@ref) or empty its disk cache so entries created under the previous mapping are no longer used. [`uncached`](@ref) bypasses the key hook and all caches; with positional defaults, supply the full arguments to the implementation. diff --git a/docs/src/reference.md b/docs/src/reference.md index 3087d81..1884dfb 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -1,17 +1,17 @@ # Reference ```@meta -CurrentModule = Cached +CurrentModule = MemoizationKit ``` ## Cache containers ```@docs -Cached.AbstractCache +MemoizationKit.AbstractCache LRU ClockCache -resize!(::Cached.AbstractCache) -Cached.cache_stats +resize!(::MemoizationKit.AbstractCache) +MemoizationKit.cache_stats ``` ## Caching functions @@ -19,7 +19,7 @@ Cached.cache_stats ```@docs @cached uncached -Cached.implementation +MemoizationKit.implementation ``` ## Custom cache keys @@ -27,7 +27,7 @@ Cached.implementation See [Custom cache keys](keys.md) for canonical keys, custom equality, and sharing across methods. ```@docs -Cached.cachekey +MemoizationKit.cachekey Hashed ``` @@ -48,7 +48,7 @@ cache_info empty_caches! set_cache_size! set_cache_preferences! -Cached.cachesize +MemoizationKit.cachesize cache_dashboard ``` @@ -57,10 +57,10 @@ cache_dashboard ```@docs DiskCacheStyle DiskCache -Cached.diskversion -Cached.disk_artifact +MemoizationKit.diskversion +MemoizationKit.disk_artifact disk_cache_info -Cached.disk_cache_stats +MemoizationKit.disk_cache_stats empty_disk_caches! export_disk_cache disable_disk_caches! @@ -70,7 +70,7 @@ enable_disk_caches! ## Hooks and timing ```@docs -Cached.instrument_label +MemoizationKit.instrument_label enable_cache_timers! disable_cache_timers! ``` diff --git a/docs/src/timing.md b/docs/src/timing.md index 060a385..359cbe0 100644 --- a/docs/src/timing.md +++ b/docs/src/timing.md @@ -1,7 +1,7 @@ # Timing ```@meta -CurrentModule = Cached +CurrentModule = MemoizationKit ``` With [TimerOutputs.jl](https://github.com/KristofferC/TimerOutputs.jl) loaded, [`enable_cache_timers!`](@ref) records the time spent in the cached functions of a package, in sections per function: @@ -11,7 +11,7 @@ With [TimerOutputs.jl](https://github.com/KristofferC/TimerOutputs.jl) loaded, [ - `disk f`, for functions with a [disk cache](disk.md), wraps the disk lookup after a RAM miss, with `compute f` nested in it when the disk misses too. ```@example timer -using Cached, TimerOutputs +using MemoizationKit, TimerOutputs @cached weights(a, b, c) = a + b * c @cached matrix(n::Int)::Matrix{Float64} = zeros(n, n) @@ -37,12 +37,12 @@ For instance, if `MyPackage` adds `@cached` methods to a function `OtherPackage. A package that wants all of its cached functions timed enables the packages that own them: ```@example owners -using Cached, TimerOutputs # hide +using MemoizationKit, TimerOutputs # hide module MyPackage # hide function enable_timers! end # hide end # hide module OtherPackage # hide - using Cached # hide + using MemoizationKit # hide @cached f(x) = x^2 # hide end # hide TIMER = TimerOutput() # hide @@ -58,9 +58,9 @@ disable_cache_timers!(OtherPackage) TIMER ``` -The labels come from [`Cached.instrument_label`](@ref), `"lookup f"` and `"compute f"` by default; overload it to pick your own: +The labels come from [`MemoizationKit.instrument_label`](@ref), `"lookup f"` and `"compute f"` by default; overload it to pick your own: ```@example timer -Cached.instrument_label(::typeof(weights), ::Val{:lookup}) = "cache: weights" -Cached.instrument_label(weights, Val(:lookup)) +MemoizationKit.instrument_label(::typeof(weights), ::Val{:lookup}) = "cache: weights" +MemoizationKit.instrument_label(weights, Val(:lookup)) ``` diff --git a/ext/CachedTimerOutputsExt.jl b/ext/CachedTimerOutputsExt.jl deleted file mode 100644 index deff3c4..0000000 --- a/ext/CachedTimerOutputsExt.jl +++ /dev/null @@ -1,27 +0,0 @@ -module CachedTimerOutputsExt - -# Timing of cached functions with TimerOutputs, per module owning them, see -# `Cached.enable_cache_timers!`. Enabling evaluates a method of `Cached.instrument` for the owner -# into this module, recompiling the callers of its functions; disabling deletes it again, so -# that the default `thunk()` compiles away as before. - -using Cached: Cached, instrument, instrument_label -using TimerOutputs: TimerOutput, timeit, get_defaulttimer - -function Cached.enable_cache_timers!(M::Module, timer::TimerOutput = get_defaulttimer()) - ccall(:jl_generating_output, Cint, ()) == 1 && - error("`enable_cache_timers!` cannot be used during precompilation") - # replacing the method by overwriting it would bring back the old one on deletion - Cached.disable_cache_timers!(M) - @eval Cached.instrument(f, phase, thunk, ::Val{$(fullname(Cached._owner(M)))}) = - timeit(thunk, $timer, instrument_label(f, phase)) - return nothing -end - -function Cached.disable_cache_timers!(M::Module) - m = which(instrument, Tuple{Any, Any, Any, Val{fullname(Cached._owner(M))}}) - m.module === CachedTimerOutputsExt && Base.delete_method(m) - return nothing -end - -end # module CachedTimerOutputsExt diff --git a/ext/CachedSQLiteExt.jl b/ext/MemoizationKitSQLiteExt.jl similarity index 88% rename from ext/CachedSQLiteExt.jl rename to ext/MemoizationKitSQLiteExt.jl index 5005b60..a7fa476 100644 --- a/ext/CachedSQLiteExt.jl +++ b/ext/MemoizationKitSQLiteExt.jl @@ -1,11 +1,11 @@ -module CachedSQLiteExt +module MemoizationKitSQLiteExt # The disk caches of `DiskCache` (see `docs/src/disk.md`): per function, an optional read-only # artifact and one SQLite database per node, in WAL mode, shared by the processes of the node. # Nodes write separate files, as WAL needs shared memory, which network file systems do not # give across nodes. Disk errors never fail a call: they warn once and the call goes on. -using Cached: Cached, DiskCache, _compute, _fname, _owner, _ispackage, _within, instrument, +using MemoizationKit: MemoizationKit, DiskCache, _compute, _fname, _owner, _ispackage, _within, instrument, _resolve_settings, _cached_section, _package_section, get_scratch!, sha256 using SQLite: SQLite, DBInterface using Serialization: serialize, deserialize, writeheader @@ -88,11 +88,11 @@ const LOCK = ReentrantLock() _sanitize(s) = replace(string(s), r"[^A-Za-z0-9_.]" => c -> join("%" * string(b; base = 16, pad = 2) for b in codeunits(c))) # `.-v`: the file name of an artifact, followed by `-` on a node. -storename(f) = string(_sanitize(join((fullname(parentmodule(typeof(f)))..., _fname(f)), '.')), "-v", _sanitize(Cached.diskversion(f))) +storename(f) = string(_sanitize(join((fullname(parentmodule(typeof(f)))..., _fname(f)), '.')), "-v", _sanitize(MemoizationKit.diskversion(f))) -# Without the `disk_path` preference: a scratch space of the package owning `f`, or of Cached. -diskdir(f, path) = !isempty(path) ? path : _ispackage(_owner(f)) ? get_scratch!(_owner(f), "Cached") : - get_scratch!(Cached, string(nameof(_owner(f)))) +# Without the `disk_path` preference: a scratch space of the package owning `f`, or of MemoizationKit. +diskdir(f, path) = !isempty(path) ? path : _ispackage(_owner(f)) ? get_scratch!(_owner(f), "MemoizationKit") : + get_scratch!(MemoizationKit, string(nameof(_owner(f)))) function stores(f::F) where {F} s = @lock LOCK get(STORES, F, nothing) @@ -106,7 +106,7 @@ end function open_stores(f) settings = _resolve_settings(_fname(f), _cached_section(), _package_section(f)) settings.disk || return Stores(f, nothing, nothing) - dir = Cached.disk_artifact(f) + 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") path = joinpath(diskdir(f, settings.disk_path), string(storename(f), '-', _sanitize(gethostname()), ".sqlite")) return Stores(f, artifact, attempt(() -> Store(path), "open the disk cache of `$(_fname(f))` at $path")) @@ -123,7 +123,7 @@ function attempt(f, msg) f() catch e e isa InterruptException && rethrow() - msg === nothing || @warn "Cached: cannot $msg; continuing without it" exception = e maxlog = 1 _id = Symbol(msg) + msg === nothing || @warn "MemoizationKit: cannot $msg; continuing without it" exception = e maxlog = 1 _id = Symbol(msg) nothing end end @@ -138,7 +138,7 @@ end # ---- the lookup ---- -function Cached.disk_lookup(f::F, ::DiskCache{S}, ::Type{V}, key, args, kw, o) where {F, S, V} +function MemoizationKit.disk_lookup(f::F, ::DiskCache{S}, ::Type{V}, key, args, kw, o) where {F, S, V} s = stores(f) s.artifact === nothing && s.node === nothing && return _compute(f, o, args, kw) return instrument(f, Val(:disk), () -> lookup(f, s, S, V, key, args, kw, o), o) @@ -173,7 +173,7 @@ selector(f) = (stores(f); s -> s.f isa typeof(f)) selector(m::Module) = s -> _within(parentmodule(typeof(s.f)), m) selected(x) = (select = selector(x); [s for s in @lock(LOCK, collect(values(STORES))) if s.node !== nothing && select(s)]) -function Cached.disk_cache_info(x) +function MemoizationKit.disk_cache_info(x) return map(selected(x)) do s path = s.node.db.file entries = @lock s.node.lock scalar(s.node.db, "SELECT count(*) FROM entries")::Int @@ -181,13 +181,13 @@ function Cached.disk_cache_info(x) end end -Cached.disk_cache_stats() = Pair{Any, @NamedTuple{hits::Int, misses::Int}}[ +MemoizationKit.disk_cache_stats() = Pair{Any, @NamedTuple{hits::Int, misses::Int}}[ s.f => (; hits = s.hits[], misses = s.misses[]) for s in @lock(LOCK, collect(values(STORES))) if s.artifact !== nothing || s.node !== nothing ] -Cached.empty_disk_caches!(x) = foreach(s -> @lock(s.node.lock, SQLite.execute(s.node.db, "DELETE FROM entries")), selected(x)) +MemoizationKit.empty_disk_caches!(x) = foreach(s -> @lock(s.node.lock, SQLite.execute(s.node.db, "DELETE FROM entries")), selected(x)) -function Cached.export_disk_cache(f, dir::AbstractString) +function MemoizationKit.export_disk_cache(f, dir::AbstractString) node = stores(f).node node === nothing && throw(ArgumentError("disk caching is disabled for `$(_fname(f))`")) path = joinpath(dir, storename(f) * ".sqlite") @@ -203,4 +203,4 @@ function Cached.export_disk_cache(f, dir::AbstractString) return path end -end # module CachedSQLiteExt +end # module MemoizationKitSQLiteExt diff --git a/ext/CachedTachikomaExt.jl b/ext/MemoizationKitTachikomaExt.jl similarity index 95% rename from ext/CachedTachikomaExt.jl rename to ext/MemoizationKitTachikomaExt.jl index d706024..053788b 100644 --- a/ext/CachedTachikomaExt.jl +++ b/ext/MemoizationKitTachikomaExt.jl @@ -1,6 +1,6 @@ -module CachedTachikomaExt +module MemoizationKitTachikomaExt -# Terminal dashboard of the global caches, see `Cached.cache_dashboard`. +# Terminal dashboard of the global caches, see `MemoizationKit.cache_dashboard`. # # Two tabs: the caches in RAM, and the disk caches (SQLite extension). The model keeps its own # copy of the statistics, re-read from `cache_info`, `cache_stats` and `disk_cache_stats` (which @@ -9,7 +9,7 @@ module CachedTachikomaExt # caches on a background task while the Disk tab is shown. Actions (empty, resize) call the # locked public API and refresh immediately; they act on RAM only. -using Cached: Cached, AbstractCache, cache_info, cache_stats, set_cache_size! +using MemoizationKit: MemoizationKit, AbstractCache, cache_info, cache_stats, set_cache_size! using Tachikoma: Tachikoma, Model, Frame, KeyEvent, Rect, Layout, Vertical, Fixed, Fill, split_layout, render, set_string!, set_char!, tstyle, center, right, Block, StatusBar, Span, Sparkline, Gauge, Paragraph, TextInput, TabBar, handle_key!, text, app @@ -106,7 +106,7 @@ end Tachikoma.should_quit(m::Dashboard) = m.quit -function Cached.cache_dashboard(; interval::Real = 1.0, filter::AbstractString = "") +function MemoizationKit.cache_dashboard(; interval::Real = 1.0, filter::AbstractString = "") m = Dashboard(; interval, filter) refresh!(m) app(m; fps = 20) @@ -127,7 +127,7 @@ function refresh!(m::Dashboard, now = time()) m.tabs[1].rows = map(cache_info()) do (f, c) st = cache_stats(c) s = STATS(st[fieldnames(STATS)]) - kind = (c isa Cached.ClockCache ? "Clock" : string(nameof(typeof(c)))) * (any(d -> first(d) === f, disk) ? "+disk" : "") + kind = (c isa MemoizationKit.ClockCache ? "Clock" : string(nameof(typeof(c)))) * (any(d -> first(d) === f, disk) ? "+disk" : "") Row(f, c, repr(f), kind, st.by !== nothing, s, trend!(trends, m.trends, c, s, elapsed)) end m.tabs[2].rows = map(disk) do (f, d) @@ -139,8 +139,8 @@ function refresh!(m::Dashboard, now = time()) end # Without the SQLite extension there are no disk caches. -hassqlite() = applicable(Cached.disk_cache_stats) -disk_stats() = hassqlite() ? Cached.disk_cache_stats() : Pair{Any, @NamedTuple{hits::Int, misses::Int}}[] +hassqlite() = applicable(MemoizationKit.disk_cache_stats) +disk_stats() = hassqlite() ? MemoizationKit.disk_cache_stats() : Pair{Any, @NamedTuple{hits::Int, misses::Int}}[] function trend!(trends, old, c, s, elapsed) t = get(old, c, nothing) @@ -194,7 +194,7 @@ end function read_diskinfo(f) return try - info = Cached.disk_cache_info(f) + info = MemoizationKit.disk_cache_info(f) isempty(info) ? nothing : last(first(info)) catch nothing @@ -274,7 +274,7 @@ end # The limit is set for the function, so that it persists, in its current measure # (`set_cache_size!` would discard the cache if `by` changed). function resize_row!(m::Dashboard, r::Row, n::Int) - set_cache_size!(r.f, n; by = Cached.cache_stats(r.cache).by) + set_cache_size!(r.f, n; by = MemoizationKit.cache_stats(r.cache).by) m.message = "set the limit of $(r.label) to $(_short(n, r.bytes))" return refresh!(m) end @@ -339,7 +339,7 @@ function totals(m::Dashboard) stored = any(ismissing, infos) ? "… entries · …" : "$(sum(i -> i.entries, known; init = 0)) entries · $(_short(sum(i -> i.bytes, known; init = 0), true))" hits, misses = sum(r -> r.stats.hits, rows; init = 0), sum(r -> r.stats.misses, rows; init = 0) - off = Cached.DISK_ENABLED[] ? "" : " · disk caches off" + off = MemoizationKit.DISK_ENABLED[] ? "" : " · disk caches off" return "$(length(rows)) disk caches · $stored · $hits hits · $misses misses ($(_percent(hitrate((; hits, misses)))))$off$filt" end @@ -466,4 +466,4 @@ function _short(n::Integer, bytes::Bool) return string(i == 1 || x >= 10 ? round(Int, x) : round(x; digits = 1), units[i]) end -end # module CachedTachikomaExt +end # module MemoizationKitTachikomaExt diff --git a/ext/MemoizationKitTimerOutputsExt.jl b/ext/MemoizationKitTimerOutputsExt.jl new file mode 100644 index 0000000..f91168b --- /dev/null +++ b/ext/MemoizationKitTimerOutputsExt.jl @@ -0,0 +1,27 @@ +module MemoizationKitTimerOutputsExt + +# Timing of cached functions with TimerOutputs, per module owning them, see +# `MemoizationKit.enable_cache_timers!`. Enabling evaluates a method of `MemoizationKit.instrument` for the owner +# into this module, recompiling the callers of its functions; disabling deletes it again, so +# that the default `thunk()` compiles away as before. + +using MemoizationKit: MemoizationKit, instrument, instrument_label +using TimerOutputs: TimerOutput, timeit, get_defaulttimer + +function MemoizationKit.enable_cache_timers!(M::Module, timer::TimerOutput = get_defaulttimer()) + ccall(:jl_generating_output, Cint, ()) == 1 && + error("`enable_cache_timers!` cannot be used during precompilation") + # replacing the method by overwriting it would bring back the old one on deletion + MemoizationKit.disable_cache_timers!(M) + @eval MemoizationKit.instrument(f, phase, thunk, ::Val{$(fullname(MemoizationKit._owner(M)))}) = + timeit(thunk, $timer, instrument_label(f, phase)) + return nothing +end + +function MemoizationKit.disable_cache_timers!(M::Module) + m = which(instrument, Tuple{Any, Any, Any, Val{fullname(MemoizationKit._owner(M))}}) + m.module === MemoizationKitTimerOutputsExt && Base.delete_method(m) + return nothing +end + +end # module MemoizationKitTimerOutputsExt diff --git a/src/Cached.jl b/src/MemoizationKit.jl similarity index 91% rename from src/Cached.jl rename to src/MemoizationKit.jl index 243ab17..b353060 100644 --- a/src/Cached.jl +++ b/src/MemoizationKit.jl @@ -1,4 +1,4 @@ -module Cached +module MemoizationKit export @cached, uncached export CacheStyle, NoCache, GlobalCache, GlobalLRUCache, TaskLocalCache @@ -9,7 +9,7 @@ export cache_dashboard, enable_cache_timers!, disable_cache_timers! export DiskCacheStyle, DiskCache, disk_cache_info, empty_disk_caches!, export_disk_cache export disable_disk_caches!, enable_disk_caches! -# documented API that is used qualified, e.g. by overloading `Cached.cachesize` +# 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")) end @@ -44,4 +44,4 @@ function __init__() return nothing end -end # module Cached +end # module MemoizationKit diff --git a/src/api.jl b/src/api.jl index 4959c8a..a23ff40 100644 --- a/src/api.jl +++ b/src/api.jl @@ -37,7 +37,7 @@ _empty!(caches) = (foreach(empty! ∘ last, caches); nothing) Set the size limit of the global cache of `f`, overriding the preferences (see the configuration docs). The limit applies to the function as a whole, all signatures together. Without `by`, `maxsize` counts entries; otherwise it bounds the sum of `by(value)` over the -entries, e.g. with `by = Cached.cachesize` for bytes. Changing `by` discards the cache of `f`. +entries, e.g. with `by = MemoizationKit.cachesize` for bytes. Changing `by` discards the cache of `f`. """ function set_cache_size!(f, maxsize::Integer; by = nothing) maxsize >= 0 || throw(ArgumentError("maxsize must be non-negative")) @@ -74,7 +74,7 @@ function cache_dashboard end Record the time spent in the cached functions owned by the package `M`, or any of its submodules, in `timer`: a section per function and phase, the lookup (hit or miss) with the -computation of a miss nested in it, labelled by [`Cached.instrument_label`](@ref). This +computation of a miss nested in it, labelled by [`MemoizationKit.instrument_label`](@ref). This includes methods of these functions cached in other packages. Modules outside packages, such as those defined in the REPL, count separately. Enabling `M` again replaces its timer; [`disable_cache_timers!`](@ref) stops. @@ -98,14 +98,14 @@ function disable_cache_timers! end # Functions whose methods come from a package extension, with the package to load. const EXTENSIONS = ( - cache_dashboard => (:CachedTachikomaExt, "Tachikoma"), - enable_cache_timers! => (:CachedTimerOutputsExt, "TimerOutputs"), - disable_cache_timers! => (:CachedTimerOutputsExt, "TimerOutputs"), - disk_lookup => (:CachedSQLiteExt, "SQLite"), - disk_cache_info => (:CachedSQLiteExt, "SQLite"), - disk_cache_stats => (:CachedSQLiteExt, "SQLite"), - empty_disk_caches! => (:CachedSQLiteExt, "SQLite"), - export_disk_cache => (:CachedSQLiteExt, "SQLite"), + cache_dashboard => (:MemoizationKitTachikomaExt, "Tachikoma"), + enable_cache_timers! => (:MemoizationKitTimerOutputsExt, "TimerOutputs"), + disable_cache_timers! => (:MemoizationKitTimerOutputsExt, "TimerOutputs"), + disk_lookup => (:MemoizationKitSQLiteExt, "SQLite"), + disk_cache_info => (:MemoizationKitSQLiteExt, "SQLite"), + disk_cache_stats => (:MemoizationKitSQLiteExt, "SQLite"), + empty_disk_caches! => (:MemoizationKitSQLiteExt, "SQLite"), + export_disk_cache => (:MemoizationKitSQLiteExt, "SQLite"), ) # Without the extension these functions have no methods; say how to get them. Calls with diff --git a/src/cachestyle.jl b/src/cachestyle.jl index 37566c2..50df162 100644 --- a/src/cachestyle.jl +++ b/src/cachestyle.jl @@ -7,8 +7,8 @@ The strategy for a call `f(args...)` is chosen by `CacheStyle(f, args...)`, whic [`GlobalCache()`](@ref GlobalCache). Specialize it to change the strategy per function or argument type: ```julia -Cached.CacheStyle(::typeof(f), x::SmallKey) = NoCache() -Cached.CacheStyle(::typeof(f), x::ThreadedKey) = TaskLocalCache{LRU}() +MemoizationKit.CacheStyle(::typeof(f), x::SmallKey) = NoCache() +MemoizationKit.CacheStyle(::typeof(f), x::ThreadedKey) = TaskLocalCache{LRU}() ``` See also [`NoCache`](@ref), [`GlobalCache`](@ref), [`TaskLocalCache`](@ref). diff --git a/src/call.jl b/src/call.jl index 8124c35..7d994a0 100644 --- a/src/call.jl +++ b/src/call.jl @@ -1,5 +1,5 @@ """ - Cached.implementation(f, args...; kwargs...) + MemoizationKit.implementation(f, args...; kwargs...) The uncached body of a [`@cached`](@ref) function `f`. The macro moves the body of each cached method here, dispatching on `typeof(f)`; call it through [`uncached`](@ref). @@ -26,7 +26,7 @@ _owner(m::Module) = (r = Base.moduleroot(m)) === Main ? m : r @generated _ownerval(::Type{F}) where {F} = :(Val{$(fullname(_owner(parentmodule(F))))}()) """ - Cached.instrument_label(f, ::Val{phase}) -> String + MemoizationKit.instrument_label(f, ::Val{phase}) -> String Label of the `phase` of `f` in the sections recorded by [`enable_cache_timers!`](@ref). Defaults to `"lookup f"` and `"compute f"`, with the name of `f`; overload it to choose your own. @@ -74,11 +74,11 @@ _container(f::F, ::GlobalCache{C}, key, ::Type) where {F, C} = globalcache(f, C{ function _container(f::F, ::TaskLocalCache{C}, key::K, ::Type{V}) where {F, C, K, V} T = _localtype(C, K, V) - table = get!(IdDict{Any, Any}, task_local_storage(), :__Cached_tasklocal__)::IdDict{Any, Any} + table = get!(IdDict{Any, Any}, task_local_storage(), :__MemoizationKit_tasklocal__)::IdDict{Any, Any} return get!(() -> _newlocal(f, T), table, (f, T))::T end -# One untyped cache per function for the containers of Cached; other dictionaries (e.g. `Dict`) +# One untyped cache per function for the containers of MemoizationKit; other dictionaries (e.g. `Dict`) # would box untyped keys, so they get one typed dictionary per key and value type. _localtype(C, K, V) = C isa UnionAll ? (C <: AbstractCache ? C{Any, Any} : C{K, V}) : C # Task-local caches follow the size settings of `f`, but are not registered. diff --git a/src/containers/interface.jl b/src/containers/interface.jl index 5da84a2..e74efc5 100644 --- a/src/containers/interface.jl +++ b/src/containers/interface.jl @@ -3,7 +3,7 @@ Supertype of the containers of [`GlobalCache`](@ref) and [`TaskLocalCache`](@ref). A subtype `C` implements `C{K, V}(; maxsize, by)`, `get!(default, c, key)`, `empty!(c)`, -[`resize!(c; maxsize)`](@ref resize!(::Cached.AbstractCache)) and [`Cached.cache_stats(c)`](@ref); +[`resize!(c; maxsize)`](@ref resize!(::MemoizationKit.AbstractCache)) and [`MemoizationKit.cache_stats(c)`](@ref); see [Implementing a cache](@ref). Other `AbstractDict` methods are optional. [`LRU`](@ref) and [`ClockCache`](@ref) are thread-safe dictionaries that convert keys to `K`. diff --git a/src/disk.jl b/src/disk.jl index 1437030..01a85b8 100644 --- a/src/disk.jl +++ b/src/disk.jl @@ -1,5 +1,5 @@ # Disk caching, a level below the RAM caches (see `docs/src/disk.md`). The core holds the API; -# the stores are implemented by the SQLite extension, `ext/CachedSQLiteExt.jl`. +# the stores are implemented by the SQLite extension, `ext/MemoizationKitSQLiteExt.jl`. """ DiskCacheStyle(f, args...) @@ -10,7 +10,7 @@ The disk caching strategy for the call `f(args...)` of a [`@cached`](@ref) funct `CacheStyle` is `NoCache()`. ```julia -Cached.DiskCacheStyle(::typeof(f), args...) = DiskCache() +MemoizationKit.DiskCacheStyle(::typeof(f), args...) = DiskCache() ``` """ DiskCacheStyle(f, args...) = NoCache() @@ -32,7 +32,7 @@ end DiskCache(; serializer::Type = Serializer) = DiskCache{serializer}() """ - Cached.diskversion(f) -> String + MemoizationKit.diskversion(f) -> String Version of the disk cache of `f`, part of its file name; `"1"` by default. Change it when the stored results of `f` are no longer valid, or no longer readable (e.g. after a Julia update). @@ -40,7 +40,7 @@ stored results of `f` are no longer valid, or no longer readable (e.g. after a J diskversion(f) = "1" """ - Cached.disk_artifact(f) -> Union{Nothing, String} + MemoizationKit.disk_artifact(f) -> Union{Nothing, String} A directory with a read-only disk cache of `f`, made by [`export_disk_cache`](@ref), that is consulted before the cache of the node, e.g. `artifact"results"`. Defaults to `nothing`. @@ -83,7 +83,7 @@ that have been used in this process, as `f => (; path, entries, bytes)` pairs. function disk_cache_info end """ - Cached.disk_cache_stats() -> Vector{Pair{Any, NamedTuple}} + MemoizationKit.disk_cache_stats() -> Vector{Pair{Any, NamedTuple}} The disk lookups in this process of each function whose disk cache is open, as `f => (; hits, misses)` pairs, where a hit is a result read from disk (from the artifact or @@ -105,6 +105,6 @@ function empty_disk_caches! end export_disk_cache(f, dir) -> String Write the disk cache of `f` on this node into `dir`, as a compact read-only database named as -[`Cached.disk_artifact`](@ref) expects it, and return its path. +[`MemoizationKit.disk_artifact`](@ref) expects it, and return its path. """ function export_disk_cache end diff --git a/src/keys.jl b/src/keys.jl index 7962c69..9e3ef80 100644 --- a/src/keys.jl +++ b/src/keys.jl @@ -10,7 +10,7 @@ Two wrappers compare equal only when their hash and equality callables are ident relation, and equivalent values must have equal hashes for every seed. Values and callable state that affect hashing or equality must remain unchanged while stored. -Use [`Cached.cachekey`](@ref) to wrap inputs without changing a cached function's signature. +Use [`MemoizationKit.cachekey`](@ref) to wrap inputs without changing a cached function's signature. Custom equality affects RAM caching; disk caching compares serialized key bytes. """ struct Hashed{T, H, E} @@ -30,26 +30,26 @@ Base.isequal(a::Hashed{<:Any, H, E}, b::Hashed{<:Any, H, E}) where {H, E} = Base.:(==)(a::Hashed, b::Hashed) = isequal(a, b) """ - Cached.cachekey(f, args...; kwargs...) + MemoizationKit.cachekey(f, args...; kwargs...) Return the key used to memoize a call of a [`@cached`](@ref) function. By default it is the positional argument tuple, followed by the keyword `NamedTuple` when nonempty. Specialize this function to map equivalent inputs to a shared key: ```julia -Cached.cachekey(::typeof(f), x; scale = 1) = (length(x), scale) -Cached.cachekey(::typeof(g), x) = (Hashed(x, customhash, customequal),) +MemoizationKit.cachekey(::typeof(f), x; scale = 1) = (length(x), scale) +MemoizationKit.cachekey(::typeof(g), x) = (Hashed(x, customhash, customequal),) ``` The implementation, return-type inference, and cache strategies still receive the original arguments. Custom keys can merge calls to different methods or argument types; all merged -calls must admit the same cached result, including its return type. Cached's RAM containers +calls must admit the same cached result, including its return type. MemoizationKit's RAM containers distinguish key types, so keys intended to share an entry must also have the same type. The key must remain stable while cached. [`uncached`](@ref) bypasses this hook. RAM caches use hashing and equality; disk caches compare serialized bytes, so a canonical representation is needed to share disk entries between different inputs. After changing -the mapping, empty existing RAM caches and update [`Cached.diskversion`](@ref) if using disk. +the mapping, empty existing RAM caches and update [`MemoizationKit.diskversion`](@ref) if using disk. """ cachekey(f, args...; kwargs...) = _key(args, NamedTuple(kwargs)) diff --git a/src/macro.jl b/src/macro.jl index 103f582..ea81257 100644 --- a/src/macro.jl +++ b/src/macro.jl @@ -2,11 +2,11 @@ @cached function f(args...; kwargs...) ... end @cached f(args...; kwargs...) = ... -Memoize a method. The body becomes a method of [`Cached.implementation`](@ref), and `f` keeps +Memoize a method. The body becomes a method of [`MemoizationKit.implementation`](@ref), and `f` keeps its signature (including default values and keyword arguments) but looks up the result in a cache selected by [`CacheStyle`](@ref)`(f, args...)`. -The key is chosen by [`Cached.cachekey`](@ref), which defaults to the tuple of positional +The key is chosen by [`MemoizationKit.cachekey`](@ref), which defaults to the tuple of positional arguments, followed by a `NamedTuple` of the keyword arguments if there are any. Custom keys can merge calls with different argument types. Shared caches hold all key and value types of a function together (one cache per container type). The return type annotation, if @@ -39,7 +39,7 @@ macro cached(ex) def[:args] = args impl = copy(def) - impl[:name] = :($Cached.implementation) + impl[:name] = :($MemoizationKit.implementation) impl[:args] = [selfarg; map(_nodefault, args)] isempty(kwargs) || (impl[:kwargs] = map(_nodefault, kwargs)) diff --git a/src/precompile.jl b/src/precompile.jl index bb0afb4..f33b22f 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -1,7 +1,7 @@ # Keep the example functions private and separate from the public API. module _Precompile -using ..Cached +using ..MemoizationKit using PrecompileTools: @setup_workload, @compile_workload @setup_workload begin @@ -9,22 +9,22 @@ using PrecompileTools: @setup_workload, @compile_workload offset::Int end - Cached.CacheStyle(::Workload{S}, args...) where {S} = S() - Cached.DiskCacheStyle(::Workload{S, D}, args...) where {S, D} = D() + MemoizationKit.CacheStyle(::Workload{S}, args...) where {S} = S() + MemoizationKit.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),) + MemoizationKit.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}() + had_local = haskey(tls, :__MemoizationKit_tasklocal__) + old_local = get(tls, :__MemoizationKit_tasklocal__, nothing) + tls[:__MemoizationKit_tasklocal__] = IdDict{Any, Any}() io = IOBuffer() try @compile_workload begin @@ -43,7 +43,7 @@ using PrecompileTools: @setup_workload, @compile_workload 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}) + for D in (NoCache, DiskCache{MemoizationKit.Serializer}) f = Workload{S, D}(1) set_cache_size!(f, 2) for x in (1, 2, 3, 3) @@ -53,11 +53,11 @@ using PrecompileTools: @setup_workload, @compile_workload f(1.0) uncached(f, 1) uncached(f, 1, 2; scale = 2) - Cached.instrument_label(f, Val(:lookup)) + MemoizationKit.instrument_label(f, Val(:lookup)) cache_info(f) set_cache_size!(f, 1) empty_caches!(f) - set_cache_size!(f, 32; by = Cached.cachesize) + set_cache_size!(f, 32; by = MemoizationKit.cachesize) f(1) end end @@ -75,7 +75,7 @@ using PrecompileTools: @setup_workload, @compile_workload # 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 c in (C{Any, Any}(; maxsize = 2), C{Int, Int}(; maxsize = 2), C{Int, Int}(; maxsize = 16, by = MemoizationKit.cachesize)) for x in (1, 2, 3, 3) get!(Returns(x), c, x) end @@ -86,7 +86,7 @@ using PrecompileTools: @setup_workload, @compile_workload length(c) isempty(c) collect(c) - Cached.cache_stats(c) + MemoizationKit.cache_stats(c) show(io, c) show(io, MIME"text/plain"(), c) delete!(c, 3) @@ -97,13 +97,13 @@ using PrecompileTools: @setup_workload, @compile_workload end end finally - had_local ? (tls[:__Cached_tasklocal__] = old_local) : pop!(tls, :__Cached_tasklocal__, nothing) + had_local ? (tls[:__MemoizationKit_tasklocal__] = old_local) : pop!(tls, :__MemoizationKit_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) + Base.@lock MemoizationKit.REGISTRY.lock begin + for f in collect(keys(MemoizationKit.REGISTRY.functions)) + parentmodule(typeof(f)) === (@__MODULE__) && delete!(MemoizationKit.REGISTRY.functions, f) end - Cached._publish!() + MemoizationKit._publish!() end end end diff --git a/src/preferences.jl b/src/preferences.jl index c7cc753..9ddf62a 100644 --- a/src/preferences.jl +++ b/src/preferences.jl @@ -1,23 +1,23 @@ # Default settings, configurable through Preferences.jl (see `docs/src/configuration.md`). # # Settings are resolved once per function, when its first cache is created: -# runtime calls (`set_cache_size!`, ...) > [.Cached.] > [.Cached] -# > [Cached] > built-in defaults +# runtime calls (`set_cache_size!`, ...) > [.MemoizationKit.] > [.MemoizationKit] +# > [MemoizationKit] > built-in defaults # where is the package that owns the function (`parentmodule(typeof(f))`). -# The default container is a compile-time preference of Cached only, since it selects the +# The default container is a compile-time preference of MemoizationKit only, since it selects the # `CacheStyle` and so must be a constant. const BUILTIN_SETTINGS = (; maxsize = 10_000, measure = "count", disk = true, disk_path = "") const SETTING_KEYS = map(string, keys(BUILTIN_SETTINGS)) """ - Cached.cachesize(x) -> Integer + MemoizationKit.cachesize(x) -> Integer Size in bytes of a cached value `x`, used by caches whose `measure` is `"bytes"`. Defaults to `Base.summarysize(x)`. Overload it for your own types when that is slow (it traverses the whole object) or inaccurate (memory shared between values is counted for each of them): ```julia -Cached.cachesize(t::MyTensor) = sizeof(t.data) +MemoizationKit.cachesize(t::MyTensor) = sizeof(t.data) ``` """ cachesize(x) = Base.summarysize(x) @@ -27,19 +27,19 @@ const MEASURES = Dict{String, Any}("count" => nothing, "bytes" => cachesize) const DEFAULT_CONTAINER = let name = @load_preference("container", "ClockCache") containers = Dict("ClockCache" => ClockCache, "LRU" => LRU) haskey(containers, name) || - error("Cached: invalid preference `container = $(repr(name))`; use \"ClockCache\" or \"LRU\"") + 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 -# `[.Cached]` table (or `nothing`), whose sub-tables are per-function sections. +# `[.MemoizationKit]` table (or `nothing`), whose sub-tables are per-function sections. function _resolve_settings(fname::AbstractString, cached::AbstractDict, package) settings = Dict{String, Any}(string(k) => v for (k, v) in pairs(BUILTIN_SETTINGS)) - _merge_settings!(settings, cached, "[Cached]") + _merge_settings!(settings, cached, "[MemoizationKit]") if package isa AbstractDict - _merge_settings!(settings, package, "[.Cached]") + _merge_settings!(settings, package, "[.MemoizationKit]") section = get(package, fname, nothing) - section isa AbstractDict && _merge_settings!(settings, section, "[.Cached.$fname]") + section isa AbstractDict && _merge_settings!(settings, section, "[.MemoizationKit.$fname]") end return (; maxsize = settings["maxsize"]::Integer, @@ -53,9 +53,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 "Cached: ignoring unknown preference `$k` in $where" + @warn "MemoizationKit: ignoring unknown preference `$k` in $where" elseif !_isvalid(k, v) - @warn "Cached: ignoring invalid preference `$k = $(repr(v))` in $where" + @warn "MemoizationKit: ignoring invalid preference `$k = $(repr(v))` in $where" else settings[k] = v end @@ -77,7 +77,7 @@ _ispackage(mod::Module) = Base.PkgId(mod).uuid !== nothing # not e.g. Main function _package_section(f) mod = _owner(f) - return _ispackage(mod) ? load_preference(mod, "Cached", nothing) : nothing + return _ispackage(mod) ? load_preference(mod, "MemoizationKit", nothing) : nothing end _fname(f) = string(f isa Function ? nameof(f) : nameof(typeof(f))) @@ -96,13 +96,13 @@ _functioncaches!(f) = get!(() -> FunctionCaches(f), REGISTRY.functions, f) set_cache_preferences!(package::Module; settings...) set_cache_preferences!(f; settings...) -Store default cache settings in `LocalPreferences.toml`: globally (section `[Cached]`), for the -functions of `package` (`[.Cached]`), or for the function `f` (`[.Cached.]`, +Store default cache settings in `LocalPreferences.toml`: globally (section `[MemoizationKit]`), for the +functions of `package` (`[.MemoizationKit]`), or for the function `f` (`[.MemoizationKit.]`, where `` is the package that defines `f`). Settings are `maxsize`, `measure` (`"count"` or `"bytes"`), `disk` and `disk_path` (see the disk caching docs), and, globally only, `container` (`"ClockCache"` or `"LRU"`). A value of `nothing` removes the setting. Settings apply to functions whose first cache is created afterwards, so in practice after a -restart; changing `container` recompiles Cached. See the configuration docs for how settings +restart; changing `container` recompiles MemoizationKit. See the configuration docs for how settings combine. ```julia @@ -152,12 +152,12 @@ end _apply!(section, settings) = foreach(((k, v),) -> v === nothing ? delete!(section, string(k)) : (section[string(k)] = v), settings) -# Read-modify-write the `[.Cached]` table. It is deleted first, since `set_preferences!` +# Read-modify-write the `[.MemoizationKit]` table. It is deleted first, since `set_preferences!` # merges tables and would otherwise keep removed keys. function _update_section!(update!, mod::Module) - section = deepcopy(load_preference(mod, "Cached", Dict{String, Any}())) + section = deepcopy(load_preference(mod, "MemoizationKit", Dict{String, Any}())) update!(section) - delete_preferences!(mod, "Cached"; force = true) - isempty(section) || set_preferences!(mod, "Cached" => section; force = true) + delete_preferences!(mod, "MemoizationKit"; force = true) + isempty(section) || set_preferences!(mod, "MemoizationKit" => section; force = true) return nothing end diff --git a/test/Project.toml b/test/Project.toml index b06232a..3479116 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,6 +1,6 @@ [deps] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" -Cached = "1b238080-9255-4fe9-b224-89eb24efe93b" +MemoizationKit = "1b238080-9255-4fe9-b224-89eb24efe93b" ParallelTestRunner = "d3525ed8-44d0-4b2c-a655-542cee43accc" Preferences = "21216c6a-2e73-6563-6e65-726566657250" SQLite = "0aa819cd-b072-5ff4-a722-6bc24af294d9" @@ -10,7 +10,7 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" [sources] -Cached = {path = ".."} +MemoizationKit = {path = ".."} [compat] Aqua = "0.8.9" diff --git a/test/runtests.jl b/test/runtests.jl index 627e396..940d3fe 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,4 @@ -using Cached +using MemoizationKit using ParallelTestRunner -runtests(Cached, ARGS) +runtests(MemoizationKit, ARGS) diff --git a/test/test_aqua.jl b/test/test_aqua.jl index 222146c..ebc0ce5 100644 --- a/test/test_aqua.jl +++ b/test/test_aqua.jl @@ -1,15 +1,15 @@ using Test using Aqua -using Cached +using MemoizationKit @testset "Aqua" begin - Aqua.test_all(Cached) + Aqua.test_all(MemoizationKit) end @static if VERSION >= v"1.11.0-DEV.469" @testset "public API" begin for name in (:AbstractCache, :cache_stats, :cachesize, :implementation, :instrument_label, :diskversion, :disk_artifact, :cachekey) - @test Base.ispublic(Cached, name) + @test Base.ispublic(MemoizationKit, name) end end end diff --git a/test/test_containers.jl b/test/test_containers.jl index 1ec8665..349a10a 100644 --- a/test/test_containers.jl +++ b/test/test_containers.jl @@ -1,6 +1,6 @@ using Test -using Cached -using Cached: AbstractCache, cache_stats +using MemoizationKit +using MemoizationKit: AbstractCache, cache_stats const CACHETYPES = (LRU, ClockCache) diff --git a/test/test_disk.jl b/test/test_disk.jl index bb780c6..caf4792 100644 --- a/test/test_disk.jl +++ b/test/test_disk.jl @@ -1,13 +1,13 @@ using Test -using Cached +using MemoizationKit using Preferences using Serialization using Serialization: AbstractSerializer using SQLite: SQLite, DBInterface -include(joinpath(pkgdir(Cached), "benchmark", "disk_stress.jl")) # `stress`, `use_disk_path` +include(joinpath(pkgdir(MemoizationKit), "benchmark", "disk_stress.jl")) # `stress`, `use_disk_path` -const EXT = Base.get_extension(Cached, :CachedSQLiteExt) +const EXT = Base.get_extension(MemoizationKit, :MemoizationKitSQLiteExt) const DIR = mktempdir() # the disk caches of this file const DIR_ENV = use_disk_path(DIR) @@ -24,12 +24,12 @@ function run_julia(code) end @cached square(x::Int) = (CALLS[] += 1; [x, x^2]) -Cached.DiskCacheStyle(::typeof(square), ::Int) = DiskCache() +MemoizationKit.DiskCacheStyle(::typeof(square), ::Int) = DiskCache() plainsquare(x) = square(x) @testset "RAM, then disk, then compute" begin - @test Cached.DiskCacheStyle(square, 1) === DiskCache() === DiskCache{Serializer}() - @test Cached.DiskCacheStyle(sum, 1) === NoCache() + @test MemoizationKit.DiskCacheStyle(square, 1) === DiskCache() === DiskCache{Serializer}() + @test MemoizationKit.DiskCacheStyle(sum, 1) === NoCache() @test ram_and_count(() -> square(3)) == ([3, 9], 1) # computed, written to disk @test square(3) == [3, 9] && CALLS[] == 1 # RAM hit @test ram_and_count(() -> square(3)) == ([3, 9], 0) # disk hit @@ -37,19 +37,19 @@ plainsquare(x) = square(x) @test allocs(plainsquare, 3) == 0 # the RAM hit path is unchanged info = only(disk_cache_info(square)) @test info.first === square && info.second.entries == 1 && info.second.bytes > 0 - @test Dict(Cached.disk_cache_stats())[square] == (; hits = 1, misses = 1) # lookups on disk + @test Dict(MemoizationKit.disk_cache_stats())[square] == (; hits = 1, misses = 1) # lookups on disk @test endswith(EXT.storename(square), ".square-v1") # `.square-v1` @test path(square) == joinpath(DIR, EXT.storename(square) * "-$(EXT._sanitize(gethostname())).sqlite") end @cached canonical(x) = (CALLS[] += 1; length(x)) -Cached.cachekey(::typeof(canonical), x) = length(x) -Cached.DiskCacheStyle(::typeof(canonical), x) = DiskCache() +MemoizationKit.cachekey(::typeof(canonical), x) = length(x) +MemoizationKit.DiskCacheStyle(::typeof(canonical), x) = DiskCache() @cached wrapped(x) = (CALLS[] += 1; length(x)) -Cached.cachekey(::typeof(wrapped), x) = +MemoizationKit.cachekey(::typeof(wrapped), x) = Hashed(x, (x, seed) -> hash(length(x), seed), (x, y) -> isequal(length(x), length(y))) -Cached.DiskCacheStyle(::typeof(wrapped), x) = DiskCache() +MemoizationKit.DiskCacheStyle(::typeof(wrapped), x) = DiskCache() @testset "custom keys" begin @test ram_and_count(() -> canonical([1, 2])) == (2, 1) @@ -64,8 +64,8 @@ Cached.DiskCacheStyle(::typeof(wrapped), x) = DiskCache() end @cached ondisk(x::Int)::Vector{Int} = (CALLS[] += 1; [x]) -Cached.CacheStyle(::typeof(ondisk), ::Int) = NoCache() -Cached.DiskCacheStyle(::typeof(ondisk), ::Int) = DiskCache() +MemoizationKit.CacheStyle(::typeof(ondisk), ::Int) = NoCache() +MemoizationKit.DiskCacheStyle(::typeof(ondisk), ::Int) = DiskCache() @testset "NoCache in RAM: every call goes to disk" begin n = CALLS[] @@ -80,7 +80,7 @@ struct Scale a::Int end @cached (s::Scale)(x::Int) = (CALLS[] += 1; s.a * x) -Cached.DiskCacheStyle(::Scale, ::Int) = DiskCache() +MemoizationKit.DiskCacheStyle(::Scale, ::Int) = DiskCache() @testset "callable objects are part of the key" begin @test ram_and_count(() -> (Scale(2)(3), Scale(3)(3))) == ((6, 9), 2) @@ -112,10 +112,10 @@ function Serialization.deserialize(s::PointSerializer, ::Type{Point}) end @cached point(x::Int) = (CALLS[] += 1; Point(x, 2x)) -Cached.DiskCacheStyle(::typeof(point), ::Int) = DiskCache(; serializer = PointSerializer) +MemoizationKit.DiskCacheStyle(::typeof(point), ::Int) = DiskCache(; serializer = PointSerializer) @testset "custom serializer" begin - @test Cached.DiskCacheStyle(point, 1) === DiskCache{PointSerializer}() + @test MemoizationKit.DiskCacheStyle(point, 1) === DiskCache{PointSerializer}() @test_throws ArgumentError DiskCache{Int}() @test ram_and_count(() -> point(4)) == (Point(4, 8), 1) @test ram_and_count(() -> point(4)) == (Point(4, 8), 0) @@ -127,8 +127,8 @@ end const VERSION_TAG = Ref("1") @cached versioned(x::Int) = (CALLS[] += 1; x) -Cached.DiskCacheStyle(::typeof(versioned), ::Int) = DiskCache() -Cached.diskversion(::typeof(versioned)) = VERSION_TAG[] +MemoizationKit.DiskCacheStyle(::typeof(versioned), ::Int) = DiskCache() +MemoizationKit.diskversion(::typeof(versioned)) = VERSION_TAG[] @testset "a new version invalidates" begin @test ram_and_count(() -> versioned(1)) == (1, 1) @@ -144,7 +144,7 @@ Cached.diskversion(::typeof(versioned)) = VERSION_TAG[] end @cached fragile(x::Int)::Vector{Int} = (CALLS[] += 1; [x]) -Cached.DiskCacheStyle(::typeof(fragile), ::Int) = DiskCache() +MemoizationKit.DiskCacheStyle(::typeof(fragile), ::Int) = DiskCache() @testset "unreadable entries are recomputed and overwritten" begin @test ram_and_count(() -> fragile(1)) == ([1], 1) @@ -166,7 +166,7 @@ Cached.DiskCacheStyle(::typeof(fragile), ::Int) = DiskCache() end @cached switched(x::Int) = (CALLS[] += 1; x) -Cached.DiskCacheStyle(::typeof(switched), ::Int) = DiskCache() +MemoizationKit.DiskCacheStyle(::typeof(switched), ::Int) = DiskCache() @testset "turning disk caches off" begin disable_disk_caches!() @@ -184,17 +184,17 @@ end @cached prefoff(x::Int) = (CALLS[] += 1; x) @cached prefpath(x::Int) = x @cached unwritable(x::Int) = (CALLS[] += 1; x) -Cached.DiskCacheStyle(::Union{typeof(prefoff), typeof(prefpath), typeof(unwritable)}, ::Int) = DiskCache() +MemoizationKit.DiskCacheStyle(::Union{typeof(prefoff), typeof(prefpath), typeof(unwritable)}, ::Int) = DiskCache() @testset "preferences, and disk errors" begin package = Dict{String, Any}("disk_path" => "/x", "f" => Dict{String, Any}("disk" => false)) - @test Cached._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`") Cached._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 preference `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 env = mktempdir() - write(joinpath(env, "Project.toml"), "[deps]\nCached = \"1b238080-9255-4fe9-b224-89eb24efe93b\"\n") + write(joinpath(env, "Project.toml"), "[deps]\nMemoizationKit = \"1b238080-9255-4fe9-b224-89eb24efe93b\"\n") old = Base.ACTIVE_PROJECT[] Base.ACTIVE_PROJECT[] = joinpath(env, "Project.toml") other = mktempdir() @@ -221,18 +221,18 @@ end @testset "default directory" begin scratch = mktempdir() - Cached.Scratch.with_scratch_directory(scratch) do - @test EXT.diskdir(prefpath, "") == joinpath(scratch, string(Base.PkgId(Cached).uuid), "Main") + MemoizationKit.Scratch.with_scratch_directory(scratch) do + @test EXT.diskdir(prefpath, "") == joinpath(scratch, string(Base.PkgId(MemoizationKit).uuid), "Main") # packages get their own scratch space - @test EXT.diskdir(Preferences.load_preference, "") == joinpath(scratch, string(Base.PkgId(Preferences).uuid), "Cached") + @test EXT.diskdir(Preferences.load_preference, "") == joinpath(scratch, string(Base.PkgId(Preferences).uuid), "MemoizationKit") end end module Owned - using Cached + using MemoizationKit @cached a(x::Int) = x @cached b(x::Int) = -x - Cached.DiskCacheStyle(::Union{typeof(a), typeof(b)}, ::Int) = DiskCache() + MemoizationKit.DiskCacheStyle(::Union{typeof(a), typeof(b)}, ::Int) = DiskCache() end @testset "management per module" begin @@ -246,8 +246,8 @@ end const ARTIFACT = Ref{Union{Nothing, String}}(nothing) @cached shipped(x::Int) = (CALLS[] += 1; x + 0.5) -Cached.DiskCacheStyle(::typeof(shipped), ::Int) = DiskCache() -Cached.disk_artifact(::typeof(shipped)) = ARTIFACT[] +MemoizationKit.DiskCacheStyle(::typeof(shipped), ::Int) = DiskCache() +MemoizationKit.disk_artifact(::typeof(shipped)) = ARTIFACT[] @testset "export to a read-only artifact" begin @test ram_and_count(() -> (shipped(1), shipped(2))) == ((1.5, 2.5), 2) @@ -271,11 +271,11 @@ Cached.disk_artifact(::typeof(shipped)) = ARTIFACT[] end module Timed - using Cached + using MemoizationKit const LOG = Symbol[] @cached f(x::Int) = x - Cached.DiskCacheStyle(::typeof(f), ::Int) = DiskCache() - function Cached.instrument(f, ::Val{phase}, thunk, ::Val{fullname(@__MODULE__)}) where {phase} + MemoizationKit.DiskCacheStyle(::typeof(f), ::Int) = DiskCache() + function MemoizationKit.instrument(f, ::Val{phase}, thunk, ::Val{fullname(@__MODULE__)}) where {phase} push!(LOG, phase) return thunk() end @@ -291,12 +291,12 @@ end @testset "round trip through a fresh process" begin dir = mktempdir() code = """ - using Cached, SQLite - include($(repr(joinpath(pkgdir(Cached), "benchmark", "disk_stress.jl")))) + using MemoizationKit, SQLite + include($(repr(joinpath(pkgdir(MemoizationKit), "benchmark", "disk_stress.jl")))) use_disk_path($(repr(dir))) const calls = Ref(0) @cached roundtrip(x::Int; scale = 1) = (calls[] += 1; (scale * x, string(x))) - Cached.DiskCacheStyle(::typeof(roundtrip), ::Int) = DiskCache() + MemoizationKit.DiskCacheStyle(::typeof(roundtrip), ::Int) = DiskCache() print(roundtrip(1), roundtrip(2; scale = 3), " calls=", calls[]) """ @test run_julia(code) == "(1, \"1\")(6, \"2\") calls=2" @@ -307,9 +307,9 @@ end @testset "error hint without SQLite" begin code = """ - using Cached + using MemoizationKit @cached nosqlite(x::Int) = x - Cached.DiskCacheStyle(::typeof(nosqlite), ::Int) = DiskCache() + MemoizationKit.DiskCacheStyle(::typeof(nosqlite), ::Int) = DiskCache() try nosqlite(1) catch e diff --git a/test/test_instrument.jl b/test/test_instrument.jl index b3b627f..c5e96ca 100644 --- a/test/test_instrument.jl +++ b/test/test_instrument.jl @@ -1,23 +1,23 @@ using Test -using Cached -using Cached: instrument, instrument_label +using MemoizationKit +using MemoizationKit: instrument, instrument_label # A module owning cached functions, instrumented by hand to record the phases module Traced - using Cached + using MemoizationKit const LOG = Symbol[] const calls = Ref(0) @cached f(x) = (calls[] += 1; x + 1) @cached nocache(x) = (calls[] += 1; x) - Cached.CacheStyle(::typeof(nocache), ::Int) = NoCache() + MemoizationKit.CacheStyle(::typeof(nocache), ::Int) = NoCache() @cached tasklocal(x) = (calls[] += 1; x) - Cached.CacheStyle(::typeof(tasklocal), ::Int) = TaskLocalCache{LRU}() + MemoizationKit.CacheStyle(::typeof(tasklocal), ::Int) = TaskLocalCache{LRU}() struct Scale a::Int end @cached (s::Scale)(x) = (calls[] += 1; s.a * x) - function Cached.instrument(f, ::Val{phase}, thunk, ::Val{fullname(@__MODULE__)}) where {phase} + function MemoizationKit.instrument(f, ::Val{phase}, thunk, ::Val{fullname(@__MODULE__)}) where {phase} push!(LOG, Symbol(:enter_, phase)) r = thunk() push!(LOG, Symbol(:leave_, phase)) @@ -33,10 +33,10 @@ const HIT = [:enter_lookup, :leave_lookup] const COMPUTE = [:enter_compute, :leave_compute] @testset "phases nest" begin - @test Cached._ownerval(Traced.Scale) === Val(fullname(Traced)) - @test Cached._owner(Traced) === Traced # outside packages, modules own their functions - @test Cached._owner(Base.Iterators) === Base && Cached._owner(Base) === Base - @test Cached._ownerval(typeof(Base.Iterators.flatten)) === Val((:Base,)) + @test MemoizationKit._ownerval(Traced.Scale) === Val(fullname(Traced)) + @test MemoizationKit._owner(Traced) === Traced # outside packages, modules own their functions + @test MemoizationKit._owner(Base.Iterators) === Base && MemoizationKit._owner(Base) === Base + @test MemoizationKit._ownerval(typeof(Base.Iterators.flatten)) === Val((:Base,)) @test ncalls(() -> Traced.f(1)) == (2, 1) && takelog!() == MISS @test ncalls(() -> Traced.f(1)) == (2, 0) && takelog!() == HIT @test ncalls(() -> uncached(Traced.f, 1)) == (2, 1) && isempty(takelog!()) @@ -59,13 +59,13 @@ end @cached plain(x) = x^2 @cached plain_nocache(x)::Int = x -Cached.CacheStyle(::typeof(plain_nocache), ::Int) = NoCache() +MemoizationKit.CacheStyle(::typeof(plain_nocache), ::Int) = NoCache() allocs(f, x) = (f(x); @allocated f(x)) # in a function, as Julia 1.10 allocates at top level @cached fresh(x::Int) = x # first compiled inside a caller, where Julia 1.10 could box closures sumfresh(xs) = sum(fresh, xs) @testset "default hook costs nothing" begin - @test instrument(plain, Val(:lookup), () -> 1, Cached._ownerval(typeof(plain))) == 1 + @test instrument(plain, Val(:lookup), () -> 1, MemoizationKit._ownerval(typeof(plain))) == 1 @test @inferred(plain(3)) == 9 @test allocs(plain, 3) == 0 @test @inferred(plain_nocache(3)) == 3 @@ -79,13 +79,13 @@ end end @testset "error hints without TimerOutputs" begin - # a fresh process with Cached only, since this one loads TimerOutputs below + # a fresh process with MemoizationKit only, since this one loads TimerOutputs below mktempdir() do env code = """ using Pkg Pkg.activate($(repr(env)); io = devnull) - Pkg.develop(path = $(repr(pkgdir(Cached))); io = devnull) - using Cached + Pkg.develop(path = $(repr(pkgdir(MemoizationKit))); io = devnull) + using MemoizationKit for call in (() -> enable_cache_timers!(Main), () -> disable_cache_timers!(Main)) try call() @@ -109,17 +109,17 @@ end using TimerOutputs: TimerOutputs, TimerOutput module A - using Cached + using MemoizationKit @cached f(x::Int) = x + 1 end module B - using Cached + using MemoizationKit using ..A: A struct BType end @cached A.f(::BType) = 0 # owned by A @cached g(x) = x - Cached.instrument_label(::typeof(g), ::Val{:lookup}) = "my lookup g" + MemoizationKit.instrument_label(::typeof(g), ::Val{:lookup}) = "my lookup g" end # calls of the (nested) section `labels`, 0 if absent @@ -127,7 +127,7 @@ section_calls(to, labels...) = haskey(to, first(labels)) ? TimerOutputs.ncalls(t hit_f() = A.f(1) # methods of `instrument` defined by the extension -ntimed() = count(m -> m.module === Base.get_extension(Cached, :CachedTimerOutputsExt), methods(instrument)) +ntimed() = count(m -> m.module === Base.get_extension(MemoizationKit, :MemoizationKitTimerOutputsExt), methods(instrument)) const to = TimerOutput() hit_f() # compiled before enabling @@ -135,13 +135,13 @@ hit_f() # compiled before enabling enable_cache_timers!(A, to) @testset "timers per owner module" begin - @test Base.get_extension(Cached, :CachedTimerOutputsExt) !== nothing + @test Base.get_extension(MemoizationKit, :MemoizationKitTimerOutputsExt) !== nothing @test A.f(1) == 2 && A.f(2) == 3 && A.f(B.BType()) == 0 && hit_f() == 2 @test section_calls(to, "lookup f") == 4 @test section_calls(to, "lookup f", "compute f") == 2 @test B.g(1) == 1 && !haskey(to, "lookup g") && !haskey(to, "my lookup g") @test @inferred(A.f(1)) == 2 - @test isempty(Test.detect_ambiguities(Cached)) + @test isempty(Test.detect_ambiguities(MemoizationKit)) end const to2 = TimerOutput() diff --git a/test/test_keys.jl b/test/test_keys.jl index f001c7c..35714e0 100644 --- a/test/test_keys.jl +++ b/test/test_keys.jl @@ -1,5 +1,5 @@ using Test -using Cached +using MemoizationKit lenhash(x, seed) = hash(length(x), seed) lenequal(x, y) = isequal(length(x), length(y)) diff --git a/test/test_macro.jl b/test/test_macro.jl index 78c46d1..000ad56 100644 --- a/test/test_macro.jl +++ b/test/test_macro.jl @@ -1,6 +1,6 @@ using Test -using Cached -using Cached: implementation +using MemoizationKit +using MemoizationKit: implementation const calls = Ref(0) counting(x) = (calls[] += 1; x) @@ -43,20 +43,20 @@ end # same name in different modules module ClashA - using Cached + using MemoizationKit @cached f(x) = (:A, x) end module ClashB - using Cached + using MemoizationKit @cached f(x) = (:B, x) end # a module with a submodule, both owning cached functions module Outer - using Cached + using MemoizationKit @cached a(x) = x module Inner - using Cached + using MemoizationKit @cached b(x) = x end end @@ -66,13 +66,13 @@ module Shared fusion(x) = x end module SectorsA - using Cached + using MemoizationKit import ..Shared struct IrrepA end @cached Shared.fusion(::IrrepA) = :A end module SectorsB - using Cached + using MemoizationKit import ..Shared struct IrrepB end @cached Shared.fusion(::IrrepB) = :B @@ -81,24 +81,24 @@ end @cached fib(n::Int) = n <= 2 ? big(1) : fib(n - 1) + fib(n - 2) @cached nocache(x) = counting(x) -Cached.CacheStyle(::typeof(nocache), x::String) = NoCache() +MemoizationKit.CacheStyle(::typeof(nocache), x::String) = NoCache() @cached tasklocal(x) = counting(x) -Cached.CacheStyle(::typeof(tasklocal), x::Int) = TaskLocalCache{LRU}() -Cached.CacheStyle(::typeof(tasklocal), x::Symbol) = TaskLocalCache{Dict}() -Cached.CacheStyle(::typeof(tasklocal), x::Float64) = TaskLocalCache() # default container -Cached.CacheStyle(::typeof(tasklocal), x::String) = GlobalCache() +MemoizationKit.CacheStyle(::typeof(tasklocal), x::Int) = TaskLocalCache{LRU}() +MemoizationKit.CacheStyle(::typeof(tasklocal), x::Symbol) = TaskLocalCache{Dict}() +MemoizationKit.CacheStyle(::typeof(tasklocal), x::Float64) = TaskLocalCache() # default container +MemoizationKit.CacheStyle(::typeof(tasklocal), x::String) = GlobalCache() # Custom keys share entries while the implementations receive the original inputs. @cached keyshape(x::AbstractVector, n::Int = 2; offset = 0) = counting(n * length(x) + offset) @cached keyshape(x::Tuple, n::Int = 2; offset = 0) = counting(n * length(x) + offset) -Cached.cachekey(::typeof(keyshape), x, n; offset) = (length(x), n, offset) -Cached.cachekey(::typeof(keyshape), ::Vector{Nothing}, n; offset) = error("key hook must be bypassed") +MemoizationKit.cachekey(::typeof(keyshape), x, n; offset) = (length(x), n, offset) +MemoizationKit.cachekey(::typeof(keyshape), ::Vector{Nothing}, n; offset) = error("key hook must be bypassed") @cached wrapped(x) = counting(length(x)) -Cached.cachekey(::typeof(wrapped), x) = +MemoizationKit.cachekey(::typeof(wrapped), x) = Hashed(x, (x, seed) -> hash(length(x), seed), (x, y) -> isequal(length(x), length(y))) -Cached.CacheStyle(::typeof(wrapped), ::Tuple) = TaskLocalCache{Dict}() +MemoizationKit.CacheStyle(::typeof(wrapped), ::Tuple) = TaskLocalCache{Dict}() keyallocs(f, x) = (f(x); @allocated f(x)) @cached sized(x) = counting(x) @@ -228,7 +228,7 @@ end @test isempty(cache_info(tasklocal)) @test ncalls(() -> tasklocal("a")) == ("a", 1) @test ncalls(() -> tasklocal("a")) == ("a", 0) - @test only(caches(tasklocal)) isa Cached.DEFAULT_CONTAINER + @test only(caches(tasklocal)) isa MemoizationKit.DEFAULT_CONTAINER @test_throws ArgumentError GlobalCache{Dict}() end @@ -247,7 +247,7 @@ end for i in 1:20 sized(i) end - @test Cached.cache_stats(only(caches(sized))).currentsize == 100 + @test MemoizationKit.cache_stats(only(caches(sized))).currentsize == 100 # the limit is a budget for the function as a whole, all signatures together set_cache_size!(manytypes, 3) diff --git a/test/test_precompile.jl b/test/test_precompile.jl index 5484f59..2326404 100644 --- a/test/test_precompile.jl +++ b/test/test_precompile.jl @@ -1,20 +1,20 @@ using Test -using Cached +using MemoizationKit @testset "precompile workload leaves no cache state" begin - if isdefined(Cached, :_Precompile) - workload = Cached._Precompile + if isdefined(MemoizationKit, :_Precompile) + workload = MemoizationKit._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()))) + @test all(f -> parentmodule(typeof(f)) !== workload, keys(MemoizationKit.REGISTRY.functions)) + @test all(k -> parentmodule(typeof(first(k))) !== workload, keys(get(task_local_storage(), :__MemoizationKit_tasklocal__, IdDict()))) else - @test isempty(cache_info(Cached)) + @test isempty(cache_info(MemoizationKit)) end - @test Cached.DISK_ENABLED[] + @test MemoizationKit.DISK_ENABLED[] end if Base.JLOptions().use_compiled_modules == 0 @testset "source loading skips precompile helpers" begin - @test !isdefined(Cached, :_Precompile) + @test !isdefined(MemoizationKit, :_Precompile) end end diff --git a/test/test_preferences.jl b/test/test_preferences.jl index 33317ac..3d40b6a 100644 --- a/test/test_preferences.jl +++ b/test/test_preferences.jl @@ -1,9 +1,9 @@ using Test -using Cached +using MemoizationKit using Preferences using Aqua: Aqua -resolve(args...) = Cached._resolve_settings(args...)[(:maxsize, :by)] # the RAM settings +resolve(args...) = MemoizationKit._resolve_settings(args...)[(:maxsize, :by)] # the RAM settings caches(f) = last.(cache_info(f)) @testset "resolution order" begin @@ -11,8 +11,8 @@ caches(f) = last.(cache_info(f)) 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 = Cached.cachesize) - @test resolve("g", cached, package) == (; maxsize = 5, by = Cached.cachesize) + @test resolve("f", cached, package) == (; maxsize = 50, by = MemoizationKit.cachesize) + @test resolve("g", cached, package) == (; maxsize = 5, by = MemoizationKit.cachesize) end @testset "invalid preferences are ignored with a warning" begin @@ -46,7 +46,7 @@ function with_private_preferences(f) """ [deps] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" - Cached = "1b238080-9255-4fe9-b224-89eb24efe93b" + MemoizationKit = "1b238080-9255-4fe9-b224-89eb24efe93b" """ ) old = Base.ACTIVE_PROJECT[] @@ -60,34 +60,34 @@ end with_private_preferences() do @testset "preferences are read when a function's first cache is created" begin - # Cached's own section - set_preferences!(Cached, "maxsize" => 7, "measure" => "count"; force = true) + # MemoizationKit's own section + set_preferences!(MemoizationKit, "maxsize" => 7, "measure" => "count"; force = true) try fromcached(1) - @test Cached.cache_stats(only(cache_info(fromcached)).second).maxsize == 7 + @test MemoizationKit.cache_stats(only(cache_info(fromcached)).second).maxsize == 7 finally - delete_preferences!(Cached, "maxsize", "measure"; force = true) + delete_preferences!(MemoizationKit, "maxsize", "measure"; force = true) end # the section of the package that owns the function (here Aqua), and per-function sections set_preferences!( - Aqua, "Cached" => Dict("maxsize" => 3, "test_ambiguities" => Dict("maxsize" => 2)); + Aqua, "MemoizationKit" => Dict("maxsize" => 3, "test_ambiguities" => Dict("maxsize" => 2)); force = true ) try Aqua.test_all(Key(1)) Aqua.test_ambiguities(Key(1)) - @test Cached.cache_stats(only(cache_info(Aqua.test_all)).second).maxsize == 3 - @test Cached.cache_stats(only(cache_info(Aqua.test_ambiguities)).second).maxsize == 2 + @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 # runtime settings still override preferences set_cache_size!(Aqua.test_all, 11) - @test Cached.cache_stats(only(cache_info(Aqua.test_all)).second).maxsize == 11 + @test MemoizationKit.cache_stats(only(cache_info(Aqua.test_all)).second).maxsize == 11 finally - delete_preferences!(Aqua, "Cached"; force = true) + delete_preferences!(Aqua, "MemoizationKit"; force = true) end - # functions outside packages only see Cached's section - @test Cached._package_section(fromcached) === nothing + # functions outside packages only see MemoizationKit's section + @test MemoizationKit._package_section(fromcached) === nothing end end with_private_preferences() do @@ -95,24 +95,24 @@ with_private_preferences() do try # global section, including the compile-time container set_cache_preferences!(; maxsize = 123, container = "LRU") - @test load_preference(Cached, "maxsize") == 123 - @test load_preference(Cached, "container") == "LRU" + @test load_preference(MemoizationKit, "maxsize") == 123 + @test load_preference(MemoizationKit, "container") == "LRU" set_cache_preferences!(; maxsize = nothing, container = nothing) - @test !has_preference(Cached, "maxsize") && !has_preference(Cached, "container") + @test !has_preference(MemoizationKit, "maxsize") && !has_preference(MemoizationKit, "container") # package and function sections combine without clobbering each other set_cache_preferences!(Aqua.test_all; maxsize = 4) set_cache_preferences!(Aqua; measure = "bytes") set_cache_preferences!(Aqua.test_ambiguities; measure = "count") - section = load_preference(Aqua, "Cached") + section = load_preference(Aqua, "MemoizationKit") @test section == Dict( "measure" => "bytes", "test_all" => Dict("maxsize" => 4), "test_ambiguities" => Dict("measure" => "count") ) @test resolve("test_all", Dict(), section) == - (; maxsize = 4, by = Cached.cachesize) + (; maxsize = 4, by = MemoizationKit.cachesize) set_cache_preferences!(Aqua.test_all; maxsize = nothing) # empty sections are removed - @test !haskey(load_preference(Aqua, "Cached"), "test_all") + @test !haskey(load_preference(Aqua, "MemoizationKit"), "test_all") @test_throws ArgumentError set_cache_preferences!(Aqua; maxsize = -1) @test_throws ArgumentError set_cache_preferences!(Aqua; measure = "kilos") @@ -120,8 +120,8 @@ with_private_preferences() do @test_throws ArgumentError set_cache_preferences!(Aqua; maxsise = 1) @test_throws ArgumentError set_cache_preferences!(x -> x; maxsize = 1) # not in a package finally - delete_preferences!(Aqua, "Cached"; force = true) - delete_preferences!(Cached, "maxsize", "container"; force = true) + delete_preferences!(Aqua, "MemoizationKit"; force = true) + delete_preferences!(MemoizationKit, "maxsize", "container"; force = true) end end end @@ -130,39 +130,39 @@ struct Blob data::Vector{UInt8} meta::Vector{Int} end -Cached.cachesize(b::Blob) = length(b.data) +MemoizationKit.cachesize(b::Blob) = length(b.data) @cached blob(n::Int) = Blob(zeros(UInt8, n), collect(1:1000)) @testset "cachesize can be overloaded" begin - @test Cached.cachesize([1, 2, 3]) == Base.summarysize([1, 2, 3]) - set_cache_size!(blob, 100; by = Cached.cachesize) + @test MemoizationKit.cachesize([1, 2, 3]) == Base.summarysize([1, 2, 3]) + set_cache_size!(blob, 100; by = MemoizationKit.cachesize) blob(40) blob(50) c = only(caches(blob)) - @test Cached.cache_stats(c).currentsize == 90 # ignores `meta`, unlike summarysize + @test MemoizationKit.cache_stats(c).currentsize == 90 # ignores `meta`, unlike summarysize blob(20) # evicts one entry to fit @test length(c) == 2 end @testset "container preference (compile time)" begin - @test Cached.DEFAULT_CONTAINER === ClockCache + @test MemoizationKit.DEFAULT_CONTAINER === ClockCache @test CacheStyle(sum, 1) === GlobalCache{ClockCache}() === GlobalCache() @test TaskLocalCache() === TaskLocalCache{ClockCache}() - # a fresh process with `container = "LRU"` in Cached's preferences + # a fresh process with `container = "LRU"` in MemoizationKit's preferences mktempdir() do env write( joinpath(env, "LocalPreferences.toml"), - "[Cached]\ncontainer = \"LRU\"\n" + "[MemoizationKit]\ncontainer = \"LRU\"\n" ) code = """ using Pkg Pkg.activate($(repr(env)); io = devnull) - Pkg.develop(path = $(repr(pkgdir(Cached))); io = devnull) - using Cached - print(Cached.CacheStyle(sum, 1) === GlobalCache{LRU}()) + Pkg.develop(path = $(repr(pkgdir(MemoizationKit))); io = devnull) + using MemoizationKit + print(MemoizationKit.CacheStyle(sum, 1) === GlobalCache{LRU}()) """ cmd = addenv( `$(Base.julia_cmd()) --startup-file=no -e $code`, diff --git a/test/test_tachikoma.jl b/test/test_tachikoma.jl index 6f166fc..f0864d0 100644 --- a/test/test_tachikoma.jl +++ b/test/test_tachikoma.jl @@ -1,14 +1,14 @@ using Test -using Cached +using MemoizationKit @testset "error hint without Tachikoma" begin - # a fresh process with Cached only, since this one loads Tachikoma + # a fresh process with MemoizationKit only, since this one loads Tachikoma mktempdir() do env code = """ using Pkg Pkg.activate($(repr(env)); io = devnull) - Pkg.develop(path = $(repr(pkgdir(Cached))); io = devnull) - using Cached + Pkg.develop(path = $(repr(pkgdir(MemoizationKit))); io = devnull) + using MemoizationKit for call in (() -> cache_dashboard(), () -> cache_dashboard(; interval = 2)) try call() @@ -29,10 +29,10 @@ end using Tachikoma: Tachikoma, TestBackend, KeyEvent, Rect, Frame, GraphicsRegion, PixelSnapshot, find_text, row_text -const Ext = Base.get_extension(Cached, :CachedTachikomaExt) +const Ext = Base.get_extension(MemoizationKit, :MemoizationKitTachikomaExt) @testset "no hint with Tachikoma loaded" begin - @test hasmethod(Cached.cache_dashboard, Tuple{}) + @test hasmethod(MemoizationKit.cache_dashboard, Tuple{}) e = try cache_dashboard(1) catch e @@ -46,9 +46,9 @@ end @cached dash_bytes(n::Int)::Vector{Float64} = zeros(n) @cached dash_late(x) = x @cached dash_lru(x) = x -Cached.CacheStyle(::typeof(dash_lru), x) = GlobalLRUCache() +MemoizationKit.CacheStyle(::typeof(dash_lru), x) = GlobalLRUCache() @cached dash_two(x) = x # one cache per container type -Cached.CacheStyle(::typeof(dash_two), ::Int) = GlobalLRUCache() +MemoizationKit.CacheStyle(::typeof(dash_two), ::Int) = GlobalLRUCache() function draw(m; width = 120, height = 24) tb = TestBackend(width, height) @@ -69,7 +69,7 @@ function dashboard(filter) end foreach(dash_square, (1, 2, 3, 1, 2, 1.0)) -set_cache_size!(dash_bytes, 10^6; by = Cached.cachesize) +set_cache_size!(dash_bytes, 10^6; by = MemoizationKit.cachesize) dash_bytes(10); dash_bytes(10); dash_lru(1) @@ -189,7 +189,7 @@ end @test isempty(c) @test occursin("dash_square", line(draw(m), "emptied")) - old = Cached.cache_stats(c).maxsize + old = MemoizationKit.cache_stats(c).maxsize press(m, :enter) @test m.pending == old press(m, :right, :right, :left) @@ -198,22 +198,22 @@ end press(m, ']') @test m.pending == 2old + round(Int, 2old / 10) press(m, :escape) - @test m.pending === nothing && Cached.cache_stats(c).maxsize == old && !m.quit + @test m.pending === nothing && MemoizationKit.cache_stats(c).maxsize == old && !m.quit press(m, :enter, :left, '[', :enter) n = old ÷ 2 - round(Int, (old ÷ 2) / 10) - @test Cached.cache_stats(c).maxsize == n + @test MemoizationKit.cache_stats(c).maxsize == n @test occursin("dash_square", line(draw(m), "set the limit")) # the limit is the function's (set with `set_cache_size!`), so it persists - @test Cached.REGISTRY.functions[dash_square].maxsize == n + @test MemoizationKit.REGISTRY.functions[dash_square].maxsize == n end @testset "resizing a byte-measured function keeps its cache" begin m = dashboard("dash_bytes") c = only(lines(m)).cache - @test Cached.cache_stats(c).by !== nothing && length(c) == 1 + @test MemoizationKit.cache_stats(c).by !== nothing && length(c) == 1 press(m, :enter, :right, :enter) @test only(cache_info(dash_bytes)).second === c # not discarded - @test length(c) == 1 && Cached.cache_stats(c).by !== nothing && Cached.cache_stats(c).maxsize == 2 * 10^6 + @test length(c) == 1 && MemoizationKit.cache_stats(c).by !== nothing && MemoizationKit.cache_stats(c).maxsize == 2 * 10^6 end @testset "refresh picks up new caches and rates" begin @@ -230,7 +230,7 @@ end @test late.trend.rates[end] ≈ 2.0 && Ext.recent(late.trend) == 1.0 # caches that disappear are dropped, with their trends - set_cache_size!(dash_late, 10; by = Cached.cachesize) # discards the cache + set_cache_size!(dash_late, 10; by = MemoizationKit.cachesize) # discards the cache Ext.refresh!(m) @test isempty(lines(m)) && !haskey(m.trends, late.cache) @test draw(m) isa TestBackend @@ -298,8 +298,8 @@ end @testset "without SQLite" begin # a fresh process with this environment, which has SQLite without loading it code = """ - using Cached, Tachikoma - const Ext = Base.get_extension(Cached, :CachedTachikomaExt) + using MemoizationKit, Tachikoma + const Ext = Base.get_extension(MemoizationKit, :MemoizationKitTachikomaExt) @cached nosql(x) = x nosql(1) m = Ext.Dashboard(; interval = Inf, filter = "nosql") @@ -310,7 +310,7 @@ end return tb end tb = draw(m) - print(Base.get_extension(Cached, :CachedSQLiteExt) === nothing, " ", length(Ext.current(m).lines), " ") + print(Base.get_extension(MemoizationKit, :MemoizationKitSQLiteExt) === nothing, " ", length(Ext.current(m).lines), " ") print(Tachikoma.find_text(tb, "Clock") !== nothing, " ", Tachikoma.find_text(tb, "disk") === nothing, " ") Tachikoma.update!(m, Tachikoma.KeyEvent(:tab)) tb = draw(m) @@ -323,16 +323,16 @@ end end using SQLite: SQLite -include(joinpath(pkgdir(Cached), "benchmark", "disk_stress.jl")) # `use_disk_path` +include(joinpath(pkgdir(MemoizationKit), "benchmark", "disk_stress.jl")) # `use_disk_path` const DISK_ENV = use_disk_path(mktempdir()) @cached dash_disk(x::Int) = x + 1 -Cached.DiskCacheStyle(::typeof(dash_disk), ::Int) = DiskCache() +MemoizationKit.DiskCacheStyle(::typeof(dash_disk), ::Int) = DiskCache() @cached dash_diskonly(x::Int)::Int = 2x -Cached.CacheStyle(::typeof(dash_diskonly), ::Int) = NoCache() -Cached.DiskCacheStyle(::typeof(dash_diskonly), ::Int) = DiskCache() +MemoizationKit.CacheStyle(::typeof(dash_diskonly), ::Int) = NoCache() +MemoizationKit.DiskCacheStyle(::typeof(dash_diskonly), ::Int) = DiskCache() @cached dash_disknotyet(x::Int) = x # its store is opened on the first call -Cached.DiskCacheStyle(::typeof(dash_disknotyet), ::Int) = DiskCache() +MemoizationKit.DiskCacheStyle(::typeof(dash_disknotyet), ::Int) = DiskCache() # Draw until the entries and sizes on disk are read. function settle!(m; kw...) @@ -353,7 +353,7 @@ end empty_caches!(dash_disk) dash_disk(1) # two misses on disk, then a hit foreach(dash_diskonly, (1, 1, 1, 2)) - stats = Dict(Cached.disk_cache_stats()) + stats = Dict(MemoizationKit.disk_cache_stats()) @test stats[dash_disk] == (; hits = 1, misses = 2) && stats[dash_diskonly] == (; hits = 2, misses = 2) @test !haskey(stats, dash_disknotyet) @@ -443,5 +443,5 @@ end @test find_text(tb, "Hit rate") !== nothing && occursin("░", row_text(tb, 4)) end -Base.get_extension(Cached, :CachedSQLiteExt).close_all() +Base.get_extension(MemoizationKit, :MemoizationKitSQLiteExt).close_all() filter!(!=(DISK_ENV), LOAD_PATH) From 6c04b020e843a68812b175b394e2c2e93fbec11c Mon Sep 17 00:00:00 2001 From: lkdvos Date: Thu, 8 Oct 2026 09:13:29 -0400 Subject: [PATCH 2/3] Add announcement and point URLs to QuantumKitHub Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/Documentation.yml | 2 +- ANNOUNCEMENT.md | 39 +++++++++++++++++++++++++++++ README.md | 16 ++++++------ docs/make.jl | 4 +-- 4 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 ANNOUNCEMENT.md diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml index 542d133..9c252b3 100644 --- a/.github/workflows/Documentation.yml +++ b/.github/workflows/Documentation.yml @@ -34,7 +34,7 @@ jobs: script: | const marker = ''; const issue_number = context.issue.number; - const url = `https://lkdvos.github.io/MemoizationKit.jl/previews/PR${issue_number}/`; + const url = `https://quantumkithub.github.io/MemoizationKit.jl/previews/PR${issue_number}/`; const body = `${marker}\n[Preview the documentation](${url})`; const comments = await github.paginate(github.rest.issues.listComments, { ...context.repo, diff --git a/ANNOUNCEMENT.md b/ANNOUNCEMENT.md new file mode 100644 index 0000000..c6e2fcb --- /dev/null +++ b/ANNOUNCEMENT.md @@ -0,0 +1,39 @@ +**[ANN] MemoizationKit.jl: memoization with bounded memory, disk persistence, and a live dashboard** + +MemoizationKit.jl v0.1.0 memoizes expensive Julia functions and gives you tools to manage the caches: limit memory, keep results across runs, and see what they're doing. + +```julia +using MemoizationKit + +@cached function fib(n::Int)::BigInt + n < 2 ? BigInt(n) : fib(n - 1) + fib(n - 2) +end + +fib(100) # compute and cache +cache_info(fib) # size and hit/miss statistics +set_cache_size!(fib, 1000) # bound the cache +``` + +**Features** + +- **Bounded storage:** Clock and LRU eviction, with limits in entries or bytes across all methods of a function. +- **Disk persistence:** load SQLite.jl to keep results across runs, or ship precomputed results as artifacts. + Lookups go RAM, then artifact, then local database, then computation. +- **Live dashboard:** load Tachikoma.jl and call `cache_dashboard()` to browse hit rates, sizes, and activity for RAM and disk caches. + You can also clear or resize caches from the terminal. +- **Fast RAM hits:** return-type inference is preserved, and RAM hits don't allocate for concrete keys with the built-in containers. +- **Strategies by function and argument type:** shared, task-local, or no caching via `CacheStyle`. +- **Custom keys:** `MemoizationKit.cachekey` lets equivalent inputs share a result, and `Hashed` customizes hashing and equality. +- **Configuration and profiling:** Preferences set defaults per package or function, and TimerOutputs.jl integration profiles lookups and computations. + +If a single in-memory cache is all you need, Memoize.jl or Memoization.jl with an LRU container may already be enough. +MemoizationKit targets workloads where the cache needs ongoing management. +The docs include a comparison. + +Requires Julia 1.10 or later. +Install with `Pkg.add("MemoizationKit")`. + +- Docs: https://quantumkithub.github.io/MemoizationKit.jl/stable/ +- Source: https://github.com/QuantumKitHub/MemoizationKit.jl + +Feedback, issues, and PRs are welcome. diff --git a/README.md b/README.md index 6793b60..dadfc66 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # MemoizationKit.jl -[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://lkdvos.github.io/MemoizationKit.jl/stable/) -[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://lkdvos.github.io/MemoizationKit.jl/dev/) -[![Build Status](https://github.com/lkdvos/MemoizationKit.jl/actions/workflows/Tests.yml/badge.svg?branch=main)](https://github.com/lkdvos/MemoizationKit.jl/actions/workflows/Tests.yml?query=branch%3Amain) -[![Coverage](https://codecov.io/gh/lkdvos/MemoizationKit.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/lkdvos/MemoizationKit.jl) +[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://quantumkithub.github.io/MemoizationKit.jl/stable/) +[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://quantumkithub.github.io/MemoizationKit.jl/dev/) +[![Build Status](https://github.com/QuantumKitHub/MemoizationKit.jl/actions/workflows/Tests.yml/badge.svg?branch=main)](https://github.com/QuantumKitHub/MemoizationKit.jl/actions/workflows/Tests.yml?query=branch%3Amain) +[![Coverage](https://codecov.io/gh/QuantumKitHub/MemoizationKit.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/QuantumKitHub/MemoizationKit.jl) [![Code Style: Runic](https://img.shields.io/badge/code_style-%F0%9F%AA%A8_Runic-9558B2)](https://github.com/fredrikekre/Runic.jl) [![Aqua](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl) @@ -42,7 +42,7 @@ fib(200) # new results are kept in RAM and on disk Calls look in RAM, then on disk, before computing. Processes on the same machine share the database. -You control result versions and disk cleanup; see [disk caching](https://lkdvos.github.io/MemoizationKit.jl/dev/disk/). +You control result versions and disk cleanup; see [disk caching](https://quantumkithub.github.io/MemoizationKit.jl/dev/disk/). ## See what your caches are doing @@ -63,14 +63,14 @@ The dashboard shows recent hit rates, storage use, and activity, with separate R ⇥ tab ↑↓ select ⏎ resize e empty s sort / filter q quit ``` -See the [dashboard guide](https://lkdvos.github.io/MemoizationKit.jl/dev/dashboard/) for controls and statistics. +See the [dashboard guide](https://quantumkithub.github.io/MemoizationKit.jl/dev/dashboard/) for controls and statistics. ## Why MemoizationKit? Choose MemoizationKit when memoization needs ongoing management: memory limits, reuse across runs, or visibility into a running workload. It brings these tools together with strategies chosen by function and argument type. For a single in-memory cache, Memoize.jl or Memoization.jl with an LRU container may already cover your needs. -See [the comparison and tradeoffs](https://lkdvos.github.io/MemoizationKit.jl/dev/#why-memoizationkit). +See [the comparison and tradeoffs](https://quantumkithub.github.io/MemoizationKit.jl/dev/#why-memoizationkit). Requires Julia 1.10 or later. -Start with [usage](https://lkdvos.github.io/MemoizationKit.jl/dev/#usage), [configuration](https://lkdvos.github.io/MemoizationKit.jl/dev/configuration/), or [timing](https://lkdvos.github.io/MemoizationKit.jl/dev/timing/) in the [documentation](https://lkdvos.github.io/MemoizationKit.jl/dev/). +Start with [usage](https://quantumkithub.github.io/MemoizationKit.jl/dev/#usage), [configuration](https://quantumkithub.github.io/MemoizationKit.jl/dev/configuration/), or [timing](https://quantumkithub.github.io/MemoizationKit.jl/dev/timing/) in the [documentation](https://quantumkithub.github.io/MemoizationKit.jl/dev/). diff --git a/docs/make.jl b/docs/make.jl index 4e7f936..9182d16 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -8,7 +8,7 @@ makedocs(; authors = "Lukas Devos", sitename = "MemoizationKit.jl", format = Documenter.HTML(; - canonical = "https://lkdvos.github.io/MemoizationKit.jl", + canonical = "https://quantumkithub.github.io/MemoizationKit.jl", edit_link = "main", assets = String[], ), @@ -31,4 +31,4 @@ makedocs(; ], ) -deploydocs(; repo = "github.com/lkdvos/MemoizationKit.jl", devbranch = "main", push_preview = true) +deploydocs(; repo = "github.com/QuantumKitHub/MemoizationKit.jl", devbranch = "main", push_preview = true) From 342d986763ba8cfe46b791b2dd5fa6cd09487687 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Thu, 8 Oct 2026 09:15:21 -0400 Subject: [PATCH 3/3] Remove announcement draft Co-Authored-By: Claude Sonnet 5.5 --- ANNOUNCEMENT.md | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 ANNOUNCEMENT.md diff --git a/ANNOUNCEMENT.md b/ANNOUNCEMENT.md deleted file mode 100644 index c6e2fcb..0000000 --- a/ANNOUNCEMENT.md +++ /dev/null @@ -1,39 +0,0 @@ -**[ANN] MemoizationKit.jl: memoization with bounded memory, disk persistence, and a live dashboard** - -MemoizationKit.jl v0.1.0 memoizes expensive Julia functions and gives you tools to manage the caches: limit memory, keep results across runs, and see what they're doing. - -```julia -using MemoizationKit - -@cached function fib(n::Int)::BigInt - n < 2 ? BigInt(n) : fib(n - 1) + fib(n - 2) -end - -fib(100) # compute and cache -cache_info(fib) # size and hit/miss statistics -set_cache_size!(fib, 1000) # bound the cache -``` - -**Features** - -- **Bounded storage:** Clock and LRU eviction, with limits in entries or bytes across all methods of a function. -- **Disk persistence:** load SQLite.jl to keep results across runs, or ship precomputed results as artifacts. - Lookups go RAM, then artifact, then local database, then computation. -- **Live dashboard:** load Tachikoma.jl and call `cache_dashboard()` to browse hit rates, sizes, and activity for RAM and disk caches. - You can also clear or resize caches from the terminal. -- **Fast RAM hits:** return-type inference is preserved, and RAM hits don't allocate for concrete keys with the built-in containers. -- **Strategies by function and argument type:** shared, task-local, or no caching via `CacheStyle`. -- **Custom keys:** `MemoizationKit.cachekey` lets equivalent inputs share a result, and `Hashed` customizes hashing and equality. -- **Configuration and profiling:** Preferences set defaults per package or function, and TimerOutputs.jl integration profiles lookups and computations. - -If a single in-memory cache is all you need, Memoize.jl or Memoization.jl with an LRU container may already be enough. -MemoizationKit targets workloads where the cache needs ongoing management. -The docs include a comparison. - -Requires Julia 1.10 or later. -Install with `Pkg.add("MemoizationKit")`. - -- Docs: https://quantumkithub.github.io/MemoizationKit.jl/stable/ -- Source: https://github.com/QuantumKitHub/MemoizationKit.jl - -Feedback, issues, and PRs are welcome.