diff --git a/src/audit.jl b/src/audit.jl index a150c3b..321d3ff 100644 --- a/src/audit.jl +++ b/src/audit.jl @@ -166,6 +166,19 @@ function _is_own(m::Module, name::Symbol) end end +# `own_methods` is a scan over every public callable of every loaded module — 1916 candidates and +# 11026 methods behind them for this package — and a suite that audits several modules pays it once +# per audit. Its ANSWER, though, is "the methods whose defining module is `m`", and that set can +# only change when a method is defined or deleted. Both bump the world counter: measured on 1.11.9, +# 1.12.2 and 1.14.0-DEV, a method definition bumps it in all three. +# +# A `const` binding does NOT bump it on 1.11 (it does on 1.12 and later), which is why the key is +# argued rather than assumed. A new `const` cannot change this answer: either it aliases something +# whose methods belong to another module, or creating it defined a method and bumped the counter. +const _OWN_METHODS = Ref{Tuple{UInt64,Dict{Module,Vector{Method}}}}(( + typemax(UInt64), Dict{Module,Vector{Method}}() +)) + """ own_methods(m::Module) -> Vector{Method} @@ -183,6 +196,21 @@ exported-or-`public` names of every loaded module. That last set is what catches every binding of every loaded module. """ function own_methods(m::Module) + w = Base.get_world_counter() + (cached_world, cache) = _OWN_METHODS[] + cached_world == w || (cache = Dict{Module,Vector{Method}}()) + # Copied out: the vector is the caller's to filter, sort or push to, and a caller that mutates + # it must not be able to corrupt what the next one sees. + haskey(cache, m) && return copy(cache[m]) + out = _own_methods(m) + # Copy-on-write rather than `cache[m] = out`. The suite runs on four threads and a `Dict` is + # not safe under concurrent `setindex!`; swapping a freshly built one in means the worst a race + # can cost is a recomputation, never a corrupted table. + _OWN_METHODS[] = (w, merge(cache, Dict(m => out))) + return copy(out) +end + +function _own_methods(m::Module) out = Method[] seen = Set{Method}() for f in _generic_candidates(m) @@ -195,7 +223,11 @@ function own_methods(m::Module) mm.module === m && !(mm in seen) && (push!(seen, mm); push!(out, mm)) end end - return sort!(out; by=mm -> (string(mm.name), string(mm.sig))) + # The key is built ONCE per method, not once per comparison. `sort!(…; by = f)` calls `f` on + # both sides of every comparison, and `string(mm.sig)` is not cheap: measured on this + # package's own 301 methods, the sort was 0.601s while building all 301 keys was 0.039s. That + # one line was 80% of `audit`, which is called once per module in every surface check. + return out[sortperm([(string(mm.name), string(mm.sig)) for mm in out])] end function _generic_candidates(m::Module) diff --git a/test/test_dogfood.jl b/test/test_dogfood.jl index 4a48781..0dab4b7 100644 --- a/test/test_dogfood.jl +++ b/test/test_dogfood.jl @@ -88,6 +88,11 @@ end # The control the testset above cannot be: an equality against a hand-written set is satisfied # by marking every name and updating the set to match. These are the names the front page # promises answers from, and a promise is exactly what a mark withdraws. + # + # Hoisted, and not for tidiness: `audit(ExperimentalAPI)` costs 0.75s — it reads a docstring + # for every public name and walks the method tables for the contributed ones — so calling it + # per iteration cost 17.8s, which was 16% of the whole suite. + surface = audit(ExperimentalAPI).surface for n in [ Symbol("@experimental"), :Mark, @@ -114,7 +119,7 @@ end :age, :docstring_note, ] - @test n in audit(ExperimentalAPI).surface + @test n in surface @test !isexperimental(ExperimentalAPI, n) end end