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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion src/audit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand All @@ -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)
Expand All @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion test/test_dogfood.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -114,7 +119,7 @@ end
:age,
:docstring_note,
]
@test n in audit(ExperimentalAPI).surface
@test n in surface
@test !isexperimental(ExperimentalAPI, n)
end
end
Expand Down
Loading