From e7e11896466ca20b370b8ac7af167d399c545b9d Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Thu, 3 Sep 2026 20:56:12 +0000 Subject: [PATCH 1/4] Add utility to measure the memory usage of child processes RB::RunCommandMeasuringRss() runs a command like std::system() and additionally reports the maximum resident set size of the child process tree via wait4(). This is the same value that "/usr/bin/time -v" reports as "Maximum resident set size", but without depending on an external time binary, temporary files or output parsing. This supersedes the approach of PR #190, which kept shelling out to the time binary. --- include/rootbench/MemoryMeasurement.h | 19 +++++++++++++++ lib/CMakeLists.txt | 1 + lib/MemoryMeasurement.cxx | 35 +++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 include/rootbench/MemoryMeasurement.h create mode 100644 lib/MemoryMeasurement.cxx diff --git a/include/rootbench/MemoryMeasurement.h b/include/rootbench/MemoryMeasurement.h new file mode 100644 index 00000000..20d076a8 --- /dev/null +++ b/include/rootbench/MemoryMeasurement.h @@ -0,0 +1,19 @@ +///\file Utilities to measure the memory usage of child processes. +#ifndef RB_MEMORYMEASUREMENT_H +#define RB_MEMORYMEASUREMENT_H + +#include + +namespace RB { +/// Runs the given command through the shell, like std::system(), and stores +/// the maximum resident set size of the child process tree in KiB in +/// maxRssKiB, as reported by wait4(). This is the same value that +/// "/usr/bin/time -v" reports as "Maximum resident set size", but without +/// depending on an external time binary. +/// +///\returns the exit status of the command as reported by std::system(), or -1 +/// if the child process could not be created. +int RunCommandMeasuringRss(const std::string &cmd, long &maxRssKiB); +} // namespace RB + +#endif diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 174fa9cd..f7216fc1 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -1,4 +1,5 @@ RB_ADD_LIBRARY(RBSupport ErrorHandling.cxx + MemoryMeasurement.cxx ) target_include_directories(RBSupport PUBLIC ${PROJECT_BINARY_DIR}/include ${PROJECT_SOURCE_DIR}/include) diff --git a/lib/MemoryMeasurement.cxx b/lib/MemoryMeasurement.cxx new file mode 100644 index 00000000..84211c77 --- /dev/null +++ b/lib/MemoryMeasurement.cxx @@ -0,0 +1,35 @@ +///\file Contains utilities to measure the memory usage of child processes. +#include "rootbench/MemoryMeasurement.h" + +#include +#include +#include + +namespace RB { + +int RunCommandMeasuringRss(const std::string &cmd, long &maxRssKiB) +{ + maxRssKiB = 0; + + pid_t pid = fork(); + if (pid < 0) + return -1; + if (pid == 0) { + execl("/bin/sh", "sh", "-c", cmd.c_str(), static_cast(nullptr)); + _exit(127); + } + + int status = 0; + rusage usage{}; + if (wait4(pid, &status, 0, &usage) < 0) + return -1; +#ifdef __APPLE__ + // On macOS, ru_maxrss is in bytes instead of KiB. + maxRssKiB = usage.ru_maxrss / 1024; +#else + maxRssKiB = usage.ru_maxrss; +#endif + return status; +} + +} // namespace RB From 2386ddbc1bfec19d282b113591e4e6fa89584e69 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Thu, 3 Sep 2026 20:56:28 +0000 Subject: [PATCH 2/4] Measure memory of interpreter benchmarks natively The maximum resident set size was obtained by prefixing the tutorial invocation with a hardcoded /usr/bin/time, parsing its output file with grep and awk, and passing the result through a temporary file shared by all benchmarks. On systems without the time binary, the invocation failed before even running the tutorial, so the benchmark silently measured a failed shell command and reported an RSS of zero. Use RB::RunCommandMeasuringRss() instead, and fail the benchmark with a clear error if the tutorial cannot be run. --- root/interpreter/InterpreterTest.h | 35 +++++++++++------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/root/interpreter/InterpreterTest.h b/root/interpreter/InterpreterTest.h index dc1c7053..5ecf5b02 100644 --- a/root/interpreter/InterpreterTest.h +++ b/root/interpreter/InterpreterTest.h @@ -1,47 +1,38 @@ -#include -#include +#include #include -#include -#include -#include - -#include #include "benchmark/benchmark.h" -#include - +#include "rootbench/MemoryMeasurement.h" #include "rootbench/RBConfig.h" -static int runTutorial(const std::string& dir, const std::string& filename, const std::string& perffile) { +static int runTutorial(const std::string& dir, const std::string& filename, long& maxRssKiB) { std::string rootsys = RB::GetRootSys(); std::string rootInvocation; if (!filename.empty()) { std::string fullpath = rootsys + "/" + dir + "/" + filename; - // We are writing /usr/bin/time -v output in file to get maximum resident memory for the benchmark - rootInvocation = "/usr/bin/time -v -o \"" + perffile + "\" root.exe -l -q -b -n -x \"" + fullpath + "\" -e return "; + rootInvocation = "root.exe -l -q -b -n -x \"" + fullpath + "\" -e return "; } else { - rootInvocation = "/usr/bin/time -v -o \"" + perffile + "\" root.exe -l -q -b "; + rootInvocation = "root.exe -l -q -b "; } - return std::system(rootInvocation.c_str()); + return RB::RunCommandMeasuringRss(rootInvocation, maxRssKiB); } static void TestTutorial(benchmark::State &state, const char *dir, const char *tutorial) { - int peakSize = 0; - std::string perftutorial ("perfile.txt"); + long peakSizeKiB = 0; for(auto _ : state){ auto start = std::chrono::high_resolution_clock::now(); - runTutorial(dir, tutorial,perftutorial); + int status = runTutorial(dir, tutorial, peakSizeKiB); auto end = std::chrono::high_resolution_clock::now(); + if (status != 0) { + state.SkipWithError(("failed to run tutorial \"" + std::string(tutorial) + "\"").c_str()); + return; + } auto elapsed_seconds = std::chrono::duration_cast>( end - start); state.SetIterationTime(elapsed_seconds.count()); - std::string memorytutorial = "cat \"" + perftutorial + "\"| grep 'Maximum resident set size' | awk '{print $6}' > tmp_mem_file"; - int res = std::system(memorytutorial.c_str()); - (void) res; - std::ifstream("tmp_mem_file") >> peakSize; } - state.counters.insert({{"RSS", peakSize}}); + state.counters.insert({{"RSS", static_cast(peakSizeKiB)}}); } From 4516a993f7e8997e2b8b32e93fc28215e14c18f0 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Thu, 3 Sep 2026 20:57:38 +0000 Subject: [PATCH 3/4] Measure memory and detect failures in PyROOT benchmarks The RSS counter of the PyROOT benchmarks was always reported as zero, since no memory measurement was implemented at all. Worse, the shell quoting of the TPython::Exec() invocation was broken, so the tutorials never actually ran: root only reported an interpreter parse error, which does not even show up in its exit status. Fix the quoting, propagate a failure of the interpreted code into the exit status via gSystem->Exit(), measure the memory usage with RB::RunCommandMeasuringRss(), and fail the benchmark with a clear error if the tutorial cannot be run. --- root/pyroot/PyROOTTest.h | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/root/pyroot/PyROOTTest.h b/root/pyroot/PyROOTTest.h index 7ea1213a..01f48377 100644 --- a/root/pyroot/PyROOTTest.h +++ b/root/pyroot/PyROOTTest.h @@ -1,35 +1,36 @@ -#include -#include +#include #include -#include -#include -#include - -#include #include "benchmark/benchmark.h" -#include - +#include "rootbench/MemoryMeasurement.h" #include "rootbench/RBConfig.h" -static int runTutorial(const std::string& dir, const std::string& filename) { +static int runTutorial(const std::string& dir, const std::string& filename, long& maxRssKiB) { std::string rootsys = RB::GetRootSys(); std::string fullpath = rootsys + "/" + dir + "/" + filename; - std::string rootInvocation = "root -l -b -q -e 'TPython::Exec(\" exec( open(\"" + fullpath + "\").read())\")'"; - return std::system(rootInvocation.c_str()); + // Exit with a non-zero status if the Python tutorial fails, since a failure + // of the interpreted code does not propagate into the exit status of root + // by itself. + std::string rootInvocation = + "root -l -b -q -e 'gSystem->Exit(!TPython::Exec(\"exec(open(\\\"" + fullpath + "\\\").read())\"))'"; + return RB::RunCommandMeasuringRss(rootInvocation, maxRssKiB); } static void TestTutorial(benchmark::State &state, const char *dir, const char *tutorial) { - int peakSize = 0; + long peakSizeKiB = 0; for(auto _ : state){ auto start = std::chrono::high_resolution_clock::now(); - runTutorial(dir, tutorial); + int status = runTutorial(dir, tutorial, peakSizeKiB); auto end = std::chrono::high_resolution_clock::now(); + if (status != 0) { + state.SkipWithError(("failed to run tutorial \"" + std::string(tutorial) + "\"").c_str()); + return; + } auto elapsed_seconds = std::chrono::duration_cast>( end - start); state.SetIterationTime(elapsed_seconds.count()); } - state.counters.insert({{"RSS", peakSize}}); + state.counters.insert({{"RSS", static_cast(peakSizeKiB)}}); } From ab97103ff752b0e52de9b90df48b218101c2e1aa Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Thu, 3 Sep 2026 21:00:51 +0000 Subject: [PATCH 4/4] Fix the location of the PyROOT tutorials The Python tutorials moved with the big tutorials reorganization in ROOT, and several of them do not exist anymore. Update the paths of the six remaining tutorials that still run standalone, following the same convention as the interpreter benchmarks, and drop the others. --- root/pyroot/RunPyROOT.cxx | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/root/pyroot/RunPyROOT.cxx b/root/pyroot/RunPyROOT.cxx index 833518ff..57478b6b 100644 --- a/root/pyroot/RunPyROOT.cxx +++ b/root/pyroot/RunPyROOT.cxx @@ -2,22 +2,16 @@ #include "benchmark/benchmark.h" -BENCHMARK_CAPTURE(TestTutorial, Test_hsimple_py, "tutorials/pyroot/", "hsimple.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); -BENCHMARK_CAPTURE(TestTutorial, Test_framework_py, "tutorials/pyroot/", "framework.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); -BENCHMARK_CAPTURE(TestTutorial, Test_hsum_py, "tutorials/pyroot/", "hsum.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); -BENCHMARK_CAPTURE(TestTutorial, Test_formula1_py, "tutorials/pyroot/", "formula1.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); -BENCHMARK_CAPTURE(TestTutorial, Test_fillrandom_py, "tutorials/pyroot/", "fillrandom.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); -BENCHMARK_CAPTURE(TestTutorial, Test_fit1_py, "tutorials/pyroot/", "fit1.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); -BENCHMARK_CAPTURE(TestTutorial, Test_h1draw_py, "tutorials/pyroot/", "h1draw.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); -BENCHMARK_CAPTURE(TestTutorial, Test_graph_py, "tutorials/pyroot/", "graph.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); -BENCHMARK_CAPTURE(TestTutorial, Test_gerrors_py, "tutorials/pyroot/", "gerrors.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); -BENCHMARK_CAPTURE(TestTutorial, Test_tornado_py, "tutorials/pyroot/", "tornado.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); -BENCHMARK_CAPTURE(TestTutorial, Test_surfaces_py, "tutorials/pyroot/", "surfaces.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); -BENCHMARK_CAPTURE(TestTutorial, Test_zdemo_py, "tutorials/pyroot/", "zdemo.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); -BENCHMARK_CAPTURE(TestTutorial, Test_geometry_py, "tutorials/pyroot/", "geometry.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); -BENCHMARK_CAPTURE(TestTutorial, Test_na49view_py, "tutorials/pyroot/", "na49view.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); -BENCHMARK_CAPTURE(TestTutorial, Test_file_py, "tutorials/pyroot/", "file.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); -BENCHMARK_CAPTURE(TestTutorial, Test_ntuple1_py, "tutorials/pyroot/", "ntuple1.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); -BENCHMARK_CAPTURE(TestTutorial, Test_rootmarks_py, "tutorials/pyroot/", "rootmarks.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); +// The Python tutorials that no longer exist in ROOT (framework.py, hsum.py, +// fillrandom.py, h1draw.py, graph.py, gerrors.py, zdemo.py, file.py and +// rootmarks.py) were dropped from the benchmarks, as well as geometry.py and +// na49view.py, which cannot run standalone because they depend on a geometry +// that needs to be created by other macros first. +BENCHMARK_CAPTURE(TestTutorial, Test_hsimple_py, "tutorials", "hsimple.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); +BENCHMARK_CAPTURE(TestTutorial, Test_formula1_py, "tutorials", "visualisation/graphics/formula1.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); +BENCHMARK_CAPTURE(TestTutorial, Test_fit1_py, "tutorials", "math/fit/fit1.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); +BENCHMARK_CAPTURE(TestTutorial, Test_tornado_py, "tutorials", "visualisation/graphics/tornado.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); +BENCHMARK_CAPTURE(TestTutorial, Test_surfaces_py, "tutorials", "visualisation/graphics/surfaces.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); +BENCHMARK_CAPTURE(TestTutorial, Test_ntuple1_py, "tutorials", "io/tree/ntuple1.py")->Unit(benchmark::kMicrosecond)->UseManualTime(); BENCHMARK_MAIN();