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.
This commit is contained in:
Martin Duffy
2026-09-22 09:36:44 -04:00
parent f5efe7a6b5
commit 1528113bdf
6 changed files with 85 additions and 5 deletions
+16 -3
View File
@@ -33,6 +33,8 @@
#include "cmGeneratorTarget.h" #include "cmGeneratorTarget.h"
#include "cmGlobalCommonGenerator.h" #include "cmGlobalCommonGenerator.h"
#include "cmGlobalFastbuildGenerator.h" #include "cmGlobalFastbuildGenerator.h"
#include "cmInstrumentation.h"
#include "cmInstrumentationQuery.h"
#include "cmLinkLineComputer.h" #include "cmLinkLineComputer.h"
#include "cmLinkLineDeviceComputer.h" #include "cmLinkLineDeviceComputer.h"
#include "cmList.h" #include "cmList.h"
@@ -1156,19 +1158,30 @@ void cmFastbuildNormalTargetGenerator::CollapseAllExecsIntoOneScriptfile(
} }
LogMessage("Writing collapsed Execs to " + scriptFileName); LogMessage("Writing collapsed Execs to " + scriptFileName);
auto const shell = cmGlobalFastbuildGenerator::GetExternalShellExecutable(); 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) { for (auto const& exec : execs) {
if (exec.ScriptFile.empty()) { if (exec.ScriptFile.empty()) {
scriptFile << cmSystemTools::ConvertToOutputPath(exec.ExecExecutable) scriptFile << cmSystemTools::ConvertToOutputPath(exec.ExecExecutable)
<< " " << exec.ExecArguments << '\n'; << " " << exec.ExecArguments << suffix;
} else { } else {
#if defined(_WIN32) #if defined(_WIN32)
scriptFile << "call " scriptFile << "call "
<< cmSystemTools::ConvertToWindowsOutputPath(exec.ScriptFile) << cmSystemTools::ConvertToWindowsOutputPath(exec.ScriptFile)
<< '\n'; << suffix;
#else #else
scriptFile << cmSystemTools::ConvertToOutputPath(shell) << " " scriptFile << cmSystemTools::ConvertToOutputPath(shell) << " "
<< cmSystemTools::ConvertToOutputPath(exec.ScriptFile) << cmSystemTools::ConvertToOutputPath(exec.ScriptFile)
<< '\n'; << suffix;
#endif #endif
} }
} }
+12
View File
@@ -21,6 +21,8 @@
#include "cmGeneratorTarget.h" #include "cmGeneratorTarget.h"
#include "cmGlobalCommonGenerator.h" #include "cmGlobalCommonGenerator.h"
#include "cmGlobalFastbuildGenerator.h" #include "cmGlobalFastbuildGenerator.h"
#include "cmInstrumentation.h"
#include "cmInstrumentationQuery.h"
#include "cmList.h" #include "cmList.h"
#include "cmListFileCache.h" #include "cmListFileCache.h"
#include "cmLocalCommonGenerator.h" #include "cmLocalCommonGenerator.h"
@@ -38,6 +40,7 @@
#include "cmTarget.h" #include "cmTarget.h"
#include "cmTargetTypes.h" #include "cmTargetTypes.h"
#include "cmValue.h" #include "cmValue.h"
#include "cmake.h"
#define FASTBUILD_DOLLAR_TAG "FASTBUILD_DOLLAR_TAG" #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% " "echo Batch file failed at line %FAIL_LINE% "
"with errorcode %ERRORLEVEL%\n" "with errorcode %ERRORLEVEL%\n"
"exit /b %ERROR_CODE%"; "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 #endif
} }
+17
View File
@@ -25,6 +25,8 @@
#include "cmGeneratorTarget.h" #include "cmGeneratorTarget.h"
#include "cmGlobalGenerator.h" #include "cmGlobalGenerator.h"
#include "cmGlobalNinjaGenerator.h" #include "cmGlobalNinjaGenerator.h"
#include "cmInstrumentation.h"
#include "cmInstrumentationQuery.h"
#include "cmList.h" #include "cmList.h"
#include "cmListFileCache.h" #include "cmListFileCache.h"
#include "cmLocalGenerator.h" #include "cmLocalGenerator.h"
@@ -476,6 +478,13 @@ std::string cmLocalNinjaGenerator::WriteCommandScript(
"echo Batch file failed at line %FAIL_LINE% " "echo Batch file failed at line %FAIL_LINE% "
"with errorcode %ERRORLEVEL%\n" "with errorcode %ERRORLEVEL%\n"
"exit /b %ERROR_CODE%"; "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 #endif
return scriptPath; return scriptPath;
@@ -567,6 +576,14 @@ std::string cmLocalNinjaGenerator::BuildCommandLine(
} }
cmd << *li; 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 #endif
return cmd.str(); return cmd.str();
} }
+19 -1
View File
@@ -29,6 +29,7 @@
#include "cmGlobalGenerator.h" #include "cmGlobalGenerator.h"
#include "cmGlobalUnixMakefileGenerator3.h" #include "cmGlobalUnixMakefileGenerator3.h"
#include "cmInstrumentation.h" #include "cmInstrumentation.h"
#include "cmInstrumentationQuery.h"
#include "cmList.h" #include "cmList.h"
#include "cmListFileCache.h" #include "cmListFileCache.h"
#include "cmLocalGenerator.h" #include "cmLocalGenerator.h"
@@ -631,7 +632,24 @@ void cmLocalUnixMakefileGenerator3::WriteMakeRule(
if (!commands.empty()) { if (!commands.empty()) {
// Write the list of commands. // 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()) { if (symbolic && !this->IsWatcomWMake()) {
os << ".PHONY : " << tgt << '\n'; os << ".PHONY : " << tgt << '\n';
@@ -8,6 +8,8 @@ if (NOT snippets)
endif() endif()
set(FOUND_SNIPPETS "") set(FOUND_SNIPPETS "")
unset(process_metrics_chain_head_rss)
unset(process_metrics_chain_tail_rss)
foreach(snippet IN LISTS snippets) foreach(snippet IN LISTS snippets)
get_filename_component(filename "${snippet}" NAME) get_filename_component(filename "${snippet}" NAME)
@@ -42,7 +44,13 @@ foreach(snippet IN LISTS snippets)
if (filename MATCHES "^custom-") if (filename MATCHES "^custom-")
string(JSON outputs ERROR_VARIABLE noOutputs GET "${contents}" outputs) string(JSON outputs ERROR_VARIABLE noOutputs GET "${contents}" outputs)
if (outputs MATCHES "process_metrics_custom\.stamp") 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") elseif (outputs MATCHES "process_metrics_cpu_custom\.stamp")
set(process_metrics_case "cpu") set(process_metrics_case "cpu")
endif() endif()
@@ -254,6 +262,16 @@ foreach(snippet IN LISTS snippets)
endforeach() 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 # Verify that listed snippets match expected roles
set(EXPECTED_SNIPPETS configure generate) set(EXPECTED_SNIPPETS configure generate)
if (ARGS_BUILD OR ARGS_BUILD_MAKE_PROGRAM) if (ARGS_BUILD OR ARGS_BUILD_MAKE_PROGRAM)
@@ -30,6 +30,8 @@ if (PROCESS_METRICS_QUERY)
COMMAND ${Python_EXECUTABLE} COMMAND ${Python_EXECUTABLE}
${CMAKE_CURRENT_LIST_DIR}/process_metrics_memory.py ${CMAKE_CURRENT_LIST_DIR}/process_metrics_memory.py
${CMAKE_CURRENT_BINARY_DIR}/process_metrics_custom.stamp ${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 VERBATIM
) )
add_custom_command( add_custom_command(