Browse Source

Merge topic 'instrumentation-build-hooks'

2680f30caf instrumentation: Allow multiple CALLBACK arguments
fc1d55f6a5 instrumentation: Add preBuild and postBuild hooks for ninja
f62a4ab2ee instrumentation: Refactor cmInstrumentation constructor and usage
c57d1752d4 cmUVProcessChain: Add Detached option for spawning daemons

Acked-by: Kitware Robot <[email protected]>
Merge-request: !10230
Brad King 8 tháng trước cách đây
mục cha
commit
7065e7a555

+ 5 - 3
Help/command/cmake_instrumentation.rst

@@ -23,8 +23,8 @@ only supported value for both fields is 1.  See :ref:`cmake-instrumentation v1`
 for details of the data output content and location.
 
 Each of the optional keywords ``HOOKS``, ``QUERIES``, and ``CALLBACK``
-correspond to one of the parameters to the :ref:`cmake-instrumentation v1 Query Files`. Note that the
-``CALLBACK`` keyword only accepts a single callback.
+correspond to one of the parameters to the :ref:`cmake-instrumentation v1 Query Files`.
+The ``CALLBACK`` keyword can be provided multiple times to create multiple callbacks.
 
 Whenever ``cmake_instrumentation`` is invoked, a query file is generated in
 ``<build>/.cmake/timing/v1/query/generated`` to enable instrumentation
@@ -43,7 +43,8 @@ equivalent JSON query file.
     DATA_VERSION 1
     HOOKS postGenerate preCMakeBuild postCMakeBuild
     QUERIES staticSystemInformation dynamicSystemInformation
-    CALLBACK "${CMAKE_COMMAND} -P /path/to/handle_data.cmake"
+    CALLBACK ${CMAKE_COMMAND} -P /path/to/handle_data.cmake
+    CALLBACK ${CMAKE_COMMAND} -P /path/to/handle_data_2.cmake
   )
 
 .. code-block:: json
@@ -58,5 +59,6 @@ equivalent JSON query file.
     ],
     "callbacks": [
       "/path/to/cmake -P /path/to/handle_data.cmake"
+      "/path/to/cmake -P /path/to/handle_data_2.cmake"
     ]
   }

+ 4 - 2
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``
 

+ 2 - 2
Source/CTest/cmCTestLaunch.cxx

@@ -260,7 +260,7 @@ void cmCTestLaunch::RunChild()
 
 int cmCTestLaunch::Run()
 {
-  auto instrumenter = cmInstrumentation(this->Reporter.OptionBuildDir);
+  auto instrumentation = cmInstrumentation(this->Reporter.OptionBuildDir);
   std::map<std::string, std::string> options;
   options["target"] = this->Reporter.OptionTargetName;
   options["source"] = this->Reporter.OptionSource;
@@ -270,7 +270,7 @@ int cmCTestLaunch::Run()
   std::map<std::string, std::string> arrayOptions;
   arrayOptions["outputs"] = this->Reporter.OptionOutput;
   arrayOptions["targetLabels"] = this->Reporter.OptionTargetLabels;
-  instrumenter.InstrumentCommand(
+  instrumentation.InstrumentCommand(
     this->Reporter.OptionCommandType, this->RealArgV,
     [this]() -> int {
       this->RunChild();

+ 52 - 0
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 =

+ 1 - 0
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(

+ 68 - 31
Source/cmInstrumentation.cxx

@@ -11,6 +11,7 @@
 #include <cm/optional>
 
 #include <cm3p/json/writer.h>
+#include <cm3p/uv.h>
 
 #include "cmsys/Directory.hxx"
 #include "cmsys/FStream.hxx"
@@ -22,9 +23,9 @@
 #include "cmStringAlgorithms.h"
 #include "cmSystemTools.h"
 #include "cmTimestamp.h"
+#include "cmUVProcessChain.h"
 
-cmInstrumentation::cmInstrumentation(std::string const& binary_dir,
-                                     bool clear_generated)
+cmInstrumentation::cmInstrumentation(std::string const& binary_dir)
 {
   std::string const uuid =
     cmExperimental::DataForFeature(cmExperimental::Feature::Instrumentation)
@@ -32,9 +33,6 @@ cmInstrumentation::cmInstrumentation(std::string const& binary_dir,
   this->binaryDir = binary_dir;
   this->timingDirv1 =
     cmStrCat(this->binaryDir, "/.cmake/instrumentation-", uuid, "/v1");
-  if (clear_generated) {
-    this->ClearGeneratedQueries();
-  }
   if (cm::optional<std::string> configDir =
         cmSystemTools::GetCMakeConfigDirectory()) {
     this->userTimingDirv1 =
@@ -57,24 +55,6 @@ void cmInstrumentation::LoadQueries()
   }
 }
 
-cmInstrumentation::cmInstrumentation(
-  std::string const& binary_dir,
-  std::set<cmInstrumentationQuery::Query>& queries_,
-  std::set<cmInstrumentationQuery::Hook>& hooks_, std::string& callback)
-{
-  this->binaryDir = binary_dir;
-  this->timingDirv1 = cmStrCat(
-    this->binaryDir, "/.cmake/instrumentation-",
-    cmExperimental::DataForFeature(cmExperimental::Feature::Instrumentation)
-      .Uuid,
-    "/v1");
-  this->queries = queries_;
-  this->hooks = hooks_;
-  if (!callback.empty()) {
-    this->callbacks.push_back(callback);
-  }
-}
-
 bool cmInstrumentation::ReadJSONQueries(std::string const& directory)
 {
   cmsys::Directory d;
@@ -99,21 +79,24 @@ void cmInstrumentation::ReadJSONQuery(std::string const& file)
                  this->callbacks);
 }
 
-void cmInstrumentation::WriteJSONQuery()
+void cmInstrumentation::WriteJSONQuery(
+  std::set<cmInstrumentationQuery::Query> const& queries_,
+  std::set<cmInstrumentationQuery::Hook> const& hooks_,
+  std::vector<std::vector<std::string>> const& callbacks_)
 {
   Json::Value root;
   root["version"] = 1;
   root["queries"] = Json::arrayValue;
-  for (auto const& query : this->queries) {
+  for (auto const& query : queries_) {
     root["queries"].append(cmInstrumentationQuery::QueryString[query]);
   }
   root["hooks"] = Json::arrayValue;
-  for (auto const& hook : this->hooks) {
+  for (auto const& hook : hooks_) {
     root["hooks"].append(cmInstrumentationQuery::HookString[hook]);
   }
   root["callbacks"] = Json::arrayValue;
-  for (auto const& callback : this->callbacks) {
-    root["callbacks"].append(callback);
+  for (auto const& callback : callbacks_) {
+    root["callbacks"].append(cmInstrumentation::GetCommandStr(callback));
   }
   cmsys::Directory d;
   int n = 0;
@@ -132,16 +115,27 @@ void cmInstrumentation::ClearGeneratedQueries()
   }
 }
 
-bool cmInstrumentation::HasQuery()
+bool cmInstrumentation::HasQuery() const
 {
   return this->hasQuery;
 }
 
-bool cmInstrumentation::HasQuery(cmInstrumentationQuery::Query query)
+bool cmInstrumentation::HasQuery(cmInstrumentationQuery::Query query) const
 {
   return (this->queries.find(query) != this->queries.end());
 }
 
+bool cmInstrumentation::HasHook(cmInstrumentationQuery::Hook hook) const
+{
+  return (this->hooks.find(hook) != this->hooks.end());
+}
+
+bool cmInstrumentation::HasPreOrPostBuildHook() const
+{
+  return (this->HasHook(cmInstrumentationQuery::Hook::PreBuild) ||
+          this->HasHook(cmInstrumentationQuery::Hook::PostBuild));
+}
+
 int cmInstrumentation::CollectTimingData(cmInstrumentationQuery::Hook hook)
 {
   // Don't run collection if hook is disabled
@@ -462,7 +456,7 @@ std::string cmInstrumentation::GetCommandStr(
   for (size_t i = 0; i < args.size(); ++i) {
     command_str = cmStrCat(command_str, args[i]);
     if (i < args.size() - 1) {
-      command_str = cmStrCat(command_str, " ");
+      command_str = cmStrCat(command_str, ' ');
     }
   }
   return command_str;
@@ -494,3 +488,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<std::string> 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);
+}

+ 11 - 12
Source/cmInstrumentation.h

@@ -21,14 +21,7 @@
 class cmInstrumentation
 {
 public:
-  // Read Queries
-  cmInstrumentation(std::string const& binary_dir,
-                    bool clear_generated = false);
-  // Create Query
-  cmInstrumentation(std::string const& binary_dir,
-                    std::set<cmInstrumentationQuery::Query>& queries,
-                    std::set<cmInstrumentationQuery::Hook>& hooks,
-                    std::string& callback);
+  cmInstrumentation(std::string const& binary_dir);
   int InstrumentCommand(
     std::string command_type, std::vector<std::string> const& command,
     std::function<int()> const& callback,
@@ -42,12 +35,19 @@ public:
                      std::chrono::system_clock::time_point systemStart);
   void GetPreTestStats();
   void LoadQueries();
-  bool HasQuery();
-  bool HasQuery(cmInstrumentationQuery::Query);
+  bool HasQuery() const;
+  bool HasQuery(cmInstrumentationQuery::Query) const;
+  bool HasHook(cmInstrumentationQuery::Hook) const;
+  bool HasPreOrPostBuildHook() const;
   bool ReadJSONQueries(std::string const& directory);
   void ReadJSONQuery(std::string const& file);
-  void WriteJSONQuery();
+  void WriteJSONQuery(std::set<cmInstrumentationQuery::Query> const& queries,
+                      std::set<cmInstrumentationQuery::Hook> const& hooks,
+                      std::vector<std::vector<std::string>> const& callback);
+  void ClearGeneratedQueries();
   int CollectTimingData(cmInstrumentationQuery::Hook hook);
+  int SpawnBuildDaemon();
+  int CollectTimingAfterBuild(int ppid);
   std::string errorMsg;
 
 private:
@@ -61,7 +61,6 @@ private:
   static void InsertTimingData(
     Json::Value& root, std::chrono::steady_clock::time_point steadyStart,
     std::chrono::system_clock::time_point systemStart);
-  void ClearGeneratedQueries();
   bool HasQueryFile(std::string const& file);
   static std::string GetCommandStr(std::vector<std::string> const& args);
   static std::string ComputeSuffixHash(std::string const& command_str);

+ 7 - 10
Source/cmInstrumentationCommand.cxx

@@ -18,6 +18,7 @@ file Copyright.txt or https://cmake.org/licensing for details.  */
 #include "cmInstrumentationQuery.h"
 #include "cmMakefile.h"
 #include "cmStringAlgorithms.h"
+#include "cmake.h"
 
 namespace {
 
@@ -80,7 +81,7 @@ bool cmInstrumentationCommand(std::vector<std::string> const& args,
     ArgumentParser::NonEmpty<std::string> DataVersion;
     ArgumentParser::NonEmpty<std::vector<std::string>> Queries;
     ArgumentParser::NonEmpty<std::vector<std::string>> Hooks;
-    ArgumentParser::NonEmpty<std::vector<std::string>> Callback;
+    ArgumentParser::NonEmpty<std::vector<std::vector<std::string>>> Callbacks;
   };
 
   static auto const parser = cmArgumentParser<Arguments>{}
@@ -88,7 +89,7 @@ bool cmInstrumentationCommand(std::vector<std::string> const& args,
                                .Bind("DATA_VERSION"_s, &Arguments::DataVersion)
                                .Bind("QUERIES"_s, &Arguments::Queries)
                                .Bind("HOOKS"_s, &Arguments::Hooks)
-                               .Bind("CALLBACK"_s, &Arguments::Callback);
+                               .Bind("CALLBACK"_s, &Arguments::Callbacks);
 
   std::vector<std::string> unparsedArguments;
   Arguments const arguments = parser.Parse(args, &unparsedArguments);
@@ -136,14 +137,10 @@ bool cmInstrumentationCommand(std::vector<std::string> const& args,
     hooks.insert(hook);
   }
 
-  std::string callback;
-  for (auto const& arg : arguments.Callback) {
-    callback = cmStrCat(callback, arg);
-  }
-
-  auto instrument = cmInstrumentation(
-    status.GetMakefile().GetHomeOutputDirectory(), queries, hooks, callback);
-  instrument.WriteJSONQuery();
+  status.GetMakefile()
+    .GetCMakeInstance()
+    ->GetInstrumentation()
+    ->WriteJSONQuery(queries, hooks, arguments.Callbacks);
 
   return true;
 }

+ 12 - 0
Source/cmUVProcessChain.cxx

@@ -156,6 +156,12 @@ cmUVProcessChainBuilder& cmUVProcessChainBuilder::SetWorkingDirectory(
   return *this;
 }
 
+cmUVProcessChainBuilder& cmUVProcessChainBuilder::SetDetached()
+{
+  this->Detached = true;
+  return *this;
+}
+
 uv_loop_t* cmUVProcessChainBuilder::GetLoop() const
 {
   return this->Loop;
@@ -337,6 +343,9 @@ void cmUVProcessChain::InternalData::SpawnProcess(
   arguments.push_back(nullptr);
   options.args = const_cast<char**>(arguments.data());
   options.flags = UV_PROCESS_WINDOWS_HIDE;
+  if (this->Builder->Detached) {
+    options.flags |= UV_PROCESS_DETACHED;
+  }
 #if UV_VERSION_MAJOR > 1 ||                                                   \
   (UV_VERSION_MAJOR == 1 && UV_VERSION_MINOR >= 48) ||                        \
   !defined(CMAKE_USE_SYSTEM_LIBUV)
@@ -380,6 +389,9 @@ void cmUVProcessChain::InternalData::SpawnProcess(
          process.Process.spawn(*this->Loop, options, &process)) < 0) {
     process.Finish();
   }
+  if (this->Builder->Detached) {
+    uv_unref((uv_handle_t*)process.Process);
+  }
   process.InputPipe.reset();
   process.OutputPipe.reset();
 }

+ 2 - 0
Source/cmUVProcessChain.h

@@ -38,6 +38,7 @@ public:
   cmUVProcessChainBuilder& SetExternalStream(Stream stdio, int fd);
   cmUVProcessChainBuilder& SetExternalStream(Stream stdio, FILE* stream);
   cmUVProcessChainBuilder& SetWorkingDirectory(std::string dir);
+  cmUVProcessChainBuilder& SetDetached();
 
   uv_loop_t* GetLoop() const;
 
@@ -68,6 +69,7 @@ private:
   std::vector<ProcessConfiguration> Processes;
   std::string WorkingDirectory;
   bool MergedBuiltinStreams = false;
+  bool Detached = false;
   uv_loop_t* Loop = nullptr;
 };
 

+ 12 - 9
Source/cmake.cxx

@@ -2611,21 +2611,24 @@ int cmake::ActualConfigure()
       cmStrCat(this->GetHomeOutputDirectory(), "/CMakeFiles"_s),
       this->FileAPI->GetConfigureLogVersions());
   }
+
+  this->Instrumentation =
+    cm::make_unique<cmInstrumentation>(this->State->GetBinaryDirectory());
+  this->Instrumentation->ClearGeneratedQueries();
 #endif
 
   // actually do the configure
   auto startTime = std::chrono::steady_clock::now();
 #if !defined(CMAKE_BOOTSTRAP)
-  cmInstrumentation instrumentation(this->State->GetBinaryDirectory(), true);
-  if (!instrumentation.errorMsg.empty()) {
-    cmSystemTools::Error(instrumentation.errorMsg);
+  if (!this->Instrumentation->errorMsg.empty()) {
+    cmSystemTools::Error(this->Instrumentation->errorMsg);
     return 1;
   }
   std::function<int()> doConfigure = [this]() -> int {
     this->GlobalGenerator->Configure();
     return 0;
   };
-  int ret = instrumentation.InstrumentCommand(
+  int ret = this->Instrumentation->InstrumentCommand(
     "configure", this->cmdArgs, [doConfigure]() { return doConfigure(); },
     cm::nullopt, cm::nullopt, true);
   if (ret != 0) {
@@ -2670,8 +2673,8 @@ int cmake::ActualConfigure()
   }
   // Setup launchers for instrumentation
 #if !defined(CMAKE_BOOTSTRAP)
-  instrumentation.LoadQueries();
-  if (instrumentation.HasQuery()) {
+  this->Instrumentation->LoadQueries();
+  if (this->Instrumentation->HasQuery()) {
     std::string launcher;
     if (mf->IsOn("CTEST_USE_LAUNCHERS")) {
       launcher =
@@ -3016,7 +3019,6 @@ int cmake::Generate()
   auto startTime = std::chrono::steady_clock::now();
 #if !defined(CMAKE_BOOTSTRAP)
   auto profilingRAII = this->CreateProfilingEntry("project", "generate");
-  cmInstrumentation instrumentation(this->State->GetBinaryDirectory());
   std::function<int()> doGenerate = [this]() -> int {
     if (!this->GlobalGenerator->Compute()) {
       return -1;
@@ -3025,7 +3027,8 @@ int cmake::Generate()
     return 0;
   };
 
-  int ret = instrumentation.InstrumentCommand(
+  this->Instrumentation->LoadQueries();
+  int ret = this->Instrumentation->InstrumentCommand(
     "generate", this->cmdArgs, [doGenerate]() { return doGenerate(); });
   if (ret != 0) {
     return ret;
@@ -3046,7 +3049,7 @@ int cmake::Generate()
     this->UpdateProgress(msg.str(), -1);
   }
 #if !defined(CMAKE_BOOTSTRAP)
-  instrumentation.CollectTimingData(
+  this->Instrumentation->CollectTimingData(
     cmInstrumentationQuery::Hook::PostGenerate);
 #endif
   if (!this->GraphVizFile.empty()) {

+ 6 - 0
Source/cmake.h

@@ -49,6 +49,7 @@ class cmDebuggerAdapter;
 
 class cmExternalMakefileProjectGeneratorFactory;
 class cmFileAPI;
+class cmInstrumentation;
 class cmFileTimeCache;
 class cmGlobalGenerator;
 class cmMakefile;
@@ -663,6 +664,10 @@ public:
 
 #if !defined(CMAKE_BOOTSTRAP)
   cmFileAPI* GetFileAPI() const { return this->FileAPI.get(); }
+  cmInstrumentation* GetInstrumentation() const
+  {
+    return this->Instrumentation.get();
+  }
 #endif
 
   cmState* GetState() const { return this->State.get(); }
@@ -816,6 +821,7 @@ private:
 #if !defined(CMAKE_BOOTSTRAP)
   std::unique_ptr<cmVariableWatch> VariableWatch;
   std::unique_ptr<cmFileAPI> FileAPI;
+  std::unique_ptr<cmInstrumentation> Instrumentation;
 #endif
 
   std::unique_ptr<cmState> State;

+ 12 - 0
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(

+ 9 - 1
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()

+ 35 - 0
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()

+ 4 - 0
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)

+ 6 - 0
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"
+)

+ 3 - 2
Tests/RunCMake/Instrumentation/query/cmake-command.cmake

@@ -8,7 +8,7 @@
     API_VERSION 1
     DATA_VERSION 1
     HOOKS postGenerate
-    CALLBACK "\"${CMAKE_COMMAND}\" -E echo callback1"
+    CALLBACK \"${CMAKE_COMMAND}\" -E echo callback1
   )
   # Query 2
   cmake_instrumentation(
@@ -16,5 +16,6 @@
     DATA_VERSION 1
     HOOKS postCMakeBuild
     QUERIES staticSystemInformation dynamicSystemInformation
-    CALLBACK "\"${CMAKE_COMMAND}\" -E echo callback2"
+    CALLBACK \"${CMAKE_COMMAND}\" -E echo callback2
+    CALLBACK \"${CMAKE_COMMAND}\" -E echo callback3
   )

+ 2 - 1
Tests/RunCMake/Instrumentation/query/generated/query-2.json.in

@@ -1,7 +1,8 @@
 {
   "callbacks" :
   [
-    "\"@CMAKE_COMMAND@\" -E echo callback2"
+    "\"@CMAKE_COMMAND@\" -E echo callback2",
+    "\"@CMAKE_COMMAND@\" -E echo callback3"
   ],
   "hooks" :
   [