diff --git a/python_bindings/halide/src/halide_/PyEnums.cpp b/python_bindings/halide/src/halide_/PyEnums.cpp index d6327abab3ba..0e702b37d913 100644 --- a/python_bindings/halide/src/halide_/PyEnums.cpp +++ b/python_bindings/halide/src/halide_/PyEnums.cpp @@ -18,6 +18,16 @@ void define_enums(py::module &m) { .value("Input", Internal::ArgInfoDirection::Input) .value("Output", Internal::ArgInfoDirection::Output); + py::enum_(m, "ProfilerFuncKind") + .value("Func", halide_profiler_func_kind_func) + .value("Overhead", halide_profiler_func_kind_overhead) + .value("ThreadIdle", halide_profiler_func_kind_thread_idle) + .value("Malloc", halide_profiler_func_kind_malloc) + .value("Free", halide_profiler_func_kind_free) + .value("CopyToHost", halide_profiler_func_kind_copy_to_host) + .value("CopyToDevice", halide_profiler_func_kind_copy_to_device) + .value("Allocation", halide_profiler_func_kind_allocation); + py::enum_(m, "DeviceAPI") .value("None", DeviceAPI::None) .value("Host", DeviceAPI::Host) diff --git a/python_bindings/halide/src/halide_/PyPipeline.cpp b/python_bindings/halide/src/halide_/PyPipeline.cpp index ec551a501e56..9c29cbc75e92 100644 --- a/python_bindings/halide/src/halide_/PyPipeline.cpp +++ b/python_bindings/halide/src/halide_/PyPipeline.cpp @@ -20,6 +20,65 @@ py::object realization_to_object(const Realization &r) { return to_python_tuple(r); } +// Python-owned snapshots of the profiler's stats, so that they stay valid +// after the ProfilerScope exits and the profiler resets. +struct ProfilerFuncStats { + std::string name; + halide_profiler_func_stats stats; +}; + +struct ProfilerPipelineStats { + std::string name; + halide_profiler_pipeline_stats stats; + std::vector funcs; +}; + +ProfilerFuncStats snapshot(const halide_profiler_func_stats &f) { + return {f.name, f}; +} + +std::optional snapshot(const halide_profiler_pipeline_stats *p) { + if (!p) { + return std::nullopt; + } + ProfilerPipelineStats result{p->name, *p, {}}; + for (int i = 0; i < p->num_funcs; i++) { + result.funcs.push_back(snapshot(p->funcs[i])); + } + return result; +} + +std::optional snapshot(const halide_profiler_func_stats *f) { + if (!f) { + return std::nullopt; + } + return snapshot(*f); +} + +// Owns a ProfilerScope that a with-statement can end early, via exit(), +// rather than waiting on garbage collection. +struct PyProfilerScope { + std::unique_ptr scope; + + explicit PyProfilerScope(Pipeline p) + : scope(std::make_unique(std::move(p))) { + } + explicit PyProfilerScope(Func &f) + : scope(std::make_unique(f)) { + } + + const ProfilerScope &get() const { + if (!scope) { + throw std::runtime_error("This ProfilerScope has already exited"); + } + return *scope; + } + + void exit() { + scope.reset(); + } +}; + } // namespace void define_pipeline(py::module &m) { @@ -305,6 +364,83 @@ void define_pipeline(py::module &m) { return create_callable_from_generator(target, name, generator_params); }, py::arg("target"), py::arg("name"), py::arg("generator_params") = std::map{}); + + auto func_stats_class = py::class_(m, "ProfilerFuncStats") + .def_readonly("name", &ProfilerFuncStats::name) + .def("__repr__", [](const ProfilerFuncStats &s) -> std::string { + return ""; + }); +#define HALIDE_PROFILER_FUNC_FIELD(field) \ + func_stats_class.def_property_readonly(#field, [](const ProfilerFuncStats &s) { return s.stats.field; }) + HALIDE_PROFILER_FUNC_FIELD(parent); + HALIDE_PROFILER_FUNC_FIELD(canonical_id); + HALIDE_PROFILER_FUNC_FIELD(kind); + HALIDE_PROFILER_FUNC_FIELD(buffer_func_id); + HALIDE_PROFILER_FUNC_FIELD(counters_approximated); + HALIDE_PROFILER_FUNC_FIELD(time); + HALIDE_PROFILER_FUNC_FIELD(memory_current); + HALIDE_PROFILER_FUNC_FIELD(memory_peak); + HALIDE_PROFILER_FUNC_FIELD(stack_peak); + HALIDE_PROFILER_FUNC_FIELD(memory_total); + HALIDE_PROFILER_FUNC_FIELD(active_threads_numerator); + HALIDE_PROFILER_FUNC_FIELD(active_threads_denominator); + HALIDE_PROFILER_FUNC_FIELD(num_allocs); + HALIDE_PROFILER_FUNC_FIELD(parallel_loops); + HALIDE_PROFILER_FUNC_FIELD(parallel_tasks); + HALIDE_PROFILER_FUNC_FIELD(points_required_at_root); + HALIDE_PROFILER_FUNC_FIELD(points_computed); + HALIDE_PROFILER_FUNC_FIELD(scalar_loads); + HALIDE_PROFILER_FUNC_FIELD(vector_loads); + HALIDE_PROFILER_FUNC_FIELD(gathers); + HALIDE_PROFILER_FUNC_FIELD(bytes_loaded); + HALIDE_PROFILER_FUNC_FIELD(scalar_stores); + HALIDE_PROFILER_FUNC_FIELD(vector_stores); + HALIDE_PROFILER_FUNC_FIELD(scatters); + HALIDE_PROFILER_FUNC_FIELD(bytes_stored); + HALIDE_PROFILER_FUNC_FIELD(realizations); + HALIDE_PROFILER_FUNC_FIELD(productions); + HALIDE_PROFILER_FUNC_FIELD(points_required_at_realization); + HALIDE_PROFILER_FUNC_FIELD(points_required_at_production); + HALIDE_PROFILER_FUNC_FIELD(points_required_inwards); + HALIDE_PROFILER_FUNC_FIELD(productions_if_inwards); +#undef HALIDE_PROFILER_FUNC_FIELD + + auto pipeline_stats_class = py::class_(m, "ProfilerPipelineStats") + .def_readonly("name", &ProfilerPipelineStats::name) + .def_readonly("funcs", &ProfilerPipelineStats::funcs) + .def("__repr__", [](const ProfilerPipelineStats &s) -> std::string { + return ""; + }); +#define HALIDE_PROFILER_PIPELINE_FIELD(field) \ + pipeline_stats_class.def_property_readonly(#field, [](const ProfilerPipelineStats &s) { return s.stats.field; }) + HALIDE_PROFILER_PIPELINE_FIELD(time); + HALIDE_PROFILER_PIPELINE_FIELD(memory_current); + HALIDE_PROFILER_PIPELINE_FIELD(memory_peak); + HALIDE_PROFILER_PIPELINE_FIELD(memory_total); + HALIDE_PROFILER_PIPELINE_FIELD(active_threads_numerator); + HALIDE_PROFILER_PIPELINE_FIELD(active_threads_denominator); + HALIDE_PROFILER_PIPELINE_FIELD(native_vector_bytes); + HALIDE_PROFILER_PIPELINE_FIELD(runs); + HALIDE_PROFILER_PIPELINE_FIELD(billed_runs); + HALIDE_PROFILER_PIPELINE_FIELD(samples); + HALIDE_PROFILER_PIPELINE_FIELD(num_allocs); +#undef HALIDE_PROFILER_PIPELINE_FIELD + + py::class_(m, "ProfilerScope") + .def(py::init(), py::arg("pipeline")) + .def(py::init(), py::arg("func")) + .def("__enter__", [](PyProfilerScope &s) -> PyProfilerScope & { return s; }) + .def("__exit__", [](PyProfilerScope &s, const py::object &exc_type, const py::object &exc_value, const py::object &exc_traceback) -> bool { + s.exit(); + return false; + }) + .def("exit", &PyProfilerScope::exit) + .def("pipeline_stats", [](const PyProfilerScope &s) { + return snapshot(s.get().pipeline_stats()); + }) + .def("func_stats", [](const PyProfilerScope &s, const Func &f) { return snapshot(s.get().func_stats(f)); }, py::arg("func")) + .def("func_stats", [](const PyProfilerScope &s, const std::string &name) { return snapshot(s.get().func_stats(name)); }, py::arg("name")) + .def("__repr__", [](const PyProfilerScope &s) -> std::string { return ""; }); } } // namespace PythonBindings diff --git a/python_bindings/halide/test/correctness/CMakeLists.txt b/python_bindings/halide/test/correctness/CMakeLists.txt index b9231af7e5c3..7c96cb29c1d7 100644 --- a/python_bindings/halide/test/correctness/CMakeLists.txt +++ b/python_bindings/halide/test/correctness/CMakeLists.txt @@ -24,6 +24,7 @@ set(tests memoize.py multi_method_module_test.py multipass_constraints.py + profiler_scope.py pystub.py rdom.py realize_warnings.py diff --git a/python_bindings/halide/test/correctness/profiler_scope.py b/python_bindings/halide/test/correctness/profiler_scope.py new file mode 100644 index 000000000000..51c38bf45fbe --- /dev/null +++ b/python_bindings/halide/test/correctness/profiler_scope.py @@ -0,0 +1,62 @@ +import halide as hl + + +def test_profiler_scope(): + target = hl.get_jit_target_from_environment() + if target.arch == hl.TargetArch.WebAssembly: + print("[SKIP] Profiler state is not accessible under WebAssembly.") + return + target = target.with_feature(hl.TargetFeature.Profile) + + x, y = hl.Var("x"), hl.Var("y") + g = hl.Func("g_profiled") + f = hl.Func("f_profiled") + g[x, y] = x + y + f[x, y] = g[x, y] * 2 + g.compute_root() + + size = 256 + with hl.ProfilerScope(f) as scope: + assert scope.pipeline_stats() is None + + for _ in range(3): + f.realize([size, size], target) + + p = scope.pipeline_stats() + assert p is not None + assert p.runs == 3 + assert p.name == f.name() + assert any(fs.name == g.name() for fs in p.funcs) + + gs = scope.func_stats(g) + assert gs is not None + assert gs.kind == hl.ProfilerFuncKind.Func + assert gs.num_allocs == 3 + assert gs.memory_peak == size * size * 4 + assert gs.memory_total == 3 * size * size * 4 + assert scope.func_stats(g.name()).num_allocs == gs.num_allocs + assert scope.func_stats("nonexistent") is None + + # Snapshots outlive the scope. + assert gs.num_allocs == 3 + + try: + scope.pipeline_stats() + raise AssertionError("Expected an error after the scope exited") + except RuntimeError: + pass + + # Explicitly constructing a Pipeline works too, and the scope's exit + # reset the stats. + pipe = hl.Pipeline(f) + with hl.ProfilerScope(pipe) as scope: + pipe.realize([size, size], target) + assert scope.pipeline_stats().runs == 1 + + +def main(): + test_profiler_scope() + + +if __name__ == "__main__": + main() diff --git a/src/Func.h b/src/Func.h index 4df562e272ca..81d9f6563102 100644 --- a/src/Func.h +++ b/src/Func.h @@ -794,6 +794,8 @@ class Func { * creating it (and freezing the Func) if necessary. */ Pipeline pipeline(); + friend class ProfilerScope; + // Helper function for recursive reordering support Func &reorder_storage(const std::vector &dims, size_t start); diff --git a/src/Pipeline.cpp b/src/Pipeline.cpp index e935fd4852df..0b5a92e11c49 100644 --- a/src/Pipeline.cpp +++ b/src/Pipeline.cpp @@ -151,6 +151,10 @@ struct PipelineContents { bool trace_pipeline = false; + /** The number of live ProfilerScopes for this pipeline. While + * nonzero, realize leaves the profiler's statistics in place. */ + int profiler_scopes = 0; + /** Optional prefixes used to rename halide_-prefixed runtime symbols. * Empty unless set via Pipeline::apply_runtime_prefixes(). */ RuntimePrefixParams runtime_prefixes_params; @@ -837,7 +841,9 @@ Realization Pipeline::realize(JITUserContext *context, } // If we're profiling, report runtimes and reset profiler stats. - contents->jit_cache.finish_profiling(context); + if (contents->profiler_scopes == 0) { + contents->jit_cache.finish_profiling(context); + } jit_context.finalize(exit_status); // Crop back to the requested size if necessary @@ -900,6 +906,88 @@ void Pipeline::trace_pipeline() { contents->trace_pipeline = true; } +ProfilerScope::ProfilerScope(Pipeline p) + : pipeline(std::move(p)) { + user_assert(pipeline.defined()) << "Pipeline is undefined\n"; + pipeline.contents->profiler_scopes++; +} + +ProfilerScope::ProfilerScope(Func &f) + : ProfilerScope(f.pipeline()) { +} + +ProfilerScope::~ProfilerScope() { + if (--pipeline.contents->profiler_scopes > 0) { + return; + } + // Report and reset as a realize outside of any scope would have. + JITUserContext context{}; + JITFuncCallContext jit_context(&context, pipeline.jit_handlers()); + pipeline.contents->jit_cache.finish_profiling(&context); + jit_context.finalize(0); +} + +const halide_profiler_pipeline_stats *ProfilerScope::pipeline_stats() const { + const JITCache &cache = pipeline.contents->jit_cache; + if (!cache.jit_target.has_feature(Target::Profile) && + !cache.jit_target.has_feature(Target::ProfileByTimer)) { + return nullptr; + } + // The profiler lives in the shared JIT runtime, which the wasm + // module does not link against, so the symbols may not exist. + using GetStateFn = halide_profiler_state *(*)(); + using LockFn = void (*)(halide_profiler_state *); + auto find = [&](const char *symbol) { + return cache.jit_module.find_symbol_by_name(symbol).address; + }; + auto get_state = (GetStateFn)find("halide_profiler_get_state"); + auto lock = (LockFn)find("halide_profiler_lock"); + auto unlock = (LockFn)find("halide_profiler_unlock"); + if (!get_state || !lock || !unlock) { + return nullptr; + } + + // halide_profiler_get_pipeline_state compares names by pointer, so + // walk the list comparing by string instead. Recompiling the + // pipeline produces a new entry with the same name; the newest is + // at the head of the list. + const std::string name = pipeline.generate_function_name(); + halide_profiler_state *state = get_state(); + const halide_profiler_pipeline_stats *result = nullptr; + lock(state); + for (const halide_profiler_pipeline_stats *p = state->pipelines; p; + p = (const halide_profiler_pipeline_stats *)p->next) { + if (name == p->name) { + result = p; + break; + } + } + unlock(state); + return result; +} + +const halide_profiler_func_stats *ProfilerScope::func_stats(const std::string &name) const { + const halide_profiler_pipeline_stats *p = pipeline_stats(); + if (!p) { + return nullptr; + } + for (int i = 0; i < p->num_funcs; i++) { + const halide_profiler_func_stats &f = p->funcs[i]; + if (f.kind == halide_profiler_func_kind_func && + f.canonical_id == i && + name == f.name) { + return &f; + } + } + return nullptr; +} + +const halide_profiler_func_stats *ProfilerScope::func_stats(const Func &f) const { + // The profiler reports a Func under its display name if it has one. + const std::string &display_name = f.function().profiler_display_name(); + return func_stats(display_name.empty() ? f.name() : display_name); +} + // Make a vector of void *'s to pass to the jit call using the // currently bound value for all of the params and image // params. @@ -1114,7 +1202,9 @@ void Pipeline::realize(JITUserContext *context, debug(2) << "Back from jitted function. Exit status was " << exit_status << "\n"; // If we're profiling, report runtimes and reset profiler stats. - contents->jit_cache.finish_profiling(context); + if (contents->profiler_scopes == 0) { + contents->jit_cache.finish_profiling(context); + } jit_call_context.finalize(exit_status); } diff --git a/src/Pipeline.h b/src/Pipeline.h index 0fa8b593eedf..c7fb6e876aff 100644 --- a/src/Pipeline.h +++ b/src/Pipeline.h @@ -516,6 +516,58 @@ class Pipeline { private: std::string generate_function_name() const; + + friend class ProfilerScope; +}; + +/** Accumulates the profiler's statistics for a JIT-compiled Pipeline + * across multiple runs, and makes them available for inspection. + * + * Normally each call to realize on a Target with the Profile or + * ProfileByTimer feature prints the profiler's report and then resets + * its statistics. While a ProfilerScope for the Pipeline is alive, + * realize does neither, so statistics accumulate over every run within + * the scope and can be read via pipeline_stats and func_stats. The + * report is printed and the statistics reset when the scope is + * destroyed. + * + * The scope holds a copy of the Pipeline, which keeps its JIT-compiled + * runtime, and thus the memory backing the statistics, alive. The + * profiler's statistics are global to the process, so the reset at the + * end of a scope also discards the statistics of any other profiled + * Pipelines that ran in the meantime. */ +class ProfilerScope { + Pipeline pipeline; + +public: + explicit ProfilerScope(Pipeline p); + + /** Construct from the Pipeline that Func::realize uses for f. Copies + * of a Func do not share that Pipeline, so pass the Func that will + * be realized. */ + explicit ProfilerScope(Func &f); + + ~ProfilerScope(); + + ProfilerScope(const ProfilerScope &) = delete; + ProfilerScope &operator=(const ProfilerScope &) = delete; + + /** The statistics accumulated for the Pipeline so far. Returns + * nullptr if the Pipeline has not run with profiling enabled within + * this scope, or the Target does not support inspecting the profiler + * state (e.g. WebAssembly). The pointer is valid until the scope is + * destroyed. Only call this between runs of the Pipeline. */ + const halide_profiler_pipeline_stats *pipeline_stats() const; + + /** The statistics accumulated for one Func of the Pipeline, found by + * name. Returns the Func's canonical entry; a Func computed at + * several distinct sites has further entries in pipeline_stats() + * that share its canonical_id. Returns nullptr if pipeline_stats + * would, or if no Func of that name was profiled. */ + // @{ + const halide_profiler_func_stats *func_stats(const std::string &name) const; + const halide_profiler_func_stats *func_stats(const Func &f) const; + // @} }; struct ExternSignature { diff --git a/test/performance/memory_profiler.cpp b/test/performance/memory_profiler.cpp index 84e140dac123..afe2f5f6f52b 100644 --- a/test/performance/memory_profiler.cpp +++ b/test/performance/memory_profiler.cpp @@ -1,90 +1,46 @@ #include "Halide.h" -#include "HalideRuntime.h" #include #include -#include using namespace Halide; -// JITCache::finish_profiling calls halide_profiler_reset() right after -// the report fires, so we snapshot stats while the pipeline is still -// alive. halide_trace_end_pipeline fires inside the pipeline body — -// per-instance memory and stack counters are already populated by the -// time it fires; they just haven't been folded into pipeline_stats yet. -using GetStateFn = halide_profiler_state *(*)(); -Target jit_target; -std::string target_func_name; - -struct Stats { - int heap_peak = 0; - int num_mallocs = 0; - int malloc_avg = 0; - int stack_peak = 0; -}; -Stats captured_stats; - -int32_t snapshot_trace(JITUserContext *, const halide_trace_event_t *e) { - if (e->event != halide_trace_end_pipeline) { - return 0; - } - auto get_state = (GetStateFn)Internal::JITSharedRuntime::find_symbol( - jit_target, "halide_profiler_get_state"); - if (!get_state) { - return 0; +// Exits with code 1 if the profiler's stats for f don't match the +// expectations. exp_heap_peak is a range [min, max]; for an exact +// expectation pass it twice. +void check(const ProfilerScope &scope, const Func &f, + int exp_heap_peak_min, int exp_heap_peak_max, + int exp_num_mallocs, int exp_malloc_avg, int exp_stack_peak) { + const halide_profiler_func_stats *fs = scope.func_stats(f); + if (!fs) { + printf("No profiler stats found for %s\n", f.name().c_str()); + exit(1); } - // Only one pipeline is running, so just grab the head. - halide_profiler_instance_state *inst = get_state()->instances; - if (!inst) { - return 0; + int heap_peak = 0, num_mallocs = 0, malloc_avg = 0; + if (fs->num_allocs > 0) { + heap_peak = (int)fs->memory_peak; + num_mallocs = (int)fs->num_allocs; + malloc_avg = (int)(fs->memory_total / fs->num_allocs); } - halide_profiler_pipeline_stats *p = inst->pipeline_stats; - for (int i = 0; i < p->num_funcs; i++) { - if (std::string(p->funcs[i].name) != target_func_name) { - continue; - } - const halide_profiler_func_stats *fs = &inst->funcs[i]; - if (fs->num_allocs > 0) { - captured_stats.heap_peak = (int)fs->memory_peak; - captured_stats.num_mallocs = (int)fs->num_allocs; - captured_stats.malloc_avg = (int)(fs->memory_total / fs->num_allocs); - } - if (fs->stack_peak > 0) { - captured_stats.stack_peak = (int)fs->stack_peak; - } - break; - } - return 0; -} + int stack_peak = (int)fs->stack_peak; -void install_handlers(Pipeline &pipe, const Func &target_func) { - target_func_name = target_func.name(); - pipe.trace_pipeline(); - pipe.jit_handlers().custom_trace = snapshot_trace; -} - -// Exits with code 1 if the captured stats don't match the expectations. -// exp_heap_peak is a range [min, max]; for an exact expectation pass it twice. -void check(int exp_heap_peak_min, int exp_heap_peak_max, - int exp_num_mallocs, int exp_malloc_avg, int exp_stack_peak) { - if (captured_stats.heap_peak < exp_heap_peak_min || - captured_stats.heap_peak > exp_heap_peak_max) { + if (heap_peak < exp_heap_peak_min || heap_peak > exp_heap_peak_max) { printf("Peak heap was %d, expected in [%d, %d]\n", - captured_stats.heap_peak, exp_heap_peak_min, exp_heap_peak_max); + heap_peak, exp_heap_peak_min, exp_heap_peak_max); exit(1); } - if (captured_stats.num_mallocs != exp_num_mallocs) { + if (num_mallocs != exp_num_mallocs) { printf("Num of mallocs was %d, expected %d\n", - captured_stats.num_mallocs, exp_num_mallocs); + num_mallocs, exp_num_mallocs); exit(1); } - if (captured_stats.malloc_avg != exp_malloc_avg) { + if (malloc_avg != exp_malloc_avg) { printf("Malloc average was %d, expected %d\n", - captured_stats.malloc_avg, exp_malloc_avg); + malloc_avg, exp_malloc_avg); exit(1); } - if (captured_stats.stack_peak != exp_stack_peak) { + if (stack_peak != exp_stack_peak) { printf("Stack peak was %d, expected %d\n", - captured_stats.stack_peak, exp_stack_peak); + stack_peak, exp_stack_peak); exit(1); } } @@ -92,7 +48,6 @@ void check(int exp_heap_peak_min, int exp_heap_peak_max, template void run_case(const char *desc, Fn body) { printf("Running %s...\n", desc); - captured_stats = {}; body(); } @@ -103,8 +58,7 @@ int main(int argc, char **argv) { return 0; } - jit_target = target.with_feature(Target::Profile); - const Target &t = jit_target; + const Target t = target.with_feature(Target::Profile); Var x("x"), y("y"); @@ -117,9 +71,9 @@ int main(int argc, char **argv) { g1.compute_root(); Pipeline pipe(f1); - install_handlers(pipe, g1); + ProfilerScope scope(pipe); pipe.realize({size_x, size_y}, t); - check(0, 0, 0, 0, size_x * size_y * (int)sizeof(int)); + check(scope, g1, 0, 0, 0, 0, size_x * size_y * (int)sizeof(int)); }); run_case("simple heap allocation test 1", [&]() { @@ -131,10 +85,10 @@ int main(int argc, char **argv) { g2.compute_root(); Pipeline pipe(f2); - install_handlers(pipe, g2); + ProfilerScope scope(pipe); pipe.realize({size_x, size_y}, t); int total = (size_x + 1) * (size_y + 1) * (int)sizeof(int); - check(total, total, 1, total, 0); + check(scope, g2, total, total, 1, total, 0); }); run_case("heap allocate condition is always false test", [&]() { @@ -144,9 +98,9 @@ int main(int argc, char **argv) { g3.compute_root(); Pipeline pipe(f3); - install_handlers(pipe, g3); + ProfilerScope scope(pipe); pipe.realize({1000, 1000}, t); - check(0, 0, 0, 0, 0); + check(scope, g3, 0, 0, 0, 0, 0); }); run_case("stack allocate condition is always false test", [&]() { @@ -156,9 +110,9 @@ int main(int argc, char **argv) { g3.compute_root(); Pipeline pipe(f3); - install_handlers(pipe, g3); + ProfilerScope scope(pipe); pipe.realize({1000, 1000}, t); - check(0, 0, 0, 0, 0); + check(scope, g3, 0, 0, 0, 0, 0); }); run_case("allocate with non-trivial condition test", [&]() { @@ -176,7 +130,6 @@ int main(int argc, char **argv) { f5.compute_root(); Pipeline pipe(f6); - install_handlers(pipe, g4); const int total = size_x * (int)sizeof(float); const struct { @@ -189,11 +142,12 @@ int main(int argc, char **argv) { {false, false, 0, 0, 0}, }; for (auto &c : cases) { - captured_stats = {}; + // A fresh scope per run, so that stats don't accumulate. + ProfilerScope scope(pipe); toggle1.set(c.t1); toggle2.set(c.t2); pipe.realize({size_x}, t); - check(c.exp_heap, c.exp_heap, c.exp_mallocs, c.exp_avg, 0); + check(scope, g4, c.exp_heap, c.exp_heap, c.exp_mallocs, c.exp_avg, 0); } }); @@ -208,12 +162,12 @@ int main(int argc, char **argv) { f7.compute_at(f8, y); Pipeline pipe(f8); - install_handlers(pipe, g5); + ProfilerScope scope(pipe); pipe.realize({size_x, size_y}, t); int peak = size_x * (int)sizeof(int); int total = size_x * size_y * (int)sizeof(int); - check(peak, peak, size_y, total / size_y, 0); + check(scope, g5, peak, peak, size_y, total / size_y, 0); }); run_case("parallel allocate test", [&]() { @@ -228,12 +182,12 @@ int main(int argc, char **argv) { f10.parallel(y); Pipeline pipe(f10); - install_handlers(pipe, g6); + ProfilerScope scope(pipe); pipe.realize({size_x, size_y}, t); int min_heap = size_x * (int)sizeof(int); int total = size_x * size_y * (int)sizeof(int); - check(min_heap, total, size_y, total / size_y, 0); + check(scope, g6, min_heap, total, size_y, total / size_y, 0); }); run_case("simple heap allocation test 2", [&]() { @@ -245,10 +199,10 @@ int main(int argc, char **argv) { g7.compute_root(); Pipeline pipe(f11); - install_handlers(pipe, g7); + ProfilerScope scope(pipe); pipe.realize({size_x, size_y}, t); int total = size_x * size_y * (int)sizeof(int); - check(total, total, 1, total, 0); + check(scope, g7, total, total, 1, total, 0); }); run_case("parallel stack allocation test", [&]() { @@ -260,9 +214,9 @@ int main(int argc, char **argv) { f12.parallel(y); Pipeline pipe(f12); - install_handlers(pipe, g8); + ProfilerScope scope(pipe); pipe.realize({size_x, size_y}, t); - check(0, 0, 0, 0, size_x * size_y * (int)sizeof(int)); + check(scope, g8, 0, 0, 0, 0, size_x * size_y * (int)sizeof(int)); }); printf("Success!\n");