From fc1d55f6a5bd9b05bd2b461b9596e8494fc041b6 Mon Sep 17 00:00:00 2001 From: Martin Duffy Date: Fri, 24 Jan 2025 09:58:34 -0500 Subject: instrumentation: Add preBuild and postBuild hooks for ninja Allows instrumentation indexing and callbacks to occur at the start or end of every `ninja` invocation. --- Help/manual/cmake-instrumentation.7.rst | 6 ++- Source/cmGlobalNinjaGenerator.cxx | 52 ++++++++++++++++++++++ Source/cmGlobalNinjaGenerator.h | 1 + Source/cmInstrumentation.cxx | 45 +++++++++++++++++++ Source/cmInstrumentation.h | 2 + Source/ctest.cxx | 12 +++++ Tests/RunCMake/Instrumentation/RunCMakeTest.cmake | 10 ++++- .../Instrumentation/check-ninja-hooks.cmake | 35 +++++++++++++++ Tests/RunCMake/Instrumentation/hook.cmake | 4 ++ .../query/cmake-command-ninja.cmake | 6 +++ 10 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 Tests/RunCMake/Instrumentation/check-ninja-hooks.cmake create mode 100644 Tests/RunCMake/Instrumentation/query/cmake-command-ninja.cmake diff --git a/Help/manual/cmake-instrumentation.7.rst b/Help/manual/cmake-instrumentation.7.rst index 60db156..da9c0ff 100644 --- a/Help/manual/cmake-instrumentation.7.rst +++ b/Help/manual/cmake-instrumentation.7.rst @@ -117,8 +117,10 @@ optional. should be one of the following: * ``postGenerate`` - * ``preCMakeBuild`` - * ``postCMakeBuild`` + * ``preBuild`` (:ref:`Ninja Generators`. only, when ``ninja`` is invoked) + * ``postBuild`` (:ref:`Ninja Generators`. only, when ``ninja`` completes) + * ``preCMakeBuild`` (when ``cmake --build`` is invoked) + * ``postCMakeBuild`` (when ``cmake --build`` completes) * ``postInstall`` * ``postTest`` diff --git a/Source/cmGlobalNinjaGenerator.cxx b/Source/cmGlobalNinjaGenerator.cxx index bf2da7d..e3eee6a 100644 --- a/Source/cmGlobalNinjaGenerator.cxx +++ b/Source/cmGlobalNinjaGenerator.cxx @@ -32,6 +32,7 @@ #include "cmGeneratedFileStream.h" #include "cmGeneratorTarget.h" #include "cmGlobalGenerator.h" +#include "cmInstrumentation.h" #include "cmLinkLineComputer.h" #include "cmList.h" #include "cmListFileCache.h" @@ -1761,6 +1762,13 @@ void cmGlobalNinjaGenerator::WriteBuiltinTargets(std::ostream& os) this->WriteTargetRebuildManifest(os); this->WriteTargetClean(os); this->WriteTargetHelp(os); +#if !defined(CMAKE_BOOTSTRAP) + if (this->GetCMakeInstance() + ->GetInstrumentation() + ->HasPreOrPostBuildHook()) { + this->WriteTargetInstrument(os); + } +#endif for (std::string const& config : this->GetConfigNames()) { this->WriteTargetDefault(*this->GetConfigFileStream(config)); @@ -1835,6 +1843,14 @@ void cmGlobalNinjaGenerator::WriteTargetRebuildManifest(std::ostream& os) } reBuild.ImplicitDeps.push_back(this->CMakeCacheFile); +#if !defined(CMAKE_BOOTSTRAP) + if (this->GetCMakeInstance() + ->GetInstrumentation() + ->HasPreOrPostBuildHook()) { + reBuild.ExplicitDeps.push_back(this->NinjaOutputPath("start_instrument")); + } +#endif + // Use 'console' pool to get non buffered output of the CMake re-run call // Available since Ninja 1.5 if (this->SupportsDirectConsole()) { @@ -2180,6 +2196,42 @@ void cmGlobalNinjaGenerator::WriteTargetHelp(std::ostream& os) } } +void cmGlobalNinjaGenerator::WriteTargetInstrument(std::ostream& os) +{ + // Write rule + { + cmNinjaRule rule("START_INSTRUMENT"); + rule.Command = cmStrCat( + "\"", cmSystemTools::GetCTestCommand(), "\" --start-instrumentation \"", + this->GetCMakeInstance()->GetHomeOutputDirectory(), "\""); +#ifndef _WIN32 + /* + * On Unix systems, Ninja will prefix the command with `/bin/sh -c`. + * Use exec so that Ninja is the parent process of the command. + */ + rule.Command = cmStrCat("exec ", rule.Command); +#endif + rule.Description = "Collecting build metrics"; + rule.Comment = "Rule to initialize instrumentation daemon."; + rule.Restat = "1"; + WriteRule(*this->RulesFileStream, rule); + } + + // Write build + { + cmNinjaBuild phony("phony"); + phony.Comment = "Phony target to keep START_INSTRUMENTATION out of date."; + phony.Outputs.push_back(this->NinjaOutputPath("CMakeFiles/instrument")); + cmNinjaBuild instrument("START_INSTRUMENT"); + instrument.Comment = "Start instrumentation daemon."; + instrument.Outputs.push_back(this->NinjaOutputPath("start_instrument")); + instrument.ExplicitDeps.push_back( + this->NinjaOutputPath("CMakeFiles/instrument")); + WriteBuild(os, phony); + WriteBuild(os, instrument); + } +} + void cmGlobalNinjaGenerator::InitOutputPathPrefix() { this->OutputPathPrefix = diff --git a/Source/cmGlobalNinjaGenerator.h b/Source/cmGlobalNinjaGenerator.h index 5ccb314..acc9ee4 100644 --- a/Source/cmGlobalNinjaGenerator.h +++ b/Source/cmGlobalNinjaGenerator.h @@ -536,6 +536,7 @@ private: void WriteTargetRebuildManifest(std::ostream& os); bool WriteTargetCleanAdditional(std::ostream& os); void WriteTargetClean(std::ostream& os); + void WriteTargetInstrument(std::ostream& os); void WriteTargetHelp(std::ostream& os); void ComputeTargetDependsClosure( diff --git a/Source/cmInstrumentation.cxx b/Source/cmInstrumentation.cxx index a919621..245195f 100644 --- a/Source/cmInstrumentation.cxx +++ b/Source/cmInstrumentation.cxx @@ -11,6 +11,7 @@ #include #include +#include #include "cmsys/Directory.hxx" #include "cmsys/FStream.hxx" @@ -22,6 +23,7 @@ #include "cmStringAlgorithms.h" #include "cmSystemTools.h" #include "cmTimestamp.h" +#include "cmUVProcessChain.h" cmInstrumentation::cmInstrumentation(std::string const& binary_dir) { @@ -485,3 +487,46 @@ std::string cmInstrumentation::ComputeSuffixTime() << std::setfill('0') << std::setw(4) << tms; return ss.str(); } + +/* + * Called by ctest --start-instrumentation as part of the START_INSTRUMENTATION + * rule when using the Ninja generator. + * This creates a detached process which waits for the Ninja process to die + * before running the postBuild hook. In this way, the postBuild hook triggers + * after every ninja invocation, regardless of whether the build passed or + * failed. + */ +int cmInstrumentation::SpawnBuildDaemon() +{ + // preBuild Hook + this->CollectTimingData(cmInstrumentationQuery::Hook::PreBuild); + + // postBuild Hook + if (this->HasHook(cmInstrumentationQuery::Hook::PostBuild)) { + auto ninja_pid = uv_os_getppid(); + if (ninja_pid) { + std::vector args; + args.push_back(cmSystemTools::GetCTestCommand()); + args.push_back("--wait-and-collect-instrumentation"); + args.push_back(this->binaryDir); + args.push_back(std::to_string(ninja_pid)); + auto builder = cmUVProcessChainBuilder().SetDetached().AddCommand(args); + auto chain = builder.Start(); + uv_run(&chain.GetLoop(), UV_RUN_DEFAULT); + } + } + return 0; +} + +/* + * Always called by ctest --wait-and-collect-instrumentation in a detached + * process. Waits for the given PID to end before running the postBuild hook. + * + * See SpawnBuildDaemon() + */ +int cmInstrumentation::CollectTimingAfterBuild(int ppid) +{ + while (0 == uv_kill(ppid, 0)) { + }; + return this->CollectTimingData(cmInstrumentationQuery::Hook::PostBuild); +} diff --git a/Source/cmInstrumentation.h b/Source/cmInstrumentation.h index 07b9e1d..e9eceac 100644 --- a/Source/cmInstrumentation.h +++ b/Source/cmInstrumentation.h @@ -46,6 +46,8 @@ public: std::string& callback); void ClearGeneratedQueries(); int CollectTimingData(cmInstrumentationQuery::Hook hook); + int SpawnBuildDaemon(); + int CollectTimingAfterBuild(int ppid); std::string errorMsg; private: diff --git a/Source/ctest.cxx b/Source/ctest.cxx index d253d5b..ad82daa 100644 --- a/Source/ctest.cxx +++ b/Source/ctest.cxx @@ -189,6 +189,18 @@ int main(int argc, char const* const* argv) return cmCTestLaunch::Main(argc, argv, cmCTestLaunch::Op::Instrument); } + // Dispatch post-build instrumentation daemon for ninja + if (argc == 3 && strcmp(argv[1], "--start-instrumentation") == 0) { + return cmInstrumentation(argv[2]).SpawnBuildDaemon(); + } + + // Dispatch 'ctest --collect-instrumentation' once given PID finishes + if (argc == 4 && + strcmp(argv[1], "--wait-and-collect-instrumentation") == 0) { + return cmInstrumentation(argv[2]).CollectTimingAfterBuild( + std::stoi(argv[3])); + } + // Dispatch 'ctest --collect-instrumentation' mode directly. if (argc == 3 && strcmp(argv[1], "--collect-instrumentation") == 0) { return cmInstrumentation(argv[2]).CollectTimingData( diff --git a/Tests/RunCMake/Instrumentation/RunCMakeTest.cmake b/Tests/RunCMake/Instrumentation/RunCMakeTest.cmake index f7dc4eb..2b70972 100644 --- a/Tests/RunCMake/Instrumentation/RunCMakeTest.cmake +++ b/Tests/RunCMake/Instrumentation/RunCMakeTest.cmake @@ -6,7 +6,7 @@ function(instrument test) set(config "${CMAKE_CURRENT_LIST_DIR}/config") set(ENV{CMAKE_CONFIG_DIR} ${config}) cmake_parse_arguments(ARGS - "BUILD;INSTALL;TEST;COPY_QUERIES;NO_WARN;STATIC_QUERY;DYNAMIC_QUERY;INSTALL_PARALLEL;MANUAL_HOOK" + "BUILD;BUILD_MAKE_PROGRAM;INSTALL;TEST;COPY_QUERIES;NO_WARN;STATIC_QUERY;DYNAMIC_QUERY;INSTALL_PARALLEL;MANUAL_HOOK" "CHECK_SCRIPT;CONFIGURE_ARG" "" ${ARGN}) set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test}) set(uuid "a37d1069-1972-4901-b9c9-f194aaf2b6e0") @@ -58,6 +58,9 @@ function(instrument test) if (ARGS_BUILD) run_cmake_command(${test}-build ${CMAKE_COMMAND} --build . --config Debug) endif() + if (ARGS_BUILD_MAKE_PROGRAM) + run_cmake_command(${test}-make-program ${RunCMake_MAKE_PROGRAM}) + endif() if (ARGS_INSTALL) run_cmake_command(${test}-install ${CMAKE_COMMAND} --install . --prefix install --config Debug) endif() @@ -112,3 +115,8 @@ instrument(cmake-command-bad-arg NO_WARN) instrument(cmake-command-parallel-install BUILD INSTALL TEST NO_WARN INSTALL_PARALLEL DYNAMIC_QUERY CHECK_SCRIPT check-data-dir.cmake) +if (UNIX AND ${RunCMake_GENERATOR} MATCHES "^Ninja") + instrument(cmake-command-ninja NO_WARN + BUILD_MAKE_PROGRAM + CHECK_SCRIPT check-ninja-hooks.cmake) +endif() diff --git a/Tests/RunCMake/Instrumentation/check-ninja-hooks.cmake b/Tests/RunCMake/Instrumentation/check-ninja-hooks.cmake new file mode 100644 index 0000000..60b2f7b --- /dev/null +++ b/Tests/RunCMake/Instrumentation/check-ninja-hooks.cmake @@ -0,0 +1,35 @@ +set(NUM_TRIES 30) +set(DELAY 1) + +if (NOT EXISTS ${v1}/preBuild.hook) + set(RunCMake_TEST_FAILED "preBuild hook did not run\n") +endif() + +macro(hasPostBuildArtifacts) + if (NOT postBuildRan AND EXISTS ${v1}/postBuild.hook) + set(postBuildRan 1) + endif() + if (NOT dataDirClean) + file(GLOB snippets "${v1}/data/*") + if ("${snippets}" STREQUAL "") + set(dataDirClean 1) + endif() + endif() +endmacro() + +set(postBuildRan 0) +set(dataDirClean 0) +foreach(_ RANGE ${NUM_TRIES}) + hasPostBuildArtifacts() + if (postBuildRan AND dataDirClean) + break() + endif() + execute_process(COMMAND ${CMAKE_COMMAND} -E sleep ${DELAY}) +endforeach() + +if (NOT postBuildRan) + string(APPEND RunCMake_TEST_FAILED "postBuild hook did not run\n") +endif() +if (NOT dataDirClean) + string(APPEND RunCMake_TEST_FAILED "Snippet files not fully removed post build\n") +endif() diff --git a/Tests/RunCMake/Instrumentation/hook.cmake b/Tests/RunCMake/Instrumentation/hook.cmake index 973e7d8..f84a3b9 100644 --- a/Tests/RunCMake/Instrumentation/hook.cmake +++ b/Tests/RunCMake/Instrumentation/hook.cmake @@ -68,3 +68,7 @@ has_key(vendorString ${staticSystemInformation} ${hasStaticInfo}) if (NOT ERROR_MESSAGE MATCHES "^$") message(FATAL_ERROR ${ERROR_MESSAGE}) endif() + +get_filename_component(dataDir ${index} DIRECTORY) +get_filename_component(v1 ${dataDir} DIRECTORY) +file(TOUCH ${v1}/${hook}.hook) diff --git a/Tests/RunCMake/Instrumentation/query/cmake-command-ninja.cmake b/Tests/RunCMake/Instrumentation/query/cmake-command-ninja.cmake new file mode 100644 index 0000000..60acebd --- /dev/null +++ b/Tests/RunCMake/Instrumentation/query/cmake-command-ninja.cmake @@ -0,0 +1,6 @@ +cmake_instrumentation( + API_VERSION 1 + DATA_VERSION 1 + HOOKS preBuild postBuild + CALLBACK "\"${CMAKE_COMMAND}\" -P \"${CMAKE_SOURCE_DIR}/../hook.cmake\" 0" +) -- cgit v0.12