Skip to content

Commit 12c5d2d

Browse files
Add count of lock conflicts to @time etc. Add @lock_conflicts (#52883)
1 parent 98e4f01 commit 12c5d2d

File tree

7 files changed

+102
-15
lines changed

7 files changed

+102
-15
lines changed

NEWS.md

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ New language features
3030
a `Manifest-v1.11.toml` would be used by v1.11 and `Manifest.toml` by every other julia
3131
version. This makes managing environments for multiple julia versions at the same time
3232
easier ([#43845]).
33+
* `@time` now reports a count of any lock conflicts where a `ReentrantLock` had to wait, plus a new macro
34+
`@lock_conflicts` which returns that count ([#52883]).
3335

3436
Language changes
3537
----------------

base/atomics.jl

+7
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ end
8080

8181
Atomic() = Atomic{Int}()
8282

83+
const LOCK_PROFILING = Atomic{Int}(0)
84+
lock_profiling(state::Bool) = state ? atomic_add!(LOCK_PROFILING, 1) : atomic_sub!(LOCK_PROFILING, 1)
85+
lock_profiling() = LOCK_PROFILING[] > 0
86+
87+
const LOCK_CONFLICT_COUNT = Atomic{Int}(0);
88+
inc_lock_conflict_count() = atomic_add!(LOCK_CONFLICT_COUNT, 1)
89+
8390
"""
8491
Threads.atomic_cas!(x::Atomic{T}, cmp::T, newval::T) where T
8592

base/exports.jl

+1
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,7 @@ export
10361036
@elapsed,
10371037
@allocated,
10381038
@allocations,
1039+
@lock_conflicts,
10391040

10401041
# tasks
10411042
@sync,

base/lock.jl

+1
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ Each `lock` must be matched by an [`unlock`](@ref).
145145
"""
146146
@inline function lock(rl::ReentrantLock)
147147
trylock(rl) || (@noinline function slowlock(rl::ReentrantLock)
148+
Threads.lock_profiling() && Threads.inc_lock_conflict_count()
148149
c = rl.cond_wait
149150
lock(c.lock)
150151
try

base/timing.jl

+71-11
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ function format_bytes(bytes; binary=true) # also used by InteractiveUtils
146146
end
147147
end
148148

149-
function time_print(io::IO, elapsedtime, bytes=0, gctime=0, allocs=0, compile_time=0, recompile_time=0, newline=false; msg::Union{String,Nothing}=nothing)
149+
function time_print(io::IO, elapsedtime, bytes=0, gctime=0, allocs=0, lock_conflicts=0, compile_time=0, recompile_time=0, newline=false;
150+
msg::Union{String,Nothing}=nothing)
150151
timestr = Ryu.writefixed(Float64(elapsedtime/1e9), 6)
151152
str = sprint() do io
152153
if msg isa String
@@ -172,6 +173,10 @@ function time_print(io::IO, elapsedtime, bytes=0, gctime=0, allocs=0, compile_ti
172173
end
173174
print(io, Ryu.writefixed(Float64(100*gctime/elapsedtime), 2), "% gc time")
174175
end
176+
if lock_conflicts > 0
177+
plural = lock_conflicts == 1 ? "" : "s"
178+
print(io, ", ", lock_conflicts, " lock conflict$plural")
179+
end
175180
if compile_time > 0
176181
if bytes != 0 || allocs != 0 || gctime > 0
177182
print(io, ", ")
@@ -190,11 +195,11 @@ function time_print(io::IO, elapsedtime, bytes=0, gctime=0, allocs=0, compile_ti
190195
nothing
191196
end
192197

193-
function timev_print(elapsedtime, diff::GC_Diff, compile_times; msg::Union{String,Nothing}=nothing)
198+
function timev_print(elapsedtime, diff::GC_Diff, lock_conflicts, compile_times; msg::Union{String,Nothing}=nothing)
194199
allocs = gc_alloc_count(diff)
195200
compile_time = first(compile_times)
196201
recompile_time = last(compile_times)
197-
time_print(stdout, elapsedtime, diff.allocd, diff.total_time, allocs, compile_time, recompile_time, true; msg)
202+
time_print(stdout, elapsedtime, diff.allocd, diff.total_time, allocs, lock_conflicts, compile_time, recompile_time, true; msg)
198203
padded_nonzero_print(elapsedtime, "elapsed time (ns)")
199204
padded_nonzero_print(diff.total_time, "gc time (ns)")
200205
padded_nonzero_print(diff.allocd, "bytes allocated")
@@ -226,7 +231,8 @@ end
226231
A macro to execute an expression, printing the time it took to execute, the number of
227232
allocations, and the total number of bytes its execution caused to be allocated, before
228233
returning the value of the expression. Any time spent garbage collecting (gc), compiling
229-
new code, or recompiling invalidated code is shown as a percentage.
234+
new code, or recompiling invalidated code is shown as a percentage. Any lock conflicts
235+
where a [`ReentrantLock`](@ref) had to wait are shown as a count.
230236
231237
Optionally provide a description string to print before the time report.
232238
@@ -247,6 +253,9 @@ See also [`@showtime`](@ref), [`@timev`](@ref), [`@timed`](@ref), [`@elapsed`](@
247253
248254
Recompilation time being shown separately from compilation time was introduced in Julia 1.8
249255
256+
!!! compat "Julia 1.11"
257+
The reporting of any lock conflicts was added in Julia 1.11.
258+
250259
```julia-repl
251260
julia> x = rand(10,10);
252261
@@ -283,7 +292,7 @@ macro time(msg, ex)
283292
quote
284293
local ret = @timed $(esc(ex))
285294
local _msg = $(esc(msg))
286-
time_print(stdout, ret.time*1e9, ret.gcstats.allocd, ret.gcstats.total_time, gc_alloc_count(ret.gcstats), ret.compile_time*1e9, ret.recompile_time*1e9, true; msg=_msg)
295+
time_print(stdout, ret.time*1e9, ret.gcstats.allocd, ret.gcstats.total_time, gc_alloc_count(ret.gcstats), ret.lock_conflicts, ret.compile_time*1e9, ret.recompile_time*1e9, true; msg=_msg)
287296
ret.value
288297
end
289298
end
@@ -355,7 +364,7 @@ macro timev(msg, ex)
355364
quote
356365
local ret = @timed $(esc(ex))
357366
local _msg = $(esc(msg))
358-
timev_print(ret.time*1e9, ret.gcstats, (ret.compile_time*1e9, ret.recompile_time*1e9); msg=_msg)
367+
timev_print(ret.time*1e9, ret.gcstats, ret.lock_conflicts, (ret.compile_time*1e9, ret.recompile_time*1e9); msg=_msg)
359368
ret.value
360369
end
361370
end
@@ -449,19 +458,57 @@ macro allocations(ex)
449458
end
450459
end
451460

461+
"""
462+
@lock_conflicts
463+
464+
A macro to evaluate an expression, discard the resulting value, and instead return the
465+
total number of lock conflicts during evaluation, where a lock attempt on a [`ReentrantLock`](@ref)
466+
resulted in a wait because the lock was already held.
467+
468+
See also [`@time`](@ref), [`@timev`](@ref) and [`@timed`](@ref).
469+
470+
```julia-repl
471+
julia> @lock_conflicts begin
472+
l = ReentrantLock()
473+
Threads.@threads for i in 1:Threads.nthreads()
474+
lock(l) do
475+
sleep(1)
476+
end
477+
end
478+
end
479+
5
480+
```
481+
482+
!!! compat "Julia 1.11"
483+
This macro was added in Julia 1.11.
484+
"""
485+
macro lock_conflicts(ex)
486+
quote
487+
Threads.lock_profiling(true)
488+
local lock_conflicts = Threads.LOCK_CONFLICT_COUNT[]
489+
try
490+
$(esc(ex))
491+
finally
492+
Threads.lock_profiling(false)
493+
end
494+
Threads.LOCK_CONFLICT_COUNT[] - lock_conflicts
495+
end
496+
end
497+
452498
"""
453499
@timed
454500
455501
A macro to execute an expression, and return the value of the expression, elapsed time in seconds,
456502
total bytes allocated, garbage collection time, an object with various memory allocation
457-
counters, compilation time in seconds, and recompilation time in seconds.
503+
counters, compilation time in seconds, and recompilation time in seconds. Any lock conflicts
504+
where a [`ReentrantLock`](@ref) had to wait are shown as a count.
458505
459506
In some cases the system will look inside the `@timed` expression and compile some of the
460507
called code before execution of the top-level expression begins. When that happens, some
461508
compilation time will not be counted. To include this time you can run `@timed @eval ...`.
462509
463510
See also [`@time`](@ref), [`@timev`](@ref), [`@elapsed`](@ref),
464-
[`@allocated`](@ref), and [`@allocations`](@ref).
511+
[`@allocated`](@ref), [`@allocations`](@ref), and [`@lock_conflicts`](@ref).
465512
466513
```julia-repl
467514
julia> stats = @timed rand(10^6);
@@ -493,21 +540,34 @@ julia> stats.recompile_time
493540
The return type of this macro was changed from `Tuple` to `NamedTuple` in Julia 1.5.
494541
495542
!!! compat "Julia 1.11"
496-
The `compile_time` and `recompile_time` fields were added in Julia 1.11.
543+
The `lock_conflicts`, `compile_time`, and `recompile_time` fields were added in Julia 1.11.
497544
"""
498545
macro timed(ex)
499546
quote
500547
Experimental.@force_compile
548+
Threads.lock_profiling(true)
549+
local lock_conflicts = Threads.LOCK_CONFLICT_COUNT[]
501550
local stats = gc_num()
502551
local elapsedtime = time_ns()
503552
cumulative_compile_timing(true)
504553
local compile_elapsedtimes = cumulative_compile_time_ns()
505554
local val = @__tryfinally($(esc(ex)),
506555
(elapsedtime = time_ns() - elapsedtime;
507556
cumulative_compile_timing(false);
508-
compile_elapsedtimes = cumulative_compile_time_ns() .- compile_elapsedtimes)
557+
compile_elapsedtimes = cumulative_compile_time_ns() .- compile_elapsedtimes;
558+
lock_conflicts = Threads.LOCK_CONFLICT_COUNT[] - lock_conflicts;
559+
Threads.lock_profiling(false))
509560
)
510561
local diff = GC_Diff(gc_num(), stats)
511-
(value=val, time=elapsedtime/1e9, bytes=diff.allocd, gctime=diff.total_time/1e9, gcstats=diff, compile_time=compile_elapsedtimes[1]/1e9, recompile_time=compile_elapsedtimes[2]/1e9)
562+
(
563+
value=val,
564+
time=elapsedtime/1e9,
565+
bytes=diff.allocd,
566+
gctime=diff.total_time/1e9,
567+
gcstats=diff,
568+
lock_conflicts=lock_conflicts,
569+
compile_time=compile_elapsedtimes[1]/1e9,
570+
recompile_time=compile_elapsedtimes[2]/1e9
571+
)
512572
end
513573
end

doc/src/base/base.md

+1
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ Base.@timed
352352
Base.@elapsed
353353
Base.@allocated
354354
Base.@allocations
355+
Base.@lock_conflicts
355356
Base.EnvDict
356357
Base.ENV
357358
Base.Sys.STDLIB

test/misc.jl

+19-4
Original file line numberDiff line numberDiff line change
@@ -1392,10 +1392,25 @@ end
13921392
@testset "Base/timing.jl" begin
13931393
@test Base.jit_total_bytes() >= 0
13941394

1395-
# sanity check `@allocations` returns what we expect in some very simple cases
1396-
@test (@allocations "a") == 0
1397-
@test (@allocations "a" * "b") == 0 # constant propagation
1398-
@test (@allocations "a" * Base.inferencebarrier("b")) == 1
1395+
# sanity check `@allocations` returns what we expect in some very simple cases.
1396+
# These are inside functions because `@allocations` uses `Experimental.@force_compile`
1397+
# so can be affected by other code in the same scope.
1398+
@test (() -> @allocations "a")() == 0
1399+
@test (() -> @allocations "a" * "b")() == 0 # constant propagation
1400+
@test (() -> @allocations "a" * Base.inferencebarrier("b"))() == 1
1401+
1402+
_lock_conflicts, _nthreads = eval(Meta.parse(read(`$(Base.julia_cmd()) -tauto -E '
1403+
_lock_conflicts = @lock_conflicts begin
1404+
l = ReentrantLock()
1405+
Threads.@threads for i in 1:Threads.nthreads()
1406+
lock(l) do
1407+
sleep(1)
1408+
end
1409+
end
1410+
end
1411+
_lock_conflicts,Threads.nthreads()
1412+
'`, String)))
1413+
@test _lock_conflicts > 0 skip=(_nthreads < 2) # can only test if the worker can multithread
13991414
end
14001415

14011416
#TODO: merge with `@testset "Base/timing.jl"` once https://github.com/JuliaLang/julia/issues/52948 is resolved

0 commit comments

Comments
 (0)