From 03369a8b89dd9c66a6b79c09b6e4c781a1c59469 Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Thu, 9 Jul 2026 21:04:12 +0200 Subject: [PATCH 01/12] tweak how memory of worker effect number of parallel jobs --- README.md | 4 ++- src/ParallelTestRunner.jl | 51 ++++++++++++++++++++++++++++----------- test/runtests.jl | 37 +++++++++++++++++++++++----- 3 files changed, 71 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index dccb64ff..947819ff 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,9 @@ Usage: runtests.jl [--help] [--list] [--jobs=N] [TESTS...] --list List all available tests. --verbose Print more information during testing. --quickfail Fail the entire run as soon as a single test errored. - --jobs=N Launch `N` processes to perform tests. + --jobs=N Launch `N` processes to perform tests. Can also be set + with the PARALLELTESTRUNNER_NUM_JOBS environment + variable, with `--jobs=N` taking precedence. Remaining arguments filter the tests that will be executed. ``` diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index b08642da..e908140b 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -496,17 +496,26 @@ available_memory() = Sys.free_memory() end -# This is an internal function, not to be used by end users. The keyword -# arguments are only for testing purposes. +# Assumed memory footprint of a single test worker, used to clamp the default +# number of jobs on memory-constrained machines (e.g. many cores but little +# total memory). Packages whose tests are heavier can pass a larger +# `memory_per_worker` to `runtests`. +const DEFAULT_MEMORY_PER_WORKER = Int64(2)^30 + +# This is an internal function, not to be used by end users. The +# `cpu_threads` and `total_memory` keyword arguments are only for testing +# purposes. """ - default_njobs() + default_njobs(; memory_per_worker = 2^30) -Determine default number of parallel jobs. +Determine default number of parallel jobs: the number of CPU threads, clamped +such that each worker can be assumed to use `memory_per_worker` bytes of the +total system memory. """ -function default_njobs(; cpu_threads = Sys.CPU_THREADS, free_memory = available_memory()) - jobs = cpu_threads - memory_jobs = Int64(free_memory) ÷ (2 * Int64(2)^30) - return max(1, min(jobs, memory_jobs)) +function default_njobs(; cpu_threads = Sys.CPU_THREADS, total_memory = Sys.total_memory(), + memory_per_worker = DEFAULT_MEMORY_PER_WORKER) + memory_jobs = Int64(total_memory) ÷ Int64(memory_per_worker) + return max(1, min(cpu_threads, memory_jobs)) end # Historical test duration database @@ -723,7 +732,9 @@ function parse_args(args; custom::Array{String} = String[]) --list List all available tests. --verbose Print more information during testing. --quickfail Fail the entire run as soon as a single test errored. - --jobs=N Launch `N` processes to perform tests.""" + --jobs=N Launch `N` processes to perform tests. Can also be set + with the PARALLELTESTRUNNER_NUM_JOBS environment + variable, with `--jobs=N` taking precedence.""" if !isempty(custom) usage *= "\n\nCustom arguments:" @@ -799,7 +810,8 @@ end env = Vector{Pair{String, String}}(), stdout = Base.stdout, stderr = Base.stderr, - max_worker_rss = get_max_worker_rss()) + max_worker_rss = get_max_worker_rss(), + memory_per_worker = 2^30) runtests(mod::Module, ARGS; ...) Run Julia tests in parallel across multiple worker processes. @@ -842,6 +854,11 @@ Several keyword arguments are also supported: `test_worker` hook are the caller's responsibility. - `stdout` and `stderr`: I/O streams to write to (default: `Base.stdout` and `Base.stderr`) - `max_worker_rss`: RSS threshold where a worker will be restarted once it is reached. +- `memory_per_worker`: Assumed memory footprint (in bytes) of a single worker, used to + clamp the default number of jobs on memory-constrained machines (default: 1 GiB). + Packages whose tests use a lot of memory can pass a larger value to reduce the default + parallelism. Ignored when the number of jobs is set explicitly via `--jobs=N` or the + `PARALLELTESTRUNNER_NUM_JOBS` environment variable. ## Command Line Options @@ -849,7 +866,9 @@ Several keyword arguments are also supported: - `--list`: List all available test files and exit - `--verbose`: Print more detailed information during test execution - `--quickfail`: Stop the entire test run as soon as any test fails -- `--jobs=N`: Use N worker processes (default: based on CPU threads and available memory) +- `--jobs=N`: Use N worker processes (default: based on CPU threads and total memory; + can also be set with the `PARALLELTESTRUNNER_NUM_JOBS` environment variable, with + `--jobs=N` taking precedence) - `TESTS...`: Filter test files by name, matched using `startswith` ## Behavior @@ -922,7 +941,8 @@ function runtests(mod::Module, args::ParsedArgs; exename = nothing, exeflags = nothing, env = Vector{Pair{String, String}}(), - stdout = Base.stdout, stderr = Base.stderr, max_worker_rss = get_max_worker_rss()) + stdout = Base.stdout, stderr = Base.stderr, max_worker_rss = get_max_worker_rss(), + memory_per_worker = DEFAULT_MEMORY_PER_WORKER) # # set-up @@ -962,6 +982,7 @@ function runtests(mod::Module, args::ParsedArgs; stdout, stderr, max_worker_rss, + memory_per_worker, ) end @@ -981,12 +1002,14 @@ function _runtests(mod::Module, args::ParsedArgs; stdout = Base.stdout, stderr = Base.stderr, max_worker_rss = get_max_worker_rss(), + memory_per_worker = DEFAULT_MEMORY_PER_WORKER, ) # determine parallelism - jobs = something(args.jobs, default_njobs()) + env_jobs = tryparse(Int, get(ENV, "PARALLELTESTRUNNER_NUM_JOBS", "")) + jobs = @something args.jobs env_jobs default_njobs(; memory_per_worker) jobs = clamp(jobs, 1, length(tests)) - println(stdout, "Running $(length(tests)) tests using $jobs parallel jobs. If this is too many concurrent jobs, specify the `--jobs=N` argument to the tests, or set the `JULIA_CPU_THREADS` environment variable.") + println(stdout, "Running $(length(tests)) tests using $jobs parallel jobs. To change the number of jobs, specify the `--jobs=N` argument to the tests, or set the `PARALLELTESTRUNNER_NUM_JOBS` environment variable.") !isnothing(args.verbose) && println(stdout, "Available memory: $(Base.format_bytes(available_memory()))") sem = Base.Semaphore(max(1, jobs)) worker_pool = Channel{Union{Nothing, PTRWorker}}(jobs) diff --git a/test/runtests.jl b/test/runtests.jl index 15266819..639ceab3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -40,12 +40,37 @@ include(joinpath(@__DIR__, "utils.jl")) end @testset "default njobs" begin - @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 28) == 1 - @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 30) == 1 - @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 31) == 1 - @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 32) == 2 - @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 33) == 4 - @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 34) == 4 + @test ParallelTestRunner.default_njobs(; cpu_threads=4, total_memory=UInt64(2) ^ 28) == 1 + @test ParallelTestRunner.default_njobs(; cpu_threads=4, total_memory=UInt64(2) ^ 30) == 1 + @test ParallelTestRunner.default_njobs(; cpu_threads=4, total_memory=UInt64(2) ^ 31) == 2 + @test ParallelTestRunner.default_njobs(; cpu_threads=4, total_memory=UInt64(2) ^ 32) == 4 + @test ParallelTestRunner.default_njobs(; cpu_threads=4, total_memory=UInt64(2) ^ 33) == 4 + + # heavier per-worker memory estimate lowers the default + @test ParallelTestRunner.default_njobs(; cpu_threads=4, total_memory=UInt64(2) ^ 32, + memory_per_worker=2 * Int64(2) ^ 30) == 2 +end + +@testset "number of jobs" begin + testsuite = Dict( + "t1" => :(@test true), + "t2" => :(@test true), + "t3" => :(@test true), + ) + + # environment variable overrides the default + io = IOBuffer() + withenv("PARALLELTESTRUNNER_NUM_JOBS" => "2") do + runtests(ParallelTestRunner, String[]; testsuite, stdout=io, stderr=io) + end + @test contains(String(take!(io)), "using 2 parallel jobs") + + # --jobs takes precedence over the environment variable + io = IOBuffer() + withenv("PARALLELTESTRUNNER_NUM_JOBS" => "2") do + runtests(ParallelTestRunner, ["--jobs=1"]; testsuite, stdout=io, stderr=io) + end + @test contains(String(take!(io)), "using 1 parallel jobs") end @testset "subdir use" begin From d41bcad2f1fb9a70c7491b3ef98b6b6a042cf7f4 Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Mon, 13 Jul 2026 16:06:48 +0200 Subject: [PATCH 02/12] Use available memory instead of total for default job count Rounding division instead of floor so e.g. the 7 GB macOS CI runners (~3.3 GiB available) still get all 3 workers. Co-Authored-By: Claude Fable 5 --- src/ParallelTestRunner.jl | 12 ++++++------ test/runtests.jl | 17 +++++++++++------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index e908140b..48999d08 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -498,23 +498,23 @@ end # Assumed memory footprint of a single test worker, used to clamp the default # number of jobs on memory-constrained machines (e.g. many cores but little -# total memory). Packages whose tests are heavier can pass a larger +# memory). Packages whose tests are heavier can pass a larger # `memory_per_worker` to `runtests`. const DEFAULT_MEMORY_PER_WORKER = Int64(2)^30 # This is an internal function, not to be used by end users. The -# `cpu_threads` and `total_memory` keyword arguments are only for testing +# `cpu_threads` and `free_memory` keyword arguments are only for testing # purposes. """ default_njobs(; memory_per_worker = 2^30) Determine default number of parallel jobs: the number of CPU threads, clamped such that each worker can be assumed to use `memory_per_worker` bytes of the -total system memory. +available system memory. """ -function default_njobs(; cpu_threads = Sys.CPU_THREADS, total_memory = Sys.total_memory(), +function default_njobs(; cpu_threads = Sys.CPU_THREADS, free_memory = available_memory(), memory_per_worker = DEFAULT_MEMORY_PER_WORKER) - memory_jobs = Int64(total_memory) ÷ Int64(memory_per_worker) + memory_jobs = round(Int, Int64(free_memory) / memory_per_worker) return max(1, min(cpu_threads, memory_jobs)) end @@ -866,7 +866,7 @@ Several keyword arguments are also supported: - `--list`: List all available test files and exit - `--verbose`: Print more detailed information during test execution - `--quickfail`: Stop the entire test run as soon as any test fails -- `--jobs=N`: Use N worker processes (default: based on CPU threads and total memory; +- `--jobs=N`: Use N worker processes (default: based on CPU threads and available memory; can also be set with the `PARALLELTESTRUNNER_NUM_JOBS` environment variable, with `--jobs=N` taking precedence) - `TESTS...`: Filter test files by name, matched using `startswith` diff --git a/test/runtests.jl b/test/runtests.jl index 639ceab3..e91a3d5b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -40,14 +40,19 @@ include(joinpath(@__DIR__, "utils.jl")) end @testset "default njobs" begin - @test ParallelTestRunner.default_njobs(; cpu_threads=4, total_memory=UInt64(2) ^ 28) == 1 - @test ParallelTestRunner.default_njobs(; cpu_threads=4, total_memory=UInt64(2) ^ 30) == 1 - @test ParallelTestRunner.default_njobs(; cpu_threads=4, total_memory=UInt64(2) ^ 31) == 2 - @test ParallelTestRunner.default_njobs(; cpu_threads=4, total_memory=UInt64(2) ^ 32) == 4 - @test ParallelTestRunner.default_njobs(; cpu_threads=4, total_memory=UInt64(2) ^ 33) == 4 + @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 28) == 1 + @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 30) == 1 + @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 31) == 2 + @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 32) == 4 + @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 33) == 4 + + # memory jobs are rounded, not floored (a 7 GB CI runner with ~3.3 GiB + # available should still get 3 workers) + @test ParallelTestRunner.default_njobs(; cpu_threads=3, free_memory=round(UInt64, 3.3 * 2^30)) == 3 + @test ParallelTestRunner.default_njobs(; cpu_threads=3, free_memory=round(UInt64, 2.7 * 2^30)) == 3 # heavier per-worker memory estimate lowers the default - @test ParallelTestRunner.default_njobs(; cpu_threads=4, total_memory=UInt64(2) ^ 32, + @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 32, memory_per_worker=2 * Int64(2) ^ 30) == 2 end From 097cd3f610ae0a29995c7d92cd7ca41b42e772e1 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:29:55 -0300 Subject: [PATCH 03/12] Fix merge --- src/ParallelTestRunner.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index 697065fd..36ffe978 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -543,12 +543,13 @@ such that each worker can be assumed to use `memory_per_worker` bytes of the available system memory. """ function default_njobs(; + memory_per_worker = DEFAULT_MEMORY_PER_WORKER, + ## private arguments for testing only # Just use Sys.EFFECTIVE_CPU_THREADS when min VERSION >= v"1.13" cpu_threads = (@static isdefined(Sys, :EFFECTIVE_CPU_THREADS) ? Sys.EFFECTIVE_CPU_THREADS : Sys.CPU_THREADS), free_memory = available_memory(), - memory_per_worker = DEFAULT_MEMORY_PER_WORKER ) - memory_jobs = Int64(free_memory) ÷ (2 * Int64(2)^30) + memory_jobs = round(Int, Int64(free_memory) / memory_per_worker) return max(1, min(cpu_threads, memory_jobs)) end From c1ab70aaa6bd731b823ee8c35f73808fe385bb99 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:36:02 -0300 Subject: [PATCH 04/12] Rename private default_njobs arguments --- src/ParallelTestRunner.jl | 10 +++++----- test/core.jl | 18 +++++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index 36ffe978..2ba5d1e9 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -533,7 +533,7 @@ end const DEFAULT_MEMORY_PER_WORKER = Int64(2)^30 # This is an internal function, not to be used by end users. The -# `cpu_threads` and `free_memory` keyword arguments are only for testing +# `_cpu_threads` and `_free_memory` keyword arguments are only for testing # purposes. """ default_njobs(; memory_per_worker = 2^30) @@ -546,11 +546,11 @@ function default_njobs(; memory_per_worker = DEFAULT_MEMORY_PER_WORKER, ## private arguments for testing only # Just use Sys.EFFECTIVE_CPU_THREADS when min VERSION >= v"1.13" - cpu_threads = (@static isdefined(Sys, :EFFECTIVE_CPU_THREADS) ? Sys.EFFECTIVE_CPU_THREADS : Sys.CPU_THREADS), - free_memory = available_memory(), + _cpu_threads = (@static isdefined(Sys, :EFFECTIVE_CPU_THREADS) ? Sys.EFFECTIVE_CPU_THREADS : Sys.CPU_THREADS), + _free_memory = available_memory(), ) - memory_jobs = round(Int, Int64(free_memory) / memory_per_worker) - return max(1, min(cpu_threads, memory_jobs)) + memory_jobs = round(Int, Int64(_free_memory) / memory_per_worker) + return max(1, min(_cpu_threads, memory_jobs)) end # Struct used in runtests to sort failed tests before successful ones diff --git a/test/core.jl b/test/core.jl index c5a27dee..33932308 100644 --- a/test/core.jl +++ b/test/core.jl @@ -33,20 +33,20 @@ end @testset "default njobs" begin - @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 28) == 1 - @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 30) == 1 - @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 31) == 2 - @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 32) == 4 - @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 33) == 4 - @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 34) == 4 + @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 28) == 1 + @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 30) == 1 + @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 31) == 2 + @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 32) == 4 + @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 33) == 4 + @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 34) == 4 # memory jobs are rounded, not floored (a 7 GB CI runner with ~3.3 GiB # available should still get 3 workers) - @test ParallelTestRunner.default_njobs(; cpu_threads=3, free_memory=round(UInt64, 3.3 * 2^30)) == 3 - @test ParallelTestRunner.default_njobs(; cpu_threads=3, free_memory=round(UInt64, 2.7 * 2^30)) == 3 + @test ParallelTestRunner.default_njobs(; _cpu_threads=3, _free_memory=round(UInt64, 3.3 * 2^30)) == 3 + @test ParallelTestRunner.default_njobs(; _cpu_threads=3, _free_memory=round(UInt64, 2.7 * 2^30)) == 3 # heavier per-worker memory estimate lowers the default - @test ParallelTestRunner.default_njobs(; cpu_threads=4, free_memory=UInt64(2) ^ 32, + @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 32, memory_per_worker=2 * Int64(2) ^ 30) == 2 end From 7f6033375600e68042db1d7554deacd1d6288ef6 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:47:17 -0300 Subject: [PATCH 05/12] Revert default per-runner memory change back to 2 GiB --- src/ParallelTestRunner.jl | 10 +++++----- test/core.jl | 17 ++++++++++------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index 2ba5d1e9..ac94f5d1 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -530,13 +530,13 @@ end # number of jobs on memory-constrained machines (e.g. many cores but little # memory). Packages whose tests are heavier can pass a larger # `memory_per_worker` to `runtests`. -const DEFAULT_MEMORY_PER_WORKER = Int64(2)^30 +const DEFAULT_MEMORY_PER_WORKER = 2 * Int64(2)^30 # This is an internal function, not to be used by end users. The # `_cpu_threads` and `_free_memory` keyword arguments are only for testing # purposes. """ - default_njobs(; memory_per_worker = 2^30) + default_njobs(; memory_per_worker = 2*2^30) Determine default number of parallel jobs: the number of CPU threads, clamped such that each worker can be assumed to use `memory_per_worker` bytes of the @@ -904,7 +904,7 @@ end stdout = Base.stdout, stderr = Base.stderr, max_worker_rss = get_max_worker_rss(), - memory_per_worker = 2^30) + memory_per_worker = 2*2^30) serial = String[], serial_position::Symbol = :before, recycle_on_failure::Bool = false, @@ -952,8 +952,8 @@ Several keyword arguments are also supported: - `stdout` and `stderr`: I/O streams to write to (default: `Base.stdout` and `Base.stderr`) - `max_worker_rss`: RSS threshold where a worker will be restarted once it is reached. - `memory_per_worker`: Assumed memory footprint (in bytes) of a single worker, used to - clamp the default number of jobs on memory-constrained machines (default: 1 GiB). - Packages whose tests use a lot of memory can pass a larger value to reduce the default + clamp the default number of jobs on memory-constrained machines (default: 2 GiB). + Packages whose tests use less memory can pass a smaller value to increase the default parallelism. Ignored when the number of jobs is set explicitly via `--jobs=N` or the `PARALLELTESTRUNNER_NUM_JOBS` environment variable. - `serial`: A vector of test names (keys of `testsuite`) that should be run one at a time diff --git a/test/core.jl b/test/core.jl index 33932308..76f3a77f 100644 --- a/test/core.jl +++ b/test/core.jl @@ -35,19 +35,22 @@ end @testset "default njobs" begin @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 28) == 1 @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 30) == 1 - @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 31) == 2 - @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 32) == 4 + @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 31) == 1 + @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 32) == 2 @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 33) == 4 @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 34) == 4 - # memory jobs are rounded, not floored (a 7 GB CI runner with ~3.3 GiB - # available should still get 3 workers) - @test ParallelTestRunner.default_njobs(; _cpu_threads=3, _free_memory=round(UInt64, 3.3 * 2^30)) == 3 - @test ParallelTestRunner.default_njobs(; _cpu_threads=3, _free_memory=round(UInt64, 2.7 * 2^30)) == 3 + # memory jobs are rounded, not floored + @test ParallelTestRunner.default_njobs(; _cpu_threads=3, _free_memory=round(UInt64, 6.3 * 2^30)) == 3 + @test ParallelTestRunner.default_njobs(; _cpu_threads=3, _free_memory=round(UInt64, 5.7 * 2^30)) == 3 # heavier per-worker memory estimate lowers the default @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 32, - memory_per_worker=2 * Int64(2) ^ 30) == 2 + memory_per_worker=3 * Int64(2) ^ 30) == 1 + + # lighter per-worker memory estimate increases the default + @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 32, + memory_per_worker=1 * Int64(2) ^ 30) == 4 end @testset "number of jobs" begin From 24556f1f8c02026444b55111fbc6f67444ec166b Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:13:28 -0300 Subject: [PATCH 06/12] Apply suggestion from @christiangnrd --- src/ParallelTestRunner.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index ac94f5d1..70e2e4af 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -549,7 +549,7 @@ function default_njobs(; _cpu_threads = (@static isdefined(Sys, :EFFECTIVE_CPU_THREADS) ? Sys.EFFECTIVE_CPU_THREADS : Sys.CPU_THREADS), _free_memory = available_memory(), ) - memory_jobs = round(Int, Int64(_free_memory) / memory_per_worker) + memory_jobs = Int64(_free_memory) ÷ memory_per_worker return max(1, min(_cpu_threads, memory_jobs)) end From d3b7510e6b530fd32aa656537b391ae81b071e4f Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:55:22 -0300 Subject: [PATCH 07/12] default_njobs docstring tweak --- src/ParallelTestRunner.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index 70e2e4af..400fdf0e 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -538,8 +538,8 @@ const DEFAULT_MEMORY_PER_WORKER = 2 * Int64(2)^30 """ default_njobs(; memory_per_worker = 2*2^30) -Determine default number of parallel jobs: the number of CPU threads, clamped -such that each worker can be assumed to use `memory_per_worker` bytes of the +Determine default number of parallel jobs. Calculated as the number of CPU threads, +clamped such that each worker can be assumed to use `memory_per_worker` bytes of the available system memory. """ function default_njobs(; From 1a56da6b8a531a29c06709d455e6352476d03c59 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:36:10 -0300 Subject: [PATCH 08/12] Remove outdated test --- test/core.jl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/core.jl b/test/core.jl index 76f3a77f..631433d4 100644 --- a/test/core.jl +++ b/test/core.jl @@ -40,10 +40,6 @@ end @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 33) == 4 @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 34) == 4 - # memory jobs are rounded, not floored - @test ParallelTestRunner.default_njobs(; _cpu_threads=3, _free_memory=round(UInt64, 6.3 * 2^30)) == 3 - @test ParallelTestRunner.default_njobs(; _cpu_threads=3, _free_memory=round(UInt64, 5.7 * 2^30)) == 3 - # heavier per-worker memory estimate lowers the default @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 32, memory_per_worker=3 * Int64(2) ^ 30) == 1 From d1a9f6a0162e09f5a83247ebcad8b4a347c3356d Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:53:58 -0300 Subject: [PATCH 09/12] Put info in docstring --- src/ParallelTestRunner.jl | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index 400fdf0e..ccfc1475 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -532,21 +532,19 @@ end # `memory_per_worker` to `runtests`. const DEFAULT_MEMORY_PER_WORKER = 2 * Int64(2)^30 -# This is an internal function, not to be used by end users. The -# `_cpu_threads` and `_free_memory` keyword arguments are only for testing -# purposes. """ - default_njobs(; memory_per_worker = 2*2^30) + default_njobs(; memory_per_worker = 2 * 2^30, + _cpu_threads = $(@static isdefined(Sys, :EFFECTIVE_CPU_THREADS) ? Symbol("Sys.EFFECTIVE_CPU_THREADS") : Symbol("Sys.CPU_THREADS")), + _free_memory = ParallelTestRunner.available_memory()) -Determine default number of parallel jobs. Calculated as the number of CPU threads, +*Internal* function used to determine the default number of parallel jobs. Calculated as the number of CPU threads, clamped such that each worker can be assumed to use `memory_per_worker` bytes of the available system memory. """ function default_njobs(; memory_per_worker = DEFAULT_MEMORY_PER_WORKER, - ## private arguments for testing only # Just use Sys.EFFECTIVE_CPU_THREADS when min VERSION >= v"1.13" - _cpu_threads = (@static isdefined(Sys, :EFFECTIVE_CPU_THREADS) ? Sys.EFFECTIVE_CPU_THREADS : Sys.CPU_THREADS), + _cpu_threads = (@static isdefined(Sys, :EFFECTIVE_CPU_THREADS) ? Symbol("Sys.EFFECTIVE_CPU_THREADS") : Symbol("Sys.CPU_THREADS")), _free_memory = available_memory(), ) memory_jobs = Int64(_free_memory) ÷ memory_per_worker From 1c3d403b5d3fce477370a6d8e080c284fd4ea4e6 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:56:07 -0300 Subject: [PATCH 10/12] PARALLELTESTRUNNER_NUM_JOBS -> PTR_NUM_JOBS --- README.md | 2 +- src/ParallelTestRunner.jl | 10 +++++----- test/core.jl | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4db2f610..7d35123f 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Usage: runtests.jl [--help] [--list] [--jobs=N] [TESTS...] --verbose Print more information during testing. --quickfail Fail the entire run as soon as a single test errored. --jobs=N Launch `N` processes to perform tests. Can also be set - with the PARALLELTESTRUNNER_NUM_JOBS environment + with the PTR_NUM_JOBS environment variable, with `--jobs=N` taking precedence. Remaining arguments filter the tests that will be executed. diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index ccfc1475..99abe6b7 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -782,7 +782,7 @@ function parse_args(args; custom::Array{String} = String[]) --verbose Print more information during testing. --quickfail Fail the entire run as soon as a single test errored. --jobs=N Launch `N` processes to perform tests. Can also be set - with the PARALLELTESTRUNNER_NUM_JOBS environment + with the PTR_NUM_JOBS environment variable, with `--jobs=N` taking precedence.""" if !isempty(custom) @@ -953,7 +953,7 @@ Several keyword arguments are also supported: clamp the default number of jobs on memory-constrained machines (default: 2 GiB). Packages whose tests use less memory can pass a smaller value to increase the default parallelism. Ignored when the number of jobs is set explicitly via `--jobs=N` or the - `PARALLELTESTRUNNER_NUM_JOBS` environment variable. + `PTR_NUM_JOBS` environment variable. - `serial`: A vector of test names (keys of `testsuite`) that should be run one at a time instead of in parallel. - `serial_position`: When to run serial tests relative to the parallel batch. @@ -971,7 +971,7 @@ Several keyword arguments are also supported: - `--verbose`: Print more detailed information during test execution - `--quickfail`: Stop the entire test run as soon as any test fails - `--jobs=N`: Use N worker processes (default: based on CPU threads and available memory; - can also be set with the `PARALLELTESTRUNNER_NUM_JOBS` environment variable, with + can also be set with the `PTR_NUM_JOBS` environment variable, with `--jobs=N` taking precedence) - `TESTS...`: Filter test files by name, matched using `startswith`. Arguments starting with '!' will instead be excluded from the test selection. @@ -1166,14 +1166,14 @@ function _runtests(mod::Module, args::ParsedArgs; serial_tests, parallel_tests = partition_tests(tests, serial) # determine parallelism - env_jobs = tryparse(Int, get(ENV, "PARALLELTESTRUNNER_NUM_JOBS", "")) + env_jobs = tryparse(Int, get(ENV, "PTR_NUM_JOBS", "")) _jobs = @something args.jobs env_jobs default_njobs(; memory_per_worker) jobs = clamp(_jobs, 1, max(1, length(parallel_tests))) worker_pool = Channel{Union{Nothing, PTRWorker}}(jobs) for _ in 1:jobs put!(worker_pool, nothing) end - println(stdout, "Running $(length(tests)) tests using $jobs parallel jobs. To change the number of jobs, specify the `--jobs=N` argument to the tests, or set the `PARALLELTESTRUNNER_NUM_JOBS` environment variable.") + println(stdout, "Running $(length(tests)) tests using $jobs parallel jobs. To change the number of jobs, specify the `--jobs=N` argument to the tests, or set the `PTR_NUM_JOBS` environment variable.") if !isempty(serial_tests) println(stdout, " $(length(serial_tests)) serial test(s) will run $(serial_position) the parallel batch.") end diff --git a/test/core.jl b/test/core.jl index 631433d4..aba37ef0 100644 --- a/test/core.jl +++ b/test/core.jl @@ -58,14 +58,14 @@ end # environment variable overrides the default io = IOBuffer() - withenv("PARALLELTESTRUNNER_NUM_JOBS" => "2") do + withenv("PTR_NUM_JOBS" => "2") do runtests(ParallelTestRunner, String[]; testsuite, stdout=io, stderr=io) end @test contains(String(take!(io)), "using 2 parallel jobs") # --jobs takes precedence over the environment variable io = IOBuffer() - withenv("PARALLELTESTRUNNER_NUM_JOBS" => "2") do + withenv("PTR_NUM_JOBS" => "2") do runtests(ParallelTestRunner, ["--jobs=1"]; testsuite, stdout=io, stderr=io) end @test contains(String(take!(io)), "using 1 parallel jobs") From 6eebee4cda1844aaa97ddc82bd3da5763a4e1eaf Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:57:59 -0300 Subject: [PATCH 11/12] Consistency --- src/ParallelTestRunner.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index 99abe6b7..053b37e0 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -902,7 +902,7 @@ end stdout = Base.stdout, stderr = Base.stderr, max_worker_rss = get_max_worker_rss(), - memory_per_worker = 2*2^30) + memory_per_worker = 2 * 2^30) serial = String[], serial_position::Symbol = :before, recycle_on_failure::Bool = false, From 25d01e79a07be45e5cda204c8c34a88e2c26440a Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:07:55 -0300 Subject: [PATCH 12/12] Oops --- src/ParallelTestRunner.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index 053b37e0..e7f10cf3 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -544,7 +544,7 @@ available system memory. function default_njobs(; memory_per_worker = DEFAULT_MEMORY_PER_WORKER, # Just use Sys.EFFECTIVE_CPU_THREADS when min VERSION >= v"1.13" - _cpu_threads = (@static isdefined(Sys, :EFFECTIVE_CPU_THREADS) ? Symbol("Sys.EFFECTIVE_CPU_THREADS") : Symbol("Sys.CPU_THREADS")), + _cpu_threads = (@static isdefined(Sys, :EFFECTIVE_CPU_THREADS) ? Sys.EFFECTIVE_CPU_THREADS : Sys.CPU_THREADS), _free_memory = available_memory(), ) memory_jobs = Int64(_free_memory) ÷ memory_per_worker