From 5ccc056edd55e03f310c9af3fa95063530c38ae7 Mon Sep 17 00:00:00 2001 From: Nick Robinson Date: Mon, 15 Jan 2024 01:25:54 +0000 Subject: [PATCH 1/5] Refactor testsetup handling --- src/ReTestItems.jl | 83 +++++++++++++++++++++------------------------- src/log_capture.jl | 4 +-- src/macros.jl | 4 +-- 3 files changed, 41 insertions(+), 50 deletions(-) diff --git a/src/ReTestItems.jl b/src/ReTestItems.jl index a6ff0beb..460559de 100644 --- a/src/ReTestItems.jl +++ b/src/ReTestItems.jl @@ -673,14 +673,14 @@ function include_testfiles!(project_name, projectfile, paths, ti_filter::TestIte # we set it below in tls as __RE_TEST_SETUPS__ for each included file setup_channel = Channel{Pair{Symbol, TestSetup}}(Inf) setup_task = @spawn begin - setups = Dict{Symbol, TestSetup}() - for (name, setup) in setup_channel - if haskey(setups, name) + testsetups = Dict{Symbol, TestSetup}() + for (name, ts) in setup_channel + if haskey(testsetups, name) @warn "Encountered duplicate @testsetup with name: `$name`. Replacing..." end - setups[name] = setup + testsetups[name] = ts end - return setups + return testsetups end hidden_re = r"\.\w" @sync for (root, d, files) in Base.walkdir(project_root) @@ -732,21 +732,21 @@ function include_testfiles!(project_name, projectfile, paths, ti_filter::TestIte # prune empty directories/files close(setup_channel) prune!(root_node) - ti = TestItems(root_node) - flatten_testitems!(ti) - check_ids(ti.testitems) - setups = fetch(setup_task) - for (i, x) in enumerate(ti.testitems) + tis = TestItems(root_node) + flatten_testitems!(tis) + check_ids(tis.testitems) + testsetups = fetch(setup_task) + for (i, ti) in enumerate(tis.testitems) # set a unique number for each testitem - x.number[] = i + ti.number[] = i # populate testsetups for each testitem - for s in x.setups - if haskey(setups, s) - push!(x.testsetups, setups[s]) + for setup_name in ti.setups + if haskey(testsetups, setup_name) + push!(ti.testsetups, setup_name => testsetups[setup_name]) end end end - return ti, setups # only returned for testing + return tis, testsetups # only returned for testing end function check_ids(testitems) @@ -829,28 +829,18 @@ function with_source_path(f, path) end end -function ensure_setup!(ctx::TestContext, setup::Symbol, setups::Vector{TestSetup}, logs::Symbol) +function ensure_setup!(ctx::TestContext, ts::TestSetup, logs::Symbol) mods = ctx.setups_evaled @lock mods.lock begin - mod = get(mods.modules, setup, nothing) + mod = get(mods.modules, ts.name, nothing) if mod !== nothing # we've eval-ed this module before, so just return the module name return nameof(mod) end - # we haven't eval-ed this module before, so we need to eval it - i = findfirst(s -> s.name == setup, setups) - if i === nothing - # if the setup hasn't been eval-ed before and we don't have it - # in our testsetups, then it was never found during including - # in that case, we return the expected test setup module name - # which will turn into a `using $setup` in the test item - # which will throw an appropriate error - return setup - end - ts = setups[i] - # In case the setup fails to eval, we discard its logs -- the setup will be - # attempted to eval for each of the dependent test items and we'd for each - # failed test item, we'd print the cumulative logs from all the previous attempts. + # We haven't eval-ed this module before, so we need to eval it. + # In case the setup fails to eval, we discard its logs -- we will attempt to eval + # this testsetup for each of the dependent test items, and then for each failed + # test item, we'd print the cumulative logs from all the previous attempts. isassigned(ts.logstore) && close(ts.logstore[]) ts.logstore[] = open(logpath(ts), "w") mod_expr = :(module $(gensym(ts.name)) end) @@ -860,7 +850,7 @@ function ensure_setup!(ctx::TestContext, setup::Symbol, setups::Vector{TestSetup with_source_path(() -> Core.eval(Main, mod_expr), ts.file) end # add the new module to our TestSetupModules - mods.modules[setup] = newmod + mods.modules[ts.name] = newmod return nameof(newmod) end end @@ -908,9 +898,9 @@ function runtestitem(ti::TestItem; kw...) # make a fresh TestSetupModules for each testitem run GLOBAL_TEST_CONTEXT_FOR_TESTING.setups_evaled = TestSetupModules() empty!(ti.testsetups) - for setup in ti.setups - ts = get(GLOBAL_TEST_SETUPS_FOR_TESTING, setup, nothing) - ts !== nothing && push!(ti.testsetups, ts) + for setup_name in ti.setups + ts = get(GLOBAL_TEST_SETUPS_FOR_TESTING, setup_name, nothing) + ts !== nothing && push!(ti.testsetups, setup_name => ts) end runtestitem(ti, GLOBAL_TEST_CONTEXT_FOR_TESTING; kw...) end @@ -954,23 +944,24 @@ function runtestitem( prev = get(task_local_storage(), :__TESTITEM_ACTIVE__, false) task_local_storage()[:__TESTITEM_ACTIVE__] = true try - for setup in ti.setups - # TODO(nhd): Consider implementing some affinity to setups, so that we can - # prefer to send testitems to the workers that have already eval'd setups. - # Or maybe allow user-configurable grouping of test items by worker? - # Or group them by file by default? - + for (setup_name, ts) in ti.testsetups # ensure setup has been evaled before - @debugv 1 "Ensuring setup for test item $(repr(name)) $(setup)$(_on_worker())." - ts_mod = ensure_setup!(ctx, setup, ti.testsetups, logs) + @debugv 1 "Ensuring setup for test item $(repr(name)) $(setup_name)$(_on_worker())." + ts_mod = ensure_setup!(ctx, ts, logs) # eval using in our @testitem module - @debugv 1 "Importing setup for test item $(repr(name)) $(setup)$(_on_worker())." + @debugv 1 "Importing setup for test item $(repr(name)) $(setup_name)$(_on_worker())." # We look up the testsetups from Main (since tests are eval'd in their own # temporary anonymous module environment.) push!(body.args, Expr(:using, Expr(:., :Main, ts_mod))) # ts_mod is a gensym'd name so that setup modules don't clash # so we set a const alias inside our @testitem module to make things work - push!(body.args, :(const $setup = $ts_mod)) + push!(body.args, :(const $setup_name = $ts_mod)) + end + for setup_name in setdiff(ti.setups, keys(ti.testsetups)) + # if the setup was requested but is not in our testsetups, then it was never + # found when including files. We still add `using $setup` in the test item + # so that we throw an appropriate error when running the test item. + push!(body.args, Expr(:using, Expr(:., :Main, setup_name))) end @debugv 1 "Setup for test item $(repr(name)) done$(_on_worker())." @@ -1013,7 +1004,7 @@ function runtestitem( LineNumberNode(ti.line, ti.file))) finally # Make sure all test setup logs are commited to file - foreach(ts->isassigned(ts.logstore) && flush(ts.logstore[]), ti.testsetups) + foreach(ts->isassigned(ts.logstore) && flush(ts.logstore[]), values(ti.testsetups)) ts1 = Test.pop_testset() task_local_storage()[:__TESTITEM_ACTIVE__] = prev @assert ts1 === ts diff --git a/src/log_capture.jl b/src/log_capture.jl index 6d36ac9d..f48bb27f 100644 --- a/src/log_capture.jl +++ b/src/log_capture.jl @@ -172,7 +172,7 @@ function print_errors_and_captured_logs( ) ts = ti.testsets[run_number] has_errors = any_non_pass(ts) - has_logs = _has_logs(ti, run_number) || any(_has_logs, ti.testsetups) + has_logs = _has_logs(ti, run_number) || any(_has_logs, values(ti.testsetups)) if has_errors || logs == :batched report_iob = IOContext(IOBuffer(), :color=>Base.get_have_color()) println(report_iob) @@ -214,7 +214,7 @@ end # the captured logs or a messgage that no logs were captured. `print_errors_and_captured_logs` # will call this function only if some logs were collected or when called with `verbose_results`. function _print_captured_logs(io, ti::TestItem, run_number::Int) - for setup in ti.testsetups + for (_name, setup) in ti.testsetups _print_captured_logs(io, setup, ti) end has_logs = _has_logs(ti, run_number) diff --git a/src/macros.jl b/src/macros.jl index a231de84..60f585a5 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -125,7 +125,7 @@ struct TestItem line::Int project_root::String code::Any - testsetups::Vector{TestSetup} # populated by runtests coordinator + testsetups::Dict{Symbol,TestSetup} # populated by runtests coordinator workerid::Base.RefValue{Int} # populated when the test item is scheduled testsets::Vector{DefaultTestSet} # populated when the test item is finished running eval_number::Base.RefValue{Int} # to keep track of how many items have been run so far @@ -136,7 +136,7 @@ function TestItem(number, name, id, tags, default_imports, setups, retries, time _id = @something(id, repr(hash(name, hash(relpath(file, project_root))))) return TestItem( number, name, _id, tags, default_imports, setups, retries, timeout, skip, file, line, project_root, code, - TestSetup[], + Dict{Symbol,TestSetup}(), Ref{Int}(0), DefaultTestSet[], Ref{Int}(0), From 8ecfb5f972a6f3a4316593c40978a75cac798ca3 Mon Sep 17 00:00:00 2001 From: Nick Robinson Date: Tue, 16 Jan 2024 10:24:46 +0000 Subject: [PATCH 2/5] fixup! Refactor testsetup handling --- test/log_capture.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/log_capture.jl b/test/log_capture.jl index 78d80fb2..41465c42 100644 --- a/test/log_capture.jl +++ b/test/log_capture.jl @@ -34,8 +34,8 @@ end setup1 = @testsetup module TheTestSetup1 end setup2 = @testsetup module TheTestSetup2 end ti = TestItem(Ref(42), "TheTestItem", "ID007", [], false, [], 0, nothing, false, "source/path", 42, ".", nothing) - push!(ti.testsetups, setup1) - push!(ti.testsetups, setup2) + push!(ti.testsetups, :TheTestSetup1 => setup1) + push!(ti.testsetups, :TheTestSetup2 => setup2) push!(ti.testsets, Test.DefaultTestSet("dummy")) setup1.logstore[] = open(ReTestItems.logpath(setup1), "w") setup2.logstore[] = open(ReTestItems.logpath(setup2), "w") From 71120da19572bd5b1673825f4657be665d61171a Mon Sep 17 00:00:00 2001 From: Nick Robinson Date: Sat, 20 Jan 2024 00:55:23 +0000 Subject: [PATCH 3/5] Move logic for running testsetups to own function --- src/ReTestItems.jl | 55 ++++++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/src/ReTestItems.jl b/src/ReTestItems.jl index 460559de..cda6df2e 100644 --- a/src/ReTestItems.jl +++ b/src/ReTestItems.jl @@ -829,13 +829,34 @@ function with_source_path(f, path) end end -function ensure_setup!(ctx::TestContext, ts::TestSetup, logs::Symbol) +# Call `runtestsetup(ts, ...)` for each `ts::Testsetup` required by the given `TestItem` +# Return setup_name => module_name pairs +function runtestsetups(ti::TestItem, ctx::TestContext; logs::Symbol, force::Bool=false) + # Initialse with the list of _requested_ setups, so that if it no setup by that name was + # found when including files we return the setup name as the module name. Attempting to + # import that name, like `using $setup`, will then throw an appropriate error. + setup_to_mod = Dict{Symbol,Symbol}(ti.setups .=> ti.setups) + for (ts_name, ts) in ti.testsetups + @debugv 1 "Ensuring setup for test item $(repr(ti.name)) $(ts_name)$(_on_worker())." + setup_to_mod[ts_name] = runtestsetup(ts, ctx; logs) + end + return setup_to_mod +end + +# Run the given `TestSetup`, add the resulting `Module` to the `TestContext` and returns the +# name of the `Module` (i.e. returns a `Symbol`). +# If the `TestSetup` has already been evaluated on this process and so is already in the +# `TestContext`, simply returns the `Module` name. +# Pass `force=true` to force the `TestSetup` to be re-evaluated, even if run before. +function runtestsetup(ts::TestSetup, ctx::TestContext; logs::Symbol, force::Bool=false) mods = ctx.setups_evaled @lock mods.lock begin - mod = get(mods.modules, ts.name, nothing) - if mod !== nothing - # we've eval-ed this module before, so just return the module name - return nameof(mod) + if !force + mod = get(mods.modules, ts.name, nothing) + if mod !== nothing + # we've eval-ed this module before, so just return the module name + return nameof(mod) + end end # We haven't eval-ed this module before, so we need to eval it. # In case the setup fails to eval, we discard its logs -- we will attempt to eval @@ -944,24 +965,16 @@ function runtestitem( prev = get(task_local_storage(), :__TESTITEM_ACTIVE__, false) task_local_storage()[:__TESTITEM_ACTIVE__] = true try - for (setup_name, ts) in ti.testsetups - # ensure setup has been evaled before - @debugv 1 "Ensuring setup for test item $(repr(name)) $(setup_name)$(_on_worker())." - ts_mod = ensure_setup!(ctx, ts, logs) - # eval using in our @testitem module - @debugv 1 "Importing setup for test item $(repr(name)) $(setup_name)$(_on_worker())." + # eval `using $TestSetup` in our @testitem module. + testsetups = runtestsetups(ti, ctx; logs) + for (ts_name, ts_module_name) in testsetups + @debugv 1 "Add setup imports for test item $(repr(name)) $(setup_name)$(_on_worker())." # We look up the testsetups from Main (since tests are eval'd in their own # temporary anonymous module environment.) - push!(body.args, Expr(:using, Expr(:., :Main, ts_mod))) - # ts_mod is a gensym'd name so that setup modules don't clash - # so we set a const alias inside our @testitem module to make things work - push!(body.args, :(const $setup_name = $ts_mod)) - end - for setup_name in setdiff(ti.setups, keys(ti.testsetups)) - # if the setup was requested but is not in our testsetups, then it was never - # found when including files. We still add `using $setup` in the test item - # so that we throw an appropriate error when running the test item. - push!(body.args, Expr(:using, Expr(:., :Main, setup_name))) + push!(body.args, Expr(:using, Expr(:., :Main, ts_module_name))) + # ts_module_name is a gensym'd name so that setup modules don't clash, + # so set a const alias inside our @testitem module to make things work. + push!(body.args, :(const $ts_name = $ts_module_name)) end @debugv 1 "Setup for test item $(repr(name)) done$(_on_worker())." From 5f91402c189dc1f8efdc1df9cfe8843d87f61749 Mon Sep 17 00:00:00 2001 From: Nick Robinson Date: Sat, 20 Jan 2024 00:59:25 +0000 Subject: [PATCH 4/5] Drop unused `force` keyword from `runtestsetup` --- src/ReTestItems.jl | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/ReTestItems.jl b/src/ReTestItems.jl index cda6df2e..f09ffe24 100644 --- a/src/ReTestItems.jl +++ b/src/ReTestItems.jl @@ -830,8 +830,8 @@ function with_source_path(f, path) end # Call `runtestsetup(ts, ...)` for each `ts::Testsetup` required by the given `TestItem` -# Return setup_name => module_name pairs -function runtestsetups(ti::TestItem, ctx::TestContext; logs::Symbol, force::Bool=false) +# Return `Dict` mapping `setup_name::Symbol => module_name::Symbol` +function runtestsetups(ti::TestItem, ctx::TestContext; logs::Symbol) # Initialse with the list of _requested_ setups, so that if it no setup by that name was # found when including files we return the setup name as the module name. Attempting to # import that name, like `using $setup`, will then throw an appropriate error. @@ -847,16 +847,13 @@ end # name of the `Module` (i.e. returns a `Symbol`). # If the `TestSetup` has already been evaluated on this process and so is already in the # `TestContext`, simply returns the `Module` name. -# Pass `force=true` to force the `TestSetup` to be re-evaluated, even if run before. -function runtestsetup(ts::TestSetup, ctx::TestContext; logs::Symbol, force::Bool=false) +function runtestsetup(ts::TestSetup, ctx::TestContext; logs::Symbol) mods = ctx.setups_evaled @lock mods.lock begin - if !force - mod = get(mods.modules, ts.name, nothing) - if mod !== nothing - # we've eval-ed this module before, so just return the module name - return nameof(mod) - end + mod = get(mods.modules, ts.name, nothing) + if mod !== nothing + # we've eval-ed this module before, so just return the module name + return nameof(mod) end # We haven't eval-ed this module before, so we need to eval it. # In case the setup fails to eval, we discard its logs -- we will attempt to eval From c5e918c1ea658d12b8699b38bad4592a69f3d4dd Mon Sep 17 00:00:00 2001 From: Nick Robinson Date: Sat, 20 Jan 2024 14:40:06 +0000 Subject: [PATCH 5/5] WIP log testsetup start/done and timings --- src/ReTestItems.jl | 5 ++- src/log_capture.jl | 33 +++++++++++++++--- src/macros.jl | 83 +++++++++++++++++++++++++++------------------- 3 files changed, 81 insertions(+), 40 deletions(-) diff --git a/src/ReTestItems.jl b/src/ReTestItems.jl index f09ffe24..b805d0b5 100644 --- a/src/ReTestItems.jl +++ b/src/ReTestItems.jl @@ -864,9 +864,12 @@ function runtestsetup(ts::TestSetup, ctx::TestContext; logs::Symbol) mod_expr = :(module $(gensym(ts.name)) end) # replace the module expr body with our @testsetup code mod_expr.args[3] = ts.code - newmod = _redirect_logs(logs == :eager ? DEFAULT_STDOUT[] : ts.logstore[]) do + log_testsetup_start(ts) + newmod, stats = @timed_with_compilation _redirect_logs(logs == :eager ? DEFAULT_STDOUT[] : ts.logstore[]) do with_source_path(() -> Core.eval(Main, mod_expr), ts.file) end + ts.stats[] = stats + log_testsetup_done(ts) # add the new module to our TestSetupModules mods.modules[ts.name] = newmod return nameof(newmod) diff --git a/src/log_capture.jl b/src/log_capture.jl index f48bb27f..23b420e8 100644 --- a/src/log_capture.jl +++ b/src/log_capture.jl @@ -55,6 +55,11 @@ function _print_scaled_one_dec(io, value, scale, label="") end print(io, label) end +function print_time(io::IO, s::PerfStats) + return print_time( + io; s.elapsedtime, s.bytes, s.gctime, s.allocs, s.compile_time, s.recompile_time + ) +end function print_time(io; elapsedtime, bytes=0, gctime=0, allocs=0, compile_time=0, recompile_time=0) _print_scaled_one_dec(io, elapsedtime, 1e9, " secs") if gctime > 0 || compile_time > 0 @@ -243,7 +248,7 @@ function _print_test_errors(report_iob, ts::DefaultTestSet, worker_info) return nothing end -function print_state(io, state, ti, ntestitems; color=:default) +function print_state(io::IO, state::String, ti::TestItem, ntestitems; color=:default) interactive = parse(Bool, get(ENV, "RETESTITEMS_INTERACTIVE", string(Base.isinteractive()))) print(io, format(now(), "HH:MM:SS | ")) !interactive && print(io, _mem_watermark()) @@ -256,6 +261,13 @@ function print_state(io, state, ti, ntestitems; color=:default) end print(io, " test item $(repr(ti.name)) ") end +function print_state(io::IO, state::String, ts::TestSetup) + interactive = parse(Bool, get(ENV, "RETESTITEMS_INTERACTIVE", string(Base.isinteractive()))) + print(io, format(now(), "HH:MM:SS | ")) + !interactive && print(io, _mem_watermark()) + printstyled(io, rpad(uppercase(state), 5)) + print(io, " test setup $(ts.name) ") +end function print_file_info(io, ti) print(io, "at ") @@ -270,7 +282,6 @@ function log_testitem_skipped(ti::TestItem, ntestitems=0) write(DEFAULT_STDOUT[], take!(io.io)) end -# Marks the start of each test item function log_testitem_start(ti::TestItem, ntestitems=0) io = IOContext(IOBuffer(), :color => get(DEFAULT_STDOUT[], :color, false)::Bool) print_state(io, "START", ti, ntestitems) @@ -278,12 +289,26 @@ function log_testitem_start(ti::TestItem, ntestitems=0) println(io) write(DEFAULT_STDOUT[], take!(io.io)) end +function log_testsetup_start(ts::TestSetup) + io = IOContext(IOBuffer(), :color => get(DEFAULT_STDOUT[], :color, false)::Bool) + print_state(io, "START", ts) + print_file_info(io, ts) + println(io) + write(DEFAULT_STDOUT[], take!(io.io)) +end function log_testitem_done(ti::TestItem, ntestitems=0) io = IOContext(IOBuffer(), :color => get(DEFAULT_STDOUT[], :color, false)::Bool) print_state(io, "DONE", ti, ntestitems) - x = last(ti.stats) # always print stats for most recent run - print_time(io; x.elapsedtime, x.bytes, x.gctime, x.allocs, x.compile_time, x.recompile_time) + stats = last(ti.stats) # always print stats for most recent run + print_time(io, stats) + println(io) + write(DEFAULT_STDOUT[], take!(io.io)) +end +function log_testsetup_done(ts::TestSetup, ntestitems=0) + io = IOContext(IOBuffer(), :color => get(DEFAULT_STDOUT[], :color, false)::Bool) + print_state(io, "DONE", ts) + print_time(io, ts.stats[]) println(io) write(DEFAULT_STDOUT[], take!(io.io)) end diff --git a/src/macros.jl b/src/macros.jl index 60f585a5..bdbbc038 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -1,11 +1,46 @@ gettls(k, d) = get(task_local_storage(), k, d) ### -### testsetup +### PerfStats +### + +Base.@kwdef struct PerfStats + elapsedtime::UInt=0 + bytes::Int=0 + gctime::Int=0 + allocs::Int=0 + compile_time::UInt=0 + recompile_time::UInt=0 +end + +# Adapted from Base.@time +macro timed_with_compilation(ex) + quote + Base.Experimental.@force_compile + local stats = Base.gc_num() + local elapsedtime = Base.time_ns() + Base.cumulative_compile_timing(true) + local compile_elapsedtimes = Base.cumulative_compile_time_ns() + local val = Base.@__tryfinally($(esc(ex)), + (elapsedtime = Base.time_ns() - elapsedtime; + Base.cumulative_compile_timing(false); + compile_elapsedtimes = Base.cumulative_compile_time_ns() .- compile_elapsedtimes) + ) + local diff = Base.GC_Diff(Base.gc_num(), stats) + local out = PerfStats(; + elapsedtime, bytes=diff.allocd, gctime=diff.total_time, allocs=Base.gc_alloc_count(diff), + compile_time=first(compile_elapsedtimes), recompile_time=last(compile_elapsedtimes) + ) + val, out + end +end + +### +### TestSetup ### """ - TestSetup(name, code) + TestSetup A module that a `TestItem` can require to be run before that `TestItem` is run. Used for declaring code that multiple `TestItem`s rely on. @@ -21,6 +56,10 @@ struct TestSetup # the test setup. This IO object is only for writing on the worker. The coordinator needs # to open the file when it needs to read from it. logstore::Base.RefValue{IOStream} # Populated and only safe to use on the worker + stats::Base.RefValue{PerfStats} # populated when the test setup is finished running +end +function TestSetup(name::Symbol, code, file::String, line::Int, project_root::String) + return TestSetup(name, code, file, line, project_root, Ref{IOStream}(), Ref{PerfStats}()) end """ @@ -53,46 +92,20 @@ macro testsetup(mod) name isa Symbol || error("`@testsetup module` expects a valid module name") nm = QuoteNode(name) q = QuoteNode(code) + _source = QuoteNode(__source__) esc(quote - $store_test_setup($TestSetup($nm, $q, $(String(__source__.file)), $(__source__.line), $gettls(:__RE_TEST_PROJECT__, "."), Ref{IOStream}())) + $store_test_setup( + $TestSetup( + $nm, $q, $String($_source.file), $_source.line, $gettls(:__RE_TEST_PROJECT__, "."), + ) + ) end) end ### -### testitem +### TestItem ### -Base.@kwdef struct PerfStats - elapsedtime::UInt=0 - bytes::Int=0 - gctime::Int=0 - allocs::Int=0 - compile_time::UInt=0 - recompile_time::UInt=0 -end - -# Adapted from Base.@time -macro timed_with_compilation(ex) - quote - Base.Experimental.@force_compile - local stats = Base.gc_num() - local elapsedtime = Base.time_ns() - Base.cumulative_compile_timing(true) - local compile_elapsedtimes = Base.cumulative_compile_time_ns() - local val = Base.@__tryfinally($(esc(ex)), - (elapsedtime = Base.time_ns() - elapsedtime; - Base.cumulative_compile_timing(false); - compile_elapsedtimes = Base.cumulative_compile_time_ns() .- compile_elapsedtimes) - ) - local diff = Base.GC_Diff(Base.gc_num(), stats) - local out = PerfStats(; - elapsedtime, bytes=diff.allocd, gctime=diff.total_time, allocs=Base.gc_alloc_count(diff), - compile_time=first(compile_elapsedtimes), recompile_time=last(compile_elapsedtimes) - ) - val, out - end -end - mutable struct ScheduledForEvaluation @atomic value::Bool end