diff --git a/.github/actions/coverage-setup/action.yml b/.github/actions/coverage-setup/action.yml index 28c097a..54d4e09 100644 --- a/.github/actions/coverage-setup/action.yml +++ b/.github/actions/coverage-setup/action.yml @@ -59,11 +59,12 @@ runs: shell: bash run: julia --color=yes --project=OM.jl/OMLibraryTesting.jl -e 'using Pkg; Pkg.instantiate(); Pkg.precompile()' - # OMFrontend bundles MSL 3.2.3 and 4.0.0; another version is the installed - # library: Modelica, ModelicaServices and Complex (a single file in the - # repository, a folder with its package.mo when installed). + # OMFrontend bundles MSL 3.2.3 and 4.1.0 as single files; another version is + # the installed library: Modelica, ModelicaServices and Complex (a folder with + # its package.mo when installed). 4.1.0 is installed too: a bundle has no + # Resources (the files models read through modelica:// URIs). - name: Install MSL ${{ inputs.msl }} - if: inputs.msl != '3.2.3' && inputs.msl != '4.0.0' + if: inputs.msl != '3.2.3' shell: bash env: MSL_VERSION: ${{ inputs.msl }} diff --git a/Project.toml b/Project.toml index e3b6a71..f811cbe 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "OMLibraryTesting" uuid = "4866894a-ca78-4bef-9a5c-0d35f2f81ce1" -version = "0.3.2" +version = "0.3.3" authors = ["John Tinnerholm "] [deps] @@ -46,9 +46,9 @@ DoubleEnded = "0.2.3" ImmutableList = "0.4.2" ListUtil = "0.4.1" MetaModelica = "0.4.2" -OM = "0.6.1" -OMBackend = "0.7.0" -OMFrontend = "1.3.1" +OM = "0.7.0" +OMBackend = "0.8.0" +OMFrontend = "1.7.5" OMParser = "0.2.1" SCode = "1.2.0" diff --git a/README.md b/README.md index 0111cc6..43537c5 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,8 @@ results = OMLibraryTesting.run_coverage(domain = "Thermal", msl_version = "MSL:3 results = OMLibraryTesting.run_coverage(model = "Modelica.Thermal.HeatTransfer.Examples.TwoMasses") # MSL 4.1.0: its experiment list, registry (models/models_4.1.0.toml) and reference data -# (reference/4.1.0/); OMFrontend bundles 3.2.3 and 4.0.0, so 4.1.0 is the installed library -# (~/.openmodelica/libraries: Modelica, ModelicaServices and Complex 4.1.0) +# (reference/4.1.0/); OMFrontend bundles 3.2.3 and 4.1.0 as single files (the resources a model +# reads come from the installed library: ~/.openmodelica/libraries, Modelica 4.1.0) results = OMLibraryTesting.run_coverage(msl_version = "MSL:4.1.0") # Generate an HTML report @@ -189,8 +189,8 @@ Selected keywords (`run_testsuite`): Notes and limitations: -* `:omjl` mode only runs cases that load a **bundled** MSL version (3.2.x or 4.0.0) - and target a `Modelica.*` class. Cases using MSL 4.1.0/trunk, third-party libraries, +* `:omjl` mode only runs cases that load a **bundled** MSL version (3.2.x, or 4.x as 4.1.0) + and target a `Modelica.*` class. Cases using MSL trunk, third-party libraries, or inline-defined models are reported as skipped with a reason. It stops at `SIMULATE` by default because the testsuite ships no validation CSVs in this harness. * `:rtest` mode needs `omc-diff` in the scaffolded OPENMODELICAHOME. `fetch_testsuite` diff --git a/models/models_4.1.0.toml b/models/models_4.1.0.toml index b16e835..d462088 100644 --- a/models/models_4.1.0.toml +++ b/models/models_4.1.0.toml @@ -375,7 +375,7 @@ atol = 1.0 domain = "Electrical.Analog" name = "Modelica.Electrical.Analog.Examples.CharacteristicIdealDiodes" reltol = 0.05 -solver = "FBDF" +issue = "2026-10-22: the FBDF override (2026-05-22, Rodas5 Unstable at the diodes' switching) removed: FBDF stops with MaxIters, the default solver validates 3/3." [models.ChuaCircuit] atol = 5.0 @@ -778,3 +778,9 @@ name = "Modelica.Mechanics.MultiBody.Examples.Rotational3DEffects.BevelGear1D" domain = "Mechanics.MultiBody" stopTime = 0.4 issue = "2026-10-13: the MSL 4.1.0 reference ends at 0.4 s (experiment StopTime 1): compared over the reference's span." + +[models.Mechanics_MultiBody_Examples_Elementary_PointGravityWithPointMasses2] +name = "Modelica.Mechanics.MultiBody.Examples.Elementary.PointGravityWithPointMasses2" +domain = "Mechanics.MultiBody" +stopTime = 2.0 +issue = "2026-10-20: the MSL 4.1.0 reference ends at 2.0 s (experiment StopTime 3): compared over the reference's span." diff --git a/src/comparison.jl b/src/comparison.jl index 41a94d8..81c4d0c 100644 --- a/src/comparison.jl +++ b/src/comparison.jl @@ -161,11 +161,15 @@ end _event_limits(ref_time, ref_values, t) -> Union{Nothing, Tuple{Float64, Float64}} The values before and after an event the reference recorded at `t` (a time -that appears more than once: a result file stores both), else `nothing`. +that appears more than once: a result file stores both), else `nothing`. The +reference files store their times as Float32: the event's time is `t` to +Float32's rounding (MSL HBridge_RL's switching at t = 0.099 is stored as +0.098999995, at 0.0996 as 0.099600002). """ function _event_limits(ref_time::Vector{Float64}, ref_values::Vector{Float64}, t::Float64) - first_row = searchsortedfirst(ref_time, t) - last_row = searchsortedlast(ref_time, t) + local rounding = Float64(eps(Float32(max(abs(t), floatmin(Float32))))) + first_row = searchsortedfirst(ref_time, t - rounding) + last_row = searchsortedlast(ref_time, t + rounding) last_row > first_row || return nothing return (ref_values[first_row], ref_values[last_row]) end diff --git a/src/runner.jl b/src/runner.jl index bb97127..98943e1 100644 --- a/src/runner.jl +++ b/src/runner.jl @@ -717,8 +717,61 @@ function run_model(spec::ModelSpec, mgr::WorkerManager; return ModelResult(spec, PhaseResult[], BROKEN, Dates.format(Dates.now(), "yyyy-mm-dd HH:MM")) end - return run_on_worker(mgr, spec, effective_phases; timeout = timeout, - check_sim_code = check_sim_code) + local result = run_on_worker(mgr, spec, effective_phases; timeout = timeout, + check_sim_code = check_sim_code) + recycleLargeWorker!(mgr) + return result +end + +""" + recycleLargeWorker!(mgr) + +Restarts the worker of `mgr` before its next model when its memory (`workerMemoryGB`) is over +`OMJL_COVERAGE_RECYCLE_GB` gigabytes (unset or 0: never). A worker keeps what the models before it +left (compiled code, caches, freed memory the allocator holds), and a footprint watchdog killed it +under whichever model came next (coverage 21: MSL QuasiStatic EddyCurrentLosses, under 3 GB in a +fresh process). +""" +function recycleLargeWorker!(mgr::WorkerManager)::Nothing + local limit = something(tryparse(Float64, get(ENV, "OMJL_COVERAGE_RECYCLE_GB", "0")), 0.0) + (limit > 0 && mgr.os_pid !== nothing && mgr.pid !== nothing && mgr.pid in workers()) || return nothing + local gb = workerMemoryGB(mgr.os_pid) + if gb > limit + @info "Recycling worker $(mgr.pid): memory $(round(gb; digits = 1)) GB, over $(limit) GB" + kill_worker!(mgr) + end + return nothing +end + +""" + workerMemoryGB(os_pid) -> Float64 + +A process's memory in gigabytes as a footprint watchdog measures it: on macOS `top`'s MEM (the +physical footprint), elsewhere `ps`'s resident size. On macOS the two differ in both directions +(compressed pages count in the footprint, freed pages the allocator marked reusable do not; one +process: 11.5 GB resident, 7.7 GB footprint), and recycling by the resident size let coverage 26's +workers pass the 8 GB footprint watchdog below 6 GB resident (FullRobot, +ThreePhaseTransformerWithRectifier killed; under 6 GB each alone). Freed memory from many medium +arrays stays in the footprint (a worker at 0.6 GB live heap: 7.5 GB). 0 where neither reads. +""" +function workerMemoryGB(os_pid::Integer)::Float64 + if Sys.isapple() + local top = try + read(`top -l 1 -pid $(os_pid) -stats mem`, String) + catch e + e isa InterruptException && rethrow() + "" + end + local m = match(r"([0-9.]+)([KMG])[+-]?\s*$", strip(top)) + m === nothing || return parse(Float64, m[1]) * (m[2] == "G" ? 1.0 : m[2] == "M" ? 1 / 1024 : 1 / 2^20) + end + local kb = try + something(tryparse(Int, strip(read(`ps -o rss= -p $(os_pid)`, String))), 0) + catch e + e isa InterruptException && rethrow() + 0 + end + return kb / 2^20 end """ diff --git a/src/testsuite.jl b/src/testsuite.jl index e5e1a2b..30aa2dd 100644 --- a/src/testsuite.jl +++ b/src/testsuite.jl @@ -334,11 +334,12 @@ end # --------------------------------------------------------------------------- # Map an upstream loadModel version to the bundled MSL_Version the OM.jl -# by-name pipeline understands. Only 3.2.x and 4.0.0 are bundled. +# by-name pipeline understands. 3.2.3 and 4.1.0 are bundled; 4.0 is 4.1.0 +# (4.1.0 is backwards compatible with 4.0.0). function _bundled_msl_version(version::String)::Union{Nothing, String} isempty(version) && return nothing startswith(version, "3.2") && return "MSL:3.2.3" - startswith(version, "4.0") && return "MSL:4.0.0" + (startswith(version, "4.0") || startswith(version, "4.1")) && return "MSL:4.1.0" return nothing end diff --git a/test/comparisonTests.jl b/test/comparisonTests.jl index 3cd3de8..132c36e 100644 --- a/test/comparisonTests.jl +++ b/test/comparisonTests.jl @@ -55,6 +55,14 @@ end #= Neither limit: a reset that did not happen. =# local noReset = OLT.compare_signal(SignalOf(t -> t <= 0.5 ? t : t + 0.1), ref, "x", 1.0; npoints = 5) @test !noReset.passed + #= 2026-10-22: the reference files store their times as Float32, an event at 0.099 as 0.098999995 (MSL + HBridge_RL's switching, 4.1.0): the event at the sample instant 0.099 all the same. =# + local t32 = Float64(Float32(0.099)) + local stored = OLT.ReferenceData([0.0, 0.05, t32, t32, 0.1], Dict("x" => [0.0, 0.05, 0.099, -0.099, -0.1])) + @test t32 < 0.099 + @test OLT._event_limits(stored.time, stored.signals["x"], 0.099) == (0.099, -0.099) + @test OLT._event_limits(stored.time, stored.signals["x"], 0.05) === nothing + @test OLT.compare_signal(SignalOf(t -> t <= 0.099 ? t : -t), stored, "x", 0.1; npoints = 1001).passed end @testset "comparison: a stopTime past the reference's end" begin diff --git a/test/registryTests.jl b/test/registryTests.jl index 913369a..622f356 100644 --- a/test/registryTests.jl +++ b/test/registryTests.jl @@ -45,5 +45,5 @@ end @test all(e -> e["name"] in experiments, values(OLT.TOML.parsefile(OLT.default_models_path("4.1.0"))["models"])) #= Bundled versions by MSL_Version; others are installed libraries (not loaded here). =# @test OLT._msl_kwargs("MSL:3.2.3") == (; MSL_Version = "MSL:3.2.3") - @test OLT._msl_kwargs("MSL:4.0.0") == (; MSL_Version = "MSL:4.0.0") + @test OLT._msl_kwargs("MSL:4.1.0") == (; MSL_Version = "MSL:4.1.0") end diff --git a/test/runnerTests.jl b/test/runnerTests.jl index 3c58cec..6c42486 100644 --- a/test/runnerTests.jl +++ b/test/runnerTests.jl @@ -91,3 +91,18 @@ end @test [r.spec.name for r in merged] == [s.name for s in specs] # run_coverage's order (domain, name) @test_throws ErrorException OLT.merge_shards([result.(s1), result.(s1[1:1])]) end + +#= A worker is recycled by the measure the footprint watchdog kills by (top's MEM on macOS), not ps's + resident size: they differ in both directions, and coverage 26's workers passed the 8 GB watchdog below + 6 GB resident. =# +@testset "workerMemoryGB: the footprint watchdog's measure" begin + local gb = OLT.workerMemoryGB(getpid()) + @test 0.05 < gb < 1024 + if Sys.isapple() + local top = strip(last(split(strip(read(`top -l 1 -pid $(getpid()) -stats mem`, String)), '\n'))) + local m = match(r"([0-9.]+)([KMG])", top) + local topGB = parse(Float64, m[1]) * (m[2] == "G" ? 1.0 : m[2] == "M" ? 1 / 1024 : 1 / 2^20) + @test isapprox(gb, topGB; rtol = 0.2) + end + @test redirect_stderr(devnull) do; OLT.workerMemoryGB(999_999); end == 0.0 +end