From 1528113bdf8e8c51b86ec2f617c2cbabb8488564 Mon Sep 17 00:00:00 2001 From: Martin Duffy Date: Mon, 14 Sep 2026 15:18:38 -0400 Subject: [PATCH] instrumentation: Prevent inherited process metrics in chained commands Shells may exec the final chained command instead of forking it, preserving resource usage accumulated from earlier children. When processMetrics is enabled, append a shell builtin to generated command chains and scripts for Ninja, Makefiles, and FASTBuild to prevent this optimization while preserving command exit status. --- Source/cmFastbuildNormalTargetGenerator.cxx | 19 +++++++++++++++--- Source/cmFastbuildTargetGenerator.cxx | 12 +++++++++++ Source/cmLocalNinjaGenerator.cxx | 17 ++++++++++++++++ Source/cmLocalUnixMakefileGenerator3.cxx | 20 ++++++++++++++++++- .../Instrumentation/check-data-dir.cmake | 20 ++++++++++++++++++- .../Instrumentation/project/CMakeLists.txt | 2 ++ 6 files changed, 85 insertions(+), 5 deletions(-) diff --git a/Source/cmFastbuildNormalTargetGenerator.cxx b/Source/cmFastbuildNormalTargetGenerator.cxx index db1640c06f..6344f3e45c 100644 --- a/Source/cmFastbuildNormalTargetGenerator.cxx +++ b/Source/cmFastbuildNormalTargetGenerator.cxx @@ -33,6 +33,8 @@ #include "cmGeneratorTarget.h" #include "cmGlobalCommonGenerator.h" #include "cmGlobalFastbuildGenerator.h" +#include "cmInstrumentation.h" +#include "cmInstrumentationQuery.h" #include "cmLinkLineComputer.h" #include "cmLinkLineDeviceComputer.h" #include "cmList.h" @@ -1156,19 +1158,30 @@ void cmFastbuildNormalTargetGenerator::CollapseAllExecsIntoOneScriptfile( } LogMessage("Writing collapsed Execs to " + scriptFileName); auto const shell = cmGlobalFastbuildGenerator::GetExternalShellExecutable(); + char const* suffix = "\n"; +#if !defined(_WIN32) && !defined(CMAKE_BOOTSTRAP) + cmInstrumentation* instrumentation = + this->GetLocalGenerator()->GetCMakeInstance()->GetInstrumentation(); + if (instrumentation->HasOption( + cmInstrumentationQuery::Option::ProcessMetrics)) { + // Prevent shell exec optimization from passing accumulated child resource + // usage to the final instrumentation wrapper. + suffix = " && :\n"; + } +#endif for (auto const& exec : execs) { if (exec.ScriptFile.empty()) { scriptFile << cmSystemTools::ConvertToOutputPath(exec.ExecExecutable) - << " " << exec.ExecArguments << '\n'; + << " " << exec.ExecArguments << suffix; } else { #if defined(_WIN32) scriptFile << "call " << cmSystemTools::ConvertToWindowsOutputPath(exec.ScriptFile) - << '\n'; + << suffix; #else scriptFile << cmSystemTools::ConvertToOutputPath(shell) << " " << cmSystemTools::ConvertToOutputPath(exec.ScriptFile) - << '\n'; + << suffix; #endif } } diff --git a/Source/cmFastbuildTargetGenerator.cxx b/Source/cmFastbuildTargetGenerator.cxx index 2522cc8cd3..6c22958ab0 100644 --- a/Source/cmFastbuildTargetGenerator.cxx +++ b/Source/cmFastbuildTargetGenerator.cxx @@ -21,6 +21,8 @@ #include "cmGeneratorTarget.h" #include "cmGlobalCommonGenerator.h" #include "cmGlobalFastbuildGenerator.h" +#include "cmInstrumentation.h" +#include "cmInstrumentationQuery.h" #include "cmList.h" #include "cmListFileCache.h" #include "cmLocalCommonGenerator.h" @@ -38,6 +40,7 @@ #include "cmTarget.h" #include "cmTargetTypes.h" #include "cmValue.h" +#include "cmake.h" #define FASTBUILD_DOLLAR_TAG "FASTBUILD_DOLLAR_TAG" @@ -180,6 +183,15 @@ void cmFastbuildTargetGenerator::WriteScriptEpilog(cmsys::ofstream& file) const "echo Batch file failed at line %FAIL_LINE% " "with errorcode %ERRORLEVEL%\n" "exit /b %ERROR_CODE%"; +#elif !defined(CMAKE_BOOTSTRAP) + cmInstrumentation* instrumentation = + this->GetLocalGenerator()->GetCMakeInstance()->GetInstrumentation(); + if (instrumentation->HasOption( + cmInstrumentationQuery::Option::ProcessMetrics)) { + // Prevent shell exec optimization from passing accumulated child resource + // usage to the final instrumentation wrapper. + file << ":\n"; + } #endif } diff --git a/Source/cmLocalNinjaGenerator.cxx b/Source/cmLocalNinjaGenerator.cxx index f482005345..cb842db7d8 100644 --- a/Source/cmLocalNinjaGenerator.cxx +++ b/Source/cmLocalNinjaGenerator.cxx @@ -25,6 +25,8 @@ #include "cmGeneratorTarget.h" #include "cmGlobalGenerator.h" #include "cmGlobalNinjaGenerator.h" +#include "cmInstrumentation.h" +#include "cmInstrumentationQuery.h" #include "cmList.h" #include "cmListFileCache.h" #include "cmLocalGenerator.h" @@ -476,6 +478,13 @@ std::string cmLocalNinjaGenerator::WriteCommandScript( "echo Batch file failed at line %FAIL_LINE% " "with errorcode %ERRORLEVEL%\n" "exit /b %ERROR_CODE%"; +#elif !defined(CMAKE_BOOTSTRAP) + if (this->GetCMakeInstance()->GetInstrumentation()->HasOption( + cmInstrumentationQuery::Option::ProcessMetrics)) { + // Prevent shell exec optimization from passing accumulated child resource + // usage to the final instrumentation wrapper. + script << ":\n"; + } #endif return scriptPath; @@ -567,6 +576,14 @@ std::string cmLocalNinjaGenerator::BuildCommandLine( } cmd << *li; } +# ifndef CMAKE_BOOTSTRAP + if (this->GetCMakeInstance()->GetInstrumentation()->HasOption( + cmInstrumentationQuery::Option::ProcessMetrics)) { + // Prevent shell exec optimization from passing accumulated child resource + // usage to the final instrumentation wrapper. + cmd << " && :"; + } +# endif #endif return cmd.str(); } diff --git a/Source/cmLocalUnixMakefileGenerator3.cxx b/Source/cmLocalUnixMakefileGenerator3.cxx index eac59f7f92..dc5425538a 100644 --- a/Source/cmLocalUnixMakefileGenerator3.cxx +++ b/Source/cmLocalUnixMakefileGenerator3.cxx @@ -29,6 +29,7 @@ #include "cmGlobalGenerator.h" #include "cmGlobalUnixMakefileGenerator3.h" #include "cmInstrumentation.h" +#include "cmInstrumentationQuery.h" #include "cmList.h" #include "cmListFileCache.h" #include "cmLocalGenerator.h" @@ -631,7 +632,24 @@ void cmLocalUnixMakefileGenerator3::WriteMakeRule( if (!commands.empty()) { // Write the list of commands. - os << cmWrap("\t", commands, "", "\n") << '\n'; + bool protectProcessMetrics = false; +#if !defined(_WIN32) && !defined(CMAKE_BOOTSTRAP) + if (this->GetCMakeInstance()->GetInstrumentation()->HasOption( + cmInstrumentationQuery::Option::ProcessMetrics)) { + // Prevent shell exec optimization from passing accumulated child + // resource usage to the final instrumentation wrapper. + protectProcessMetrics = true; + } +#endif + for (std::string const& command : commands) { + os << '\t' << command; + // Preserve the instrumentation daemon's explicit exec command. + if (protectProcessMetrics && !command.empty() && + !cmHasLiteralPrefix(command, "exec ")) { + os << " && :"; + } + os << '\n'; + } } if (symbolic && !this->IsWatcomWMake()) { os << ".PHONY : " << tgt << '\n'; diff --git a/Tests/RunCMake/Instrumentation/check-data-dir.cmake b/Tests/RunCMake/Instrumentation/check-data-dir.cmake index a8f9ea05c4..8a021c36aa 100644 --- a/Tests/RunCMake/Instrumentation/check-data-dir.cmake +++ b/Tests/RunCMake/Instrumentation/check-data-dir.cmake @@ -8,6 +8,8 @@ if (NOT snippets) endif() set(FOUND_SNIPPETS "") +unset(process_metrics_chain_head_rss) +unset(process_metrics_chain_tail_rss) foreach(snippet IN LISTS snippets) get_filename_component(filename "${snippet}" NAME) @@ -42,7 +44,13 @@ foreach(snippet IN LISTS snippets) if (filename MATCHES "^custom-") string(JSON outputs ERROR_VARIABLE noOutputs GET "${contents}" outputs) if (outputs MATCHES "process_metrics_custom\.stamp") - set(process_metrics_case "memory") + string(JSON command GET "${contents}" command) + if (command MATCHES "process_metrics_memory\\.py") + set(process_metrics_case "memory") + string(JSON process_metrics_chain_head_rss GET "${process_metrics}" maxRSS) + else() + string(JSON process_metrics_chain_tail_rss GET "${process_metrics}" maxRSS) + endif() elseif (outputs MATCHES "process_metrics_cpu_custom\.stamp") set(process_metrics_case "cpu") endif() @@ -254,6 +262,16 @@ foreach(snippet IN LISTS snippets) endforeach() +# The final chained command must not inherit the preceding command's memory peak. +# (SunOS does not populate maxRSS, so both commands report zero.) +if (ARGS_PROCESS_METRICS_QUERY AND ARGS_BUILD AND NOT CMAKE_HOST_SYSTEM_NAME STREQUAL "SunOS") + if (NOT DEFINED process_metrics_chain_head_rss OR NOT DEFINED process_metrics_chain_tail_rss) + add_error("Missing process metrics for one or both chained commands") + elseif (process_metrics_chain_head_rss EQUAL process_metrics_chain_tail_rss) + add_error("Chained commands reported identical maxRSS: ${process_metrics_chain_tail_rss} KiB") + endif() +endif() + # Verify that listed snippets match expected roles set(EXPECTED_SNIPPETS configure generate) if (ARGS_BUILD OR ARGS_BUILD_MAKE_PROGRAM) diff --git a/Tests/RunCMake/Instrumentation/project/CMakeLists.txt b/Tests/RunCMake/Instrumentation/project/CMakeLists.txt index d2063720fe..3c9b5d3dee 100644 --- a/Tests/RunCMake/Instrumentation/project/CMakeLists.txt +++ b/Tests/RunCMake/Instrumentation/project/CMakeLists.txt @@ -30,6 +30,8 @@ if (PROCESS_METRICS_QUERY) COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_LIST_DIR}/process_metrics_memory.py ${CMAKE_CURRENT_BINARY_DIR}/process_metrics_custom.stamp + # The last command must not inherit the preceding command's memory peak. + COMMAND ${CMAKE_COMMAND} -E true VERBATIM ) add_custom_command(