From 5a73a8392398cba844db2f98675a470e988cdb21 Mon Sep 17 00:00:00 2001 From: Younes IO Date: Wed, 13 Nov 2024 20:54:48 +0100 Subject: [PATCH 1/4] add high contention tests --- test/test_threads.jl | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/test_threads.jl b/test/test_threads.jl index 390ec17..d6d27f9 100644 --- a/test/test_threads.jl +++ b/test/test_threads.jl @@ -1,4 +1,7 @@ +using ProgressMeter +using Test + @testset "ProgressThreads tests" begin threads = Threads.nthreads() println("Testing Progress() with Threads.@threads across $threads threads") @@ -16,6 +19,32 @@ @test !any(vals .== 1) #Check that all elements have been iterated @test all(threadsUsed) #Ensure that all threads are used + println("Testing high-contention progress updates") + function test_high_contention_progress() + n_threads = Threads.nthreads() + n_iterations = n_threads * 1000 + p = Progress(n_iterations) + total_updates = Threads.Atomic{Int}(0) + + # Add different workload patterns + workload_patterns = [ + () -> sleep(0.0001), # Constant load + () -> sleep(0.0001 * rand()), # Random load + () -> sleep(0.001 * (sin(time()) + 1)), # Periodic load + ] + + Threads.@threads for i in 1:n_iterations + next!(p) + Threads.atomic_add!(total_updates, 1) + workload_patterns[1 + (i % length(workload_patterns))]() + end + + # Verify all expected updates occurred + @test total_updates[] == n_iterations + @test p.counter == n_iterations + end + test_high_contention_progress() + println("Testing ProgressUnknown() with Threads.@threads across $threads threads") trigger = 100.0 From c23a12b85f049931fc8ae37489a5b6a2a7c1f8b0 Mon Sep 17 00:00:00 2001 From: Younes IO Date: Mon, 18 Nov 2024 20:16:14 +0100 Subject: [PATCH 2/4] feat: add thread-safe progress meter --- Project.toml | 5 +- src/ProgressMeter.jl | 242 ++++++++++++++++++++++++++------- test/async_progress_tests.jl | 83 +++++++++++ test/performance_benchmarks.jl | 142 +++++++++++++++++++ 4 files changed, 424 insertions(+), 48 deletions(-) create mode 100644 test/async_progress_tests.jl create mode 100644 test/performance_benchmarks.jl diff --git a/Project.toml b/Project.toml index 13c6853..ddb7f66 100644 --- a/Project.toml +++ b/Project.toml @@ -3,17 +3,20 @@ uuid = "92933f4c-e287-5a05-a399-4b506db050ca" version = "1.10.2" [deps] +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" [compat] +BenchmarkTools = "1.5.0" julia = "1.6" [extras] +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test", "Random", "Distributed", "InteractiveUtils"] +test = ["Test", "Random", "Distributed", "InteractiveUtils", "BenchmarkTools"] diff --git a/src/ProgressMeter.jl b/src/ProgressMeter.jl index 15a9e4a..0558477 100644 --- a/src/ProgressMeter.jl +++ b/src/ProgressMeter.jl @@ -2,6 +2,7 @@ module ProgressMeter using Printf: @sprintf using Distributed +using Base.Threads: atomic_add!, atomic_sub!, atomic_cas! export Progress, ProgressThresh, ProgressUnknown, BarGlyphs, next!, update!, cancel, finish!, @showprogress, progress_map, progress_pmap, ijulia_behavior @@ -76,7 +77,7 @@ Base.@kwdef mutable struct ProgressCore showspeed::Bool = false # should the output include average time per iteration # internals check_iterations::Int = 1 # number of iterations to check time for - counter::Int = 0 # current iteration + counter::Threads.Atomic{Int}= Threads.Atomic{Int}(0) # atomic counter for thread-safe increments lock::Threads.ReentrantLock = Threads.ReentrantLock() # lock used when threading detected numprintedvalues::Int = 0 # num values printed below progress in last iteration prev_update_count::Int = 1 # counter at last update @@ -86,6 +87,12 @@ Base.@kwdef mutable struct ProgressCore tinit::Float64 = time() # time meter was initialized tlast::Float64 = time() # time of last update tsecond::Float64 = time() # ignore the first loop given usually uncharacteristically slow + max_iterations::Int = typemax(Int) # maximum number of iterations + + # Background thread management + background_thread::Union{Nothing, Task} = nothing # Dedicated progress tracking thread + update_channel::Channel{Tuple{Int, Dict{Symbol, Any}}} = Channel{Tuple{Int, Dict{Symbol, Any}}}(32) # Non-blocking update channel + stop_background_thread::Threads.Atomic{Bool} = Threads.Atomic{Bool}(false) # Atomic flag to stop background thread end """ @@ -384,6 +391,10 @@ function _updateProgress!(p::ProgressUnknown; showvalues = (), truncate_lines = p.offset = offset p.color = color p.desc = desc + + # Safely dereference the atomic counter + current_count = p.counter[] + if p.done if p.printed t = time() @@ -393,10 +404,10 @@ function _updateProgress!(p::ProgressUnknown; showvalues = (), truncate_lines = msg = @sprintf "%c %s Time: %s" spinner_char(p, spinner) p.desc dur p.spincounter += 1 else - msg = @sprintf "%s %d Time: %s" p.desc p.counter dur + msg = @sprintf "%s %d Time: %s" p.desc current_count dur end if p.showspeed - sec_per_iter = elapsed_time / p.counter + sec_per_iter = elapsed_time / current_count msg = @sprintf "%s (%s)" msg speedstring(sec_per_iter) end print(p.output, "\n" ^ (p.offset + p.numprintedvalues)) @@ -414,7 +425,7 @@ function _updateProgress!(p::ProgressUnknown; showvalues = (), truncate_lines = end if force || ignore_predictor || predicted_updates_per_dt_have_passed(p) t = time() - if p.counter > 2 + if current_count > 2 p.check_iterations = calc_check_iterations(p, t) end if force || (t > p.tlast+p.dt) @@ -423,11 +434,11 @@ function _updateProgress!(p::ProgressUnknown; showvalues = (), truncate_lines = msg = @sprintf "%c %s Time: %s" spinner_char(p, spinner) p.desc dur p.spincounter += 1 else - msg = @sprintf "%s %d Time: %s" p.desc p.counter dur + msg = @sprintf "%s %d Time: %s" p.desc current_count dur end if p.showspeed elapsed_time = t - p.tinit - sec_per_iter = elapsed_time / p.counter + sec_per_iter = elapsed_time / current_count msg = @sprintf "%s (%s)" msg speedstring(sec_per_iter) end print(p.output, "\n" ^ (p.offset + p.numprintedvalues)) @@ -441,13 +452,18 @@ function _updateProgress!(p::ProgressUnknown; showvalues = (), truncate_lines = # connection. p.tlast = t + 2*(time()-t) p.printed = true - p.prev_update_count = p.counter + p.prev_update_count = current_count end end return nothing end -predicted_updates_per_dt_have_passed(p::AbstractProgress) = p.counter - p.prev_update_count >= p.check_iterations +function predicted_updates_per_dt_have_passed(p::AbstractProgress) + # Get the current counter value directly + current = p.counter[] + prev = p.prev_update_count + return current - prev >= p.check_iterations +end function is_threading(p::AbstractProgress) p.safe_lock == 0 && return false @@ -481,10 +497,17 @@ the last update, this may or may not result in a change to the display. You may optionally change the `color` of the display. See also `update!`. """ function next!(p::Union{Progress, ProgressUnknown}; step::Int = 1, options...) - lock_if_threading(p) do - p.counter += step - updateProgress!(p; ignore_predictor = step == 0, options...) + # Non-blocking atomic increment + current_count = atomic_add!(p.counter, step) + + # Check if we've exceeded max iterations + if current_count + step > p.max_iterations + finish!(p) + return nothing end + + # Lightweight update mechanism + updateProgress!(p; ignore_predictor = step == 0, options...) end """ @@ -497,11 +520,16 @@ this may or may not result in a change to the display. You may optionally change the color of the display. See also `next!`. """ function update!(p::Union{Progress, ProgressUnknown}, counter::Int=p.counter; options...) - lock_if_threading(p) do - counter_changed = p.counter != counter - p.counter = counter - updateProgress!(p; ignore_predictor = !counter_changed, options...) + # Atomic compare-and-swap for thread-safe updates + atomic_cas!(p.counter, p.counter[], counter) + + # Check if we've exceeded max iterations + if counter > p.max_iterations + finish!(p) + return nothing end + + updateProgress!(p; ignore_predictor = false, options...) end """ @@ -566,9 +594,87 @@ function finish!(p::ProgressThresh; options...) end function finish!(p::ProgressUnknown; options...) - lock_if_threading(p) do - p.done = true - updateProgress!(p; options...) + try + lock_if_threading(p) do + p.done = true + updateProgress!(p; options...) + end + + # Ensure background thread stops + if p.core.background_thread !== nothing + p.core.stop_background_thread[] = true + + # Put a sentinel value to unblock the channel + try + put!(p.core.update_channel, (-1, Dict{Symbol, Any}())) + catch + # Ignore if channel is already closed + end + + # Wait for the thread to complete and set to nothing + wait(p.core.background_thread) + p.core.background_thread = nothing + end + catch ex + # Minimal error handling + @error "Error during progress finish" exception=ex + end +end + +# Background thread management function +function start_background_progress_thread!(p::AbstractProgress) + p.core.background_thread = Threads.@spawn begin + try + while !p.core.stop_background_thread[] + yield() + + try + update_data = take!(p.core.update_channel) + + # Exit if sentinel value received + if update_data[1] == -1 + break + end + + # Process update + counter, options = update_data + updateProgress!(p; options...) + catch e + if e isa InterruptException + break + elseif e isa Base.Channel.ChanClosedError + break + else + break + end + end + end + finally + p.core.stop_background_thread[] = true + close(p.core.update_channel) + end + end +end + +# Enhanced progress update method with background thread support +function background_update!(p::AbstractProgress, counter::Int; options...) + # If background thread not started, initialize it + if p.core.background_thread === nothing + start_background_progress_thread!(p) + end + + # Non-blocking put to update channel with timeout + try + # Atomically increment the counter + current_count = atomic_add!(p.counter, counter) + + put!(p.core.update_channel, (current_count + counter, Dict{Symbol, Any}(options))) + catch e + if e isa Base.Channel.ChanClosedError + @warn "Update channel closed, cannot send update" + else + rethrow(e) + end end end @@ -701,22 +807,23 @@ end function showprogress_process_expr(node, metersym) if !isa(node, Expr) - node + return node elseif node.head === :break || node.head === :return # special handling for break and return statements - quote + return quote ($finish!)($metersym) $node end - elseif node.head === :for || node.head === :while + elseif node.head in (:for, :while) # do not process inner loops # # FIXME: do not process break and return statements in inner functions # either - node + return Expr(node.head, + [showprogress_process_expr(arg, metersym) for arg in node.args]...) else # process each subexpression recursively - Expr(node.head, [showprogress_process_expr(a, metersym) for a in node.args]...) + return Expr(node.head, [showprogress_process_expr(a, metersym) for a in node.args]...) end end @@ -765,15 +872,20 @@ function showprogressdistributed(args...) na = length(distargs) if na == 1 loop = distargs[1] + reducer = nothing elseif na == 2 reducer = distargs[1] loop = distargs[2] else - println("$distargs $na") - throw(ArgumentError("wrong number of arguments to @distributed")) + throw(ArgumentError("wrong number of arguments to @distributed: expected 1 or 2, got $na")) end if loop.head !== :for - throw(ArgumentError("malformed @distributed loop")) + # More flexible error handling for distributed loops + @warn "Unsupported loop type for @distributed: expected a for loop, got $(loop.head)" + return expr # Return original expression + end + if na == 2 && reducer === nothing + throw(ArgumentError("reducer cannot be nothing when using @distributed")) end var = loop.args[1].args[1] r = loop.args[1].args[2] @@ -874,8 +986,15 @@ function showprogress(args...) expr.args[2] = showprogress(progressargs..., expr.args[2]) return expr - elseif expr.head in (:for, :comprehension, :typed_comprehension) - return showprogress_loop(expr, progressargs) + elseif expr.head in (:for, :comprehension, :typed_comprehension, :generator) + try + result = showprogress_loop(expr, progressargs...) + return result + catch e + # Enhanced error reporting for progress tracking failures + @warn "Failed to process progress for $(expr.head) expression" exception=e + return expr # Fallback to original expression if processing fails + end elseif expr.head == :call return showprogress_map(expr, progressargs) @@ -945,22 +1064,36 @@ function showprogress_loop(expr, progressargs) if expr.head == :for outerassignidx = 1 loopbodyidx = lastindex(expr.args) - elseif expr.head == :comprehension + elseif expr.head in (:comprehension, :generator, :typed_comprehension) outerassignidx = lastindex(expr.args) - loopbodyidx = 1 - elseif expr.head == :typed_comprehension + loopbodyidx = expr.head === :typed_comprehension ? 2 : 1 + else + # More informative error message for unsupported expression types + throw(ArgumentError("Unsupported expression type for progress tracking: $(expr.head). Supported types are: for, comprehension, generator, typed_comprehension")) + end + + # Convert generator to comprehension if needed + if expr.head == :generator + expr = Expr(:comprehension, expr) outerassignidx = lastindex(expr.args) - loopbodyidx = 2 + loopbodyidx = 1 end + # As of julia 0.5, a comprehension's "loop" is actually one level deeper in the syntax tree. if expr.head !== :for - @assert length(expr.args) == loopbodyidx + if length(expr.args) < loopbodyidx + # More descriptive error for insufficient arguments + @warn "Insufficient arguments in $(expr.head) expression. Ensure the expression has the correct structure." + return expr # Fallback to original expression + end expr = expr.args[outerassignidx] = copy(expr.args[outerassignidx]) if expr.head == :flatten # e.g. [x for x in 1:10 for y in 1:x] expr = expr.args[1] = copy(expr.args[1]) end - @assert expr.head === :generator + if expr.head !== :generator + throw(ArgumentError("Expected generator expression, got $(expr.head)")) + end outerassignidx = lastindex(expr.args) loopbodyidx = 1 end @@ -993,8 +1126,16 @@ function showprogress_loop(expr, progressargs) loopassign.args[2] = :(ProgressWrapper(iterable, $(esc(metersym)))) # Transform the loop body break and return statements - if expr.head === :for - expr.args[loopbodyidx] = showprogress_process_expr(expr.args[loopbodyidx], metersym) + if expr.head in (:for, :comprehension, :typed_comprehension, :generator) + # More robust processing of loop body expressions + try + processed_body = showprogress_process_expr(expr.args[loopbodyidx], metersym) + expr.args[loopbodyidx] = processed_body + catch e + # Enhanced error handling for body processing + @warn "Failed to process loop body for $(expr.head) expression" exception=e + # Keep original body if processing fails + end end # Escape all args except the loop assignment, which was already appropriately escaped. @@ -1015,21 +1156,28 @@ function showprogress_loop(expr, progressargs) $(esc(metersym)) = Progress(length(iterable), $(showprogress_process_args(progressargs)...)) end - if expr.head === :for + if expr.head in (:for, :generator, :comprehension, :typed_comprehension) return quote + # Enhanced setup with more robust error handling $setup - $expr - end - else - # We're dealing with a comprehension - return quote - begin - $setup - rv = $orig - finish!($(esc(metersym))) - rv + rv = try + # Ensure progress meter is always finished, even if an error occurs + local result + $orig + catch e + @warn "Error in progress-tracked expression" exception=e + rethrow(e) + finally + try + finish!($(esc(metersym))) + catch + # Ignore errors during finish + end end + rv end + else + throw(ArgumentError("Unsupported expression type: $(expr.head)")) end end diff --git a/test/async_progress_tests.jl b/test/async_progress_tests.jl new file mode 100644 index 0000000..6031e6c --- /dev/null +++ b/test/async_progress_tests.jl @@ -0,0 +1,83 @@ +module AsyncProgressTests + +using Test +using ProgressMeter: start_background_progress_thread!, background_update!, ProgressUnknown, finish! +using Statistics +using Base.Threads: atomic_add! + +@testset "Progress Background Thread" begin + @testset "Thread-Safe Progress Updates" begin + p = ProgressUnknown(desc="Concurrent Updates") + start_background_progress_thread!(p) + + total_steps = 500 + update_states = Vector{Float64}(undef, Threads.nthreads()) + + @sync begin + Threads.@threads for thread_id in 1:Threads.nthreads() + local thread_steps = 0 + + for i in 1:total_steps + background_update!(p, 1) + thread_steps = i + end + + update_states[thread_id] = thread_steps / total_steps + end + end + + # Verify meaningful progress across threads + @test all(state -> state ≈ 1.0, update_states) + end + + @testset "Background Thread Lifecycle" begin + p = ProgressUnknown(desc="Lifecycle Test") + + @test begin + start_background_progress_thread!(p) + + # Verify background thread and channel setup + @info "Before finish!" p.core.background_thread p.core.update_channel p.core.stop_background_thread[] + + p.core.background_thread !== nothing && + !isopen(p.core.update_channel) == false && + !p.core.stop_background_thread[] + end + + @test begin + finish!(p) + + # Add a small delay to allow thread termination + sleep(0.1) + + @info "After finish!" p.core.background_thread p.core.update_channel p.core.stop_background_thread[] + + # Verify thread termination + p.core.background_thread === nothing && + isopen(p.core.update_channel) == false && + p.core.stop_background_thread[] + end + end + + @testset "Update Channel Performance" begin + p = ProgressUnknown(desc="Channel Performance") + start_background_progress_thread!(p) + + update_times = Float64[] + total_updates = 1000 + + for i in 1:total_updates + start_time = time() + background_update!(p, i) + push!(update_times, time() - start_time) + end + + finish!(p) + + # Verify low-latency updates + @test mean(update_times) < 0.001 # Under 1ms per update + @test std(update_times) < 0.0005 # Low variance + end +end + +end # module AsyncProgressTests diff --git a/test/performance_benchmarks.jl b/test/performance_benchmarks.jl new file mode 100644 index 0000000..4a02697 --- /dev/null +++ b/test/performance_benchmarks.jl @@ -0,0 +1,142 @@ +module PerformanceBenchmarks + +using Test +using ProgressMeter +using ProgressMeter: start_background_progress_thread!, background_update!, finish! +using BenchmarkTools +using Base.Threads: atomic_add! +using Statistics + +@testset "Progress Meter Performance" begin + @testset "Background Thread Overhead" begin + # Simulate complex, computationally intensive work + function heavy_computational_work(complexity) + result = 0.0 + # Nested loops to increase computational complexity + for _ in 1:complexity + result += sum( + sin(x) * cos(x) * tan(x) + for x in range(0, π, length=1000) + ) + # Add matrix-like operations to increase complexity + result *= sqrt(result) + end + return result + end + + creation_times = [ + @elapsed begin + p = ProgressUnknown(desc="Heavy Computation") + start_time = time() + ProgressMeter.start_background_progress_thread!(p) + + total_steps = 500 + complexity_levels = [10, 50, 100] # Varying computational intensity + thread_results = zeros(Threads.nthreads()) + + @sync Threads.@threads for thread_id in 1:Threads.nthreads() + # Dynamically assign complexity based on thread + thread_complexity = complexity_levels[mod1(thread_id, length(complexity_levels))] + + local thread_result = 0.0 + local thread_steps = 0 + + for i in 1:total_steps + # Substantial computational work with variable complexity + thread_result += heavy_computational_work(thread_complexity) + ProgressMeter.background_update!(p, i) + thread_steps = i + end + + thread_results[thread_id] = thread_result + end + + ProgressMeter.finish!(p) + total_time = time() - start_time + end + for _ in 1:50 + ] + + # More relaxed performance constraints + @test mean(creation_times) < 1.0 # Under 1 second per tracking + @test std(creation_times) < 0.5 # Lower variance + end + + @testset "Update Channel Performance" begin + # Test non-blocking, lock-free update mechanism + p = ProgressUnknown() + start_background_progress_thread!(p) + + # Use a thread-safe data structure with atomic operations + update_times = zeros(Float64, 1000) + update_counter = Threads.Atomic{Int}(0) + + # Measure time to put updates in channel with simulated contention + total_updates = 1000 + Threads.@threads for i in 1:total_updates + start = time() + background_update!(p, i) + + # Atomic, thread-safe update of shared array + idx = atomic_add!(update_counter, 1) + 1 + update_times[idx] = time() - start + end + + finish!(p) + + # Filter out any zero values + valid_times = filter(x -> x > 0, update_times) + + # Verify update mechanism performance + @test mean(valid_times) < 0.001 # Under 1ms per update + @test std(valid_times) < 0.0005 # Low variance + end + + @testset "Atomic Progress State" begin + # Test thread-safe progress updates with atomic operations + p = ProgressUnknown(desc="Atomic Progress Test") + start_background_progress_thread!(p) + + # Use atomic counter to track updates across threads + update_counter = Threads.Atomic{Int}(0) + total_updates = 1000 + + Threads.@threads for _ in 1:total_updates + # Simulate work and update progress + background_update!(p, 1) + + # Atomically increment a shared counter + atomic_add!(update_counter, 1) + end + + finish!(p) + + # Verify all updates were processed + @test update_counter[] == total_updates + end + + @testset "Total Update Channel Performance" begin + # Test total update channel performance and throughput + p = ProgressUnknown(desc="Total Update Performance") + start_background_progress_thread!(p) + + total_updates = 10000 + start_time = time() + + Threads.@threads for i in 1:total_updates + background_update!(p, 1) + end + + finish!(p) + total_time = time() - start_time + + # Debug information + @info "Counter details" p.counter[] total_updates + + # Verify total update performance + @test total_time < 1.0 # Total update time under 1 second + @test p.counter[] == total_updates # All updates processed + end +end + +end \ No newline at end of file From 4743dc04f1546e93e2262d6158e6ad8ace57481b Mon Sep 17 00:00:00 2001 From: Younes IO Date: Mon, 18 Nov 2024 20:17:45 +0100 Subject: [PATCH 3/4] rollback --- test/test_threads.jl | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/test/test_threads.jl b/test/test_threads.jl index d6d27f9..ce33187 100644 --- a/test/test_threads.jl +++ b/test/test_threads.jl @@ -19,33 +19,7 @@ using Test @test !any(vals .== 1) #Check that all elements have been iterated @test all(threadsUsed) #Ensure that all threads are used - println("Testing high-contention progress updates") - function test_high_contention_progress() - n_threads = Threads.nthreads() - n_iterations = n_threads * 1000 - p = Progress(n_iterations) - total_updates = Threads.Atomic{Int}(0) - - # Add different workload patterns - workload_patterns = [ - () -> sleep(0.0001), # Constant load - () -> sleep(0.0001 * rand()), # Random load - () -> sleep(0.001 * (sin(time()) + 1)), # Periodic load - ] - - Threads.@threads for i in 1:n_iterations - next!(p) - Threads.atomic_add!(total_updates, 1) - workload_patterns[1 + (i % length(workload_patterns))]() - end - - # Verify all expected updates occurred - @test total_updates[] == n_iterations - @test p.counter == n_iterations - end - test_high_contention_progress() - - + println("Testing ProgressUnknown() with Threads.@threads across $threads threads") trigger = 100.0 prog = ProgressUnknown(desc="Attempts at exceeding trigger:") From d6a7f7481479f882b599420e48fd9280f331b683 Mon Sep 17 00:00:00 2001 From: Younes IO Date: Mon, 18 Nov 2024 20:18:35 +0100 Subject: [PATCH 4/4] cleanup --- test/test_threads.jl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/test_threads.jl b/test/test_threads.jl index ce33187..655b577 100644 --- a/test/test_threads.jl +++ b/test/test_threads.jl @@ -1,7 +1,4 @@ -using ProgressMeter -using Test - @testset "ProgressThreads tests" begin threads = Threads.nthreads() println("Testing Progress() with Threads.@threads across $threads threads") @@ -19,7 +16,6 @@ using Test @test !any(vals .== 1) #Check that all elements have been iterated @test all(threadsUsed) #Ensure that all threads are used - println("Testing ProgressUnknown() with Threads.@threads across $threads threads") trigger = 100.0 prog = ProgressUnknown(desc="Attempts at exceeding trigger:")