instrumentation: Collect and record project build system metrics

Add a feature for collecting build instrumentation for CMake projects.

Issue: #26099
This commit is contained in:
Martin Duffy
2024-12-11 10:38:47 -05:00
committed by Brad King
parent 8a3c195188
commit 097d4fd1b5
95 changed files with 2331 additions and 66 deletions
+62
View File
@@ -0,0 +1,62 @@
cmake_instrumentation
---------------------
.. versionadded:: 3.32
Enables interacting with the
:manual:`CMake Instrumentation API <cmake-instrumentation(7)>`.
This allows for configuring instrumentation at the project-level.
.. code-block:: cmake
cmake_instrumentation(
API_VERSION <version>
DATA_VERSION <version>
[HOOKS <hooks>...]
[QUERIES <queries>...]
[CALLBACK <callback>]
)
The ``API_VERSION`` and ``DATA_VERSION`` must always be given. Currently, the
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.
Whenever ``cmake_instrumentation`` is invoked, a query file is generated in
``<build>/.cmake/timing/v1/query/generated`` to enable instrumentation
with the provided arguments.
Example
^^^^^^^
The following example shows an invocation of the command and its
equivalent JSON query file.
.. code-block:: cmake
cmake_instrumentation(
API_VERSION 1
DATA_VERSION 1
HOOKS postGenerate preCMakeBuild postCMakeBuild
QUERIES staticSystemInformation dynamicSystemInformation
CALLBACK "${CMAKE_COMMAND} -P /path/to/handle_data.cmake"
)
.. code-block:: json
{
"version": 1,
"hooks": [
"postGenerate", "preCMakeBuild", "postCMakeBuild"
],
"queries": [
"staticSystemInformation", "dynamicSystemInformation"
],
"callbacks": [
"/path/to/cmake -P /path/to/handle_data.cmake"
]
}
+14
View File
@@ -119,3 +119,17 @@ When activated, this experimental feature provides the following:
* Targets with the property set to a true value will have their C++ build
information exported to the build database.
Instrumentation
===============
In order to activate support for the :command:`cmake_instrumentation` command,
set
* variable ``CMAKE_EXPERIMENTAL_INSTRUMENTATION`` to
* value ``a37d1069-1972-4901-b9c9-f194aaf2b6e0``.
To enable instrumentation at the user-level, files should be blaced under
either
``<CMAKE_CONFIG_DIR>/instrumentation-a37d1069-1972-4901-b9c9-f194aaf2b6e0`` or
``<CMAKE_BINARY_DIR>/.cmake/instrumentation-a37d1069-1972-4901-b9c9-f194aaf2b6e0``.
+1
View File
@@ -64,6 +64,7 @@ Reference Manuals
/manual/cmake-file-api.7
/manual/cmake-generator-expressions.7
/manual/cmake-generators.7
/manual/cmake-instrumentation.7
/manual/cmake-language.7
/manual/cmake-modules.7
/manual/cmake-packages.7
+1
View File
@@ -89,6 +89,7 @@ These commands are available only in CMake projects.
/command/aux_source_directory
/command/build_command
/command/cmake_file_api
/command/cmake_instrumentation
/command/create_test_sourcelist
/command/define_property
/command/enable_language
+374
View File
@@ -0,0 +1,374 @@
.. cmake-manual-description: CMake Instrumentation
cmake-instrumentation(7)
************************
.. versionadded:: 3.32
.. only:: html
.. contents::
Introduction
============
The CMake Instrumentation API allows for the collection of timing data, target
information and system diagnostic information during the configure, generate,
build, test and install steps for a CMake project.
This feature is only available for projects using the :ref:`Makefile Generators`
or the :ref:`Ninja Generators`.
All interactions with the CMake instrumentation API must specify both an API
version and a Data version. At this time, there is only one version for each of
these: the `API v1`_ and `Data v1`_.
When instrumentation is enabled, CMake sets the :prop_gbl:`RULE_LAUNCH_COMPILE`,
:prop_gbl:`RULE_LAUNCH_LINK` and :prop_gbl:`RULE_LAUNCH_CUSTOM` global properties
to use the ``ctest --instrument`` launcher. Whenever a command is executed with
instrumentation enabled, a `v1 Snippet File`_ is created in the project build
tree. If the project has been configured with :module:`CTestUseLaunchers`,
``ctest --instrument`` will also include the behavior usually performed by
``ctest --launch``.
Hooks are specific intervals, configured as part of the `v1 Query Files`_,
during which snippet data files are coallated. Whenever a hook executes, an
index file is generated containing a list of snippet files newer than the
previous indexing, and a sequence of custom callbacks are executed using
the index file as an argument.
Indexing and callbacks can also be performed by manually invoking
``ctest --collect-instrumentation``.
These callbacks, defined either at the user-level or project-level should read
the instrumentation data and perform any desired handling of it. The index file
and its listed snippets are automatically deleted by CMake once all callbacks
have completed.
Configuring Instrumentation at the User-Level
---------------------------------------------
Instrumentation can be configured at the user-level by placing query files in the
:envvar:`CMAKE_CONFIG_DIR` under
``<config_dir>/instrumentation/<version>/query/``. This version of CMake
supports only one version schema, `API v1`_.
Configuring Instrumentation at the Project-Level
------------------------------------------------
Configuring Instrumentation at the project level can be done by placing query
files under ``<build>/.cmake/instrumentation/query/`` at the top of a build
tree.
Additionally, project code can contain instrumentation queries with the
:command:`cmake_instrumentation` command.
.. _`cmake-instrumentation v1`:
API v1
======
The API version specifies both the subdirectory layout of the instrumentation data,
and the format of the query files.
The Instrumentation API v1 is housed in the ``instrumentation/v1/`` directory
under either ``<build>/.cmake/`` for output data and project-level queries, or
``<config_dir>/`` for user-level queries. The ``v1`` component of this
directory is what signifies the API version. It has the following
subdirectories:
``query/``
Holds query files written by users or clients. Any file with the ``.json``
file extension will be recognized as a query file. These files are owned by
whichever client or user creates them.
``query/generated/``
Holds query files generated by a CMake project with the
:command:`cmake_instrumentation` command. These files are owned by CMake and
are deleted and regenerated automatically during the CMake configure step.
``data/``
Holds instrumentation data collected on the project. CMake owns all data
files, they should never be removed by other processes.
.. _`cmake-instrumentation v1 Query Files`:
v1 Query Files
--------------
Any file with the ``.json`` extension under the ``instrumentation/v1/query/``
directory is recognized as a query for instrumentation data.
These files must contain a JSON object with the following keys which are all
optional.
``version``
The Data version of snippet file to generate, an integer. Currently the only
supported version is `1`.
``callbacks``
A list of command-line strings for callbacks to handle collected timing
data. Whenever these callbacks are executed, the full path to a
`v1 Index File`_ is appended to the arguments included in the string.
``hooks``
A list of strings specifying when instrumentation data should be collated
and user callbacks should be invoked on the data. Elements in this list
should be one of the following:
* ``postGenerate``
* ``preCMakeBuild``
* ``postCMakeBuild``
* ``postInstall``
* ``postTest``
``queries``
A list of strings specifying additional optional data to collect during
instrumentation. Elements in this list should be one of the following:
``staticSystemInformation``
Enables collection of the static information about the host machine
CMake is being run from. This data is collected once at each hook and
included in the generated ``index-<has>.json`` file.
``dynamicSystemInformation``
Enables collection of the dynamic information about the host machine
CMake is being run from. Data is collected for every snippet file
generated by CMake, with data immediately before and after the command is
executed.
The ``callbacks`` listed will be invoked during the specified hooks
*at a minimum*. When there are multiple queries, the ``callbacks``, ``hooks``
and ``queries`` between them will be merged. Therefore, if any query file
includes any ``hooks``, every ``callback`` across all query files will be
executed at every ``hook`` across all query files. Additionally, if any query
file includes any optional ``queries``, the optional query data will be present
in all data files.
Example:
.. code-block:: json
{
"version": 1,
"callbacks": [
"/usr/bin/python callback.py",
"/usr/bin/cmake -P callback.cmake arg",
],
"hooks": [
"postCMakeBuild",
"postInstall"
],
"queries": [
"staticSystemInformation",
"dynamicSystemInformation"
]
}
In this example, after every ``cmake --build`` or ``cmake --install``
invocation, an index file ``index-<hash>.json`` will be generated in
``<build>/.cmake/instrumentation/v1/data`` containing a list of data snippet
files created since the previous indexing. The commands
``/usr/bin/python callback.py index-<hash>.json`` and
``/usr/bin/cmake -P callback.cmake arg index-<hash>.json`` will be executed in
that order. The index file will contain the ``staticSystemInformation`` data and
each snippet file listed in the index will contain the
``dynamicSystemInformation`` data. Once both callbacks have completed, the index
file and all snippet files listed by it will be deleted from the project build
tree.
Data v1
=======
Data version specifies the contents of the output files generated by the CMake
instrumentation API. There are two types of data files generated. When using
the `API v1`_, these files live in ``<build>/.cmake/instrumentation/v1/data/``
under the project build tree. These are the `v1 Snippet File`_ and
`v1 Index File`_.
v1 Snippet File
---------------
Snippet files are generated for every compile, link and custom command invoked
as part of the CMake build or install step and contain instrumentation data about
the command executed. Additionally, snippet files are created for the following:
* The CMake configure step
* The CMake generate step
* Entire build step (executed with ``cmake --build``)
* Entire install step (executed with ``cmake --install``)
* Each ``ctest`` invocation
* Each individual test executed by ``ctest``.
Snippet files have a filename with the syntax ``<role>-<timestamp>-<hash>.json``
and contain the following data:
``version``
The Data version of the snippet file, an integer. Currently the version is
always `1`.
``command``
The full command executed.
``result``
The exit-value of the command, an integer.
``role``
The type of command executed, which will be one of the following values:
* ``compile``
* ``link``
* ``custom``
* ``cmakeBuild``
* ``install``
* ``ctest``
* ``test``
``target``
The CMake target associated with the command. Only included when ``role`` is
one of ``compile``, ``link``, ``custom``.
``targetType``
The :prop_tgt:`TYPE` of the target. Only included when ``role`` is
``link``.
``timeStart``
Time at which the command started, expressed as the number of milliseconds
since the system epoch.
``duration``
The duration that the command ran for, expressed in milliseconds.
``outputs``
The command's output file(s), an array. Only included when ``role`` is one
of: ``compile``, ``link``, ``custom``.
``outputSizes``
The size(s) in bytes of the ``outputs``, an array. For files which do not
exist, the size is 0.
``source``
The source file being compiled. Only included when ``role`` is ``compile``.
``language``
The language of the source file being compiled. Only included when ``role`` is
``compile``.
``testName``
The name of the test being executed. Only included when ``role`` is ``test``.
``dynamicSystemInformation``
Specifies the dynamic information collected about the host machine
CMake is being run from. Data is collected for every snippet file
generated by CMake, with data immediately before and after the command is
executed.
``beforeHostMemoryUsed``
The Host Memory Used in KiB at ``timeStart``.
``afterHostMemoryUsed``
The Host Memory Used in KiB at ``timeStop``.
``beforeCPULoadAverage``
The Average CPU Load at ``timeStart``.
``afterCPULoadAverage``
The Average CPU Load at ``timeStop``.
Example:
.. code-block:: json
{
"version": 1,
"command" : "/usr/bin/c++ -MD -MT CMakeFiles/main.dir/main.cxx.o -MF CMakeFiles/main.dir/main.cxx.o.d -o CMakeFiles/main.dir/main.cxx.o -c <src>/main.cxx",
"role" : "compile",
"return" : 1,
"target": "main",
"language" : "C++",
"outputs" : [ "CMakeFiles/main.dir/main.cxx.o" ],
"outputSizes" : [ 0 ],
"source" : "<src>/main.cxx"
"dynamicSystemInformation" :
{
"afterCPULoadAverage" : 2.3500000000000001,
"afterHostMemoryUsed" : 6635680.0
"beforeCPULoadAverage" : 2.3500000000000001,
"beforeHostMemoryUsed" : 6635832.0
},
"timeStart" : 31997009,
"timeStop" : 31997056
}
v1 Index File
-------------
Index files contain a list of `v1 Snippet File`_. It serves as an entry point
for navigating the instrumentation data.
``version``
The Data version of the index file, an integer. Currently the version is
always `1`.
``buildDir``
The build directory of the CMake project.
``dataDir``
The full path to the ``<build>/.cmake/instrumentation/v1/data/`` directory.
``hook``
The name of the hook responsible for generating the index file. In addition
to the hooks that can be specified by one of the `v1 Query Files`_, this value may
be set to ``manual`` if indexing is performed by invoking
``ctest --collect-instrumentation``.
``snippets``
Contains a list of `v1 Snippet File`_. This includes all snippet files
generated since the previous index file was created. The file paths are
relative to ``dataDir``.
``staticSystemInformation``
Specifies the static information collected about the host machine
CMake is being run from. This data is collected once at each hook and
included in the generated ``index-<has>.json`` file.
* ``OSName``
* ``OSPlatform``
* ``OSRelease``
* ``OSVersion``
* ``familyId``
* ``hostname``
* ``is64Bits``
* ``modelId``
* ``numberOfLogicalCPU``
* ``numberOfPhysicalCPU``
* ``processorAPICID``
* ``processorCacheSize``
* ``processorClockFrequency``
* ``processorName``
* ``totalPhysicalMemory``
* ``totalVirtualMemory``
* ``vendorID``
* ``vendorString``
Example:
.. code-block:: json
{
"version": 1,
"hook": "manual",
"buildDir": "<build>",
"dataDir": "<build>/.cmake/instrumentation/v1/data",
"snippets": [
"configure-<timestamp>-<hash>.json",
"generate-<timestamp>-<hash>.json",
"compile-<timestamp>-<hash>.json",
"compile-<timestamp>-<hash>.json",
"link-<timestamp>-<hash>.json",
"install-<timestamp>-<hash>.json",
"ctest-<timestamp>-<hash>.json",
"test-<timestamp>-<hash>.json",
"test-<timestamp>-<hash>.json",
]
}
+6
View File
@@ -359,6 +359,12 @@ add_library(
cmInstallDirectoryGenerator.cxx
cmInstallScriptHandler.h
cmInstallScriptHandler.cxx
cmInstrumentation.h
cmInstrumentation.cxx
cmInstrumentationCommand.h
cmInstrumentationCommand.cxx
cmInstrumentationQuery.h
cmInstrumentationQuery.cxx
cmJSONHelpers.cxx
cmJSONHelpers.h
cmJSONState.cxx
+42 -9
View File
@@ -5,6 +5,7 @@
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <memory>
#include <utility>
@@ -15,6 +16,7 @@
#include "cmCTestLaunchReporter.h"
#include "cmGlobalGenerator.h"
#include "cmInstrumentation.h"
#include "cmMakefile.h"
#include "cmProcessOutput.h"
#include "cmState.h"
@@ -33,7 +35,7 @@
# include <io.h> // for _setmode
#endif
cmCTestLaunch::cmCTestLaunch(int argc, const char* const* argv)
cmCTestLaunch::cmCTestLaunch(int argc, const char* const* argv, Op operation)
{
if (!this->ParseArguments(argc, argv)) {
return;
@@ -45,6 +47,7 @@ cmCTestLaunch::cmCTestLaunch(int argc, const char* const* argv)
this->ScrapeRulesLoaded = false;
this->HaveOut = false;
this->HaveErr = false;
this->Operation = operation;
}
cmCTestLaunch::~cmCTestLaunch() = default;
@@ -61,6 +64,8 @@ bool cmCTestLaunch::ParseArguments(int argc, const char* const* argv)
DoingLanguage,
DoingTargetName,
DoingTargetType,
DoingCommandType,
DoingRole,
DoingBuildDir,
DoingCount,
DoingFilterPrefix
@@ -71,6 +76,8 @@ bool cmCTestLaunch::ParseArguments(int argc, const char* const* argv)
const char* arg = argv[i];
if (strcmp(arg, "--") == 0) {
arg0 = i + 1;
} else if (strcmp(arg, "--command-type") == 0) {
doing = DoingCommandType;
} else if (strcmp(arg, "--output") == 0) {
doing = DoingOutput;
} else if (strcmp(arg, "--source") == 0) {
@@ -81,6 +88,8 @@ bool cmCTestLaunch::ParseArguments(int argc, const char* const* argv)
doing = DoingTargetName;
} else if (strcmp(arg, "--target-type") == 0) {
doing = DoingTargetType;
} else if (strcmp(arg, "--role") == 0) {
doing = DoingRole;
} else if (strcmp(arg, "--build-dir") == 0) {
doing = DoingBuildDir;
} else if (strcmp(arg, "--filter-prefix") == 0) {
@@ -109,6 +118,12 @@ bool cmCTestLaunch::ParseArguments(int argc, const char* const* argv)
} else if (doing == DoingFilterPrefix) {
this->Reporter.OptionFilterPrefix = arg;
doing = DoingNone;
} else if (doing == DoingCommandType) {
this->Reporter.OptionCommandType = arg;
doing = DoingNone;
} else if (doing == DoingRole) {
this->Reporter.OptionRole = arg;
doing = DoingNone;
}
}
@@ -233,15 +248,33 @@ void cmCTestLaunch::RunChild()
int cmCTestLaunch::Run()
{
this->RunChild();
auto instrumenter = cmInstrumentation(this->Reporter.OptionBuildDir);
std::map<std::string, std::string> options;
options["target"] = this->Reporter.OptionTargetName;
options["source"] = this->Reporter.OptionSource;
options["language"] = this->Reporter.OptionLanguage;
options["targetType"] = this->Reporter.OptionTargetType;
options["role"] = this->Reporter.OptionRole;
std::map<std::string, std::string> arrayOptions;
arrayOptions["outputs"] = this->Reporter.OptionOutput;
instrumenter.InstrumentCommand(
this->Reporter.OptionCommandType, this->RealArgV,
[this]() -> int {
this->RunChild();
return 0;
},
options, arrayOptions);
if (this->CheckResults()) {
return this->Reporter.ExitCode;
if (this->Operation == Op::Normal) {
if (this->CheckResults()) {
return this->Reporter.ExitCode;
}
this->LoadConfig();
this->Reporter.WriteXML();
}
this->LoadConfig();
this->Reporter.WriteXML();
return this->Reporter.ExitCode;
}
@@ -314,14 +347,14 @@ bool cmCTestLaunch::ScrapeLog(std::string const& fname)
return false;
}
int cmCTestLaunch::Main(int argc, const char* const argv[])
int cmCTestLaunch::Main(int argc, const char* const argv[], Op operation)
{
if (argc == 2) {
std::cerr << "ctest --launch: this mode is for internal CTest use only"
<< std::endl;
return 1;
}
cmCTestLaunch self(argc, argv);
cmCTestLaunch self(argc, argv, operation);
return self.Run();
}
+12 -2
View File
@@ -20,16 +20,23 @@ class RegularExpression;
*/
class cmCTestLaunch
{
public:
enum class Op
{
Normal,
Instrument,
};
/** Entry point from ctest executable main(). */
static int Main(int argc, const char* const argv[]);
static int Main(int argc, const char* const argv[], Op operation);
cmCTestLaunch(const cmCTestLaunch&) = delete;
cmCTestLaunch& operator=(const cmCTestLaunch&) = delete;
private:
// Initialize the launcher from its command line.
cmCTestLaunch(int argc, const char* const* argv);
cmCTestLaunch(int argc, const char* const* argv, Op operation);
~cmCTestLaunch();
// Run the real command.
@@ -65,4 +72,7 @@ private:
// Configuration
void LoadConfig();
// Mode
Op Operation;
};
+2
View File
@@ -38,6 +38,8 @@ public:
std::string OptionTargetType;
std::string OptionBuildDir;
std::string OptionFilterPrefix;
std::string OptionCommandType;
std::string OptionRole;
// The real command line appearing after launcher arguments.
std::string CWD;
+10
View File
@@ -34,6 +34,7 @@ cmCTestRunTest::cmCTestRunTest(cmCTestMultiProcessHandler& multiHandler,
, CTest(MultiTestHandler.CTest)
, TestHandler(MultiTestHandler.TestHandler)
, TestProperties(MultiTestHandler.Properties[Index])
, Instrumentation(cmSystemTools::GetLogicalWorkingDirectory())
{
}
@@ -663,6 +664,9 @@ bool cmCTestRunTest::StartTest(size_t completed, size_t total)
return false;
}
this->StartTime = this->CTest->CurrentTime();
if (this->Instrumentation.HasQuery()) {
this->Instrumentation.GetPreTestStats();
}
return this->ForkProcess();
}
@@ -1012,6 +1016,12 @@ void cmCTestRunTest::WriteLogOutputTop(size_t completed, size_t total)
void cmCTestRunTest::FinalizeTest(bool started)
{
if (this->Instrumentation.HasQuery()) {
this->Instrumentation.InstrumentTest(
this->TestProperties->Name, this->ActualCommand, this->Arguments,
this->TestProcess->GetExitValue(), this->TestProcess->GetStartTime(),
this->TestProcess->GetSystemStartTime());
}
this->MultiTestHandler.FinishTestProcess(this->TestProcess->GetRunner(),
started);
}
+2
View File
@@ -14,6 +14,7 @@
#include "cmCTest.h"
#include "cmCTestMultiProcessHandler.h"
#include "cmCTestTestHandler.h"
#include "cmInstrumentation.h"
#include "cmProcess.h"
/** \class cmRunTest
@@ -140,6 +141,7 @@ private:
int NumberOfRunsTotal = 1; // default to 1 run of the test
bool RunAgain = false; // default to not having to run again
size_t TotalNumberOfTests;
cmInstrumentation Instrumentation;
};
inline int getNumWidth(size_t n)
+9
View File
@@ -66,6 +66,14 @@ public:
void SetId(int id) { this->Id = id; }
int64_t GetExitValue() const { return this->ExitValue; }
cmDuration GetTotalTime() { return this->TotalTime; }
std::chrono::steady_clock::time_point GetStartTime()
{
return this->StartTime;
}
std::chrono::system_clock::time_point GetSystemStartTime()
{
return this->SystemStartTime;
}
enum class Exception
{
@@ -97,6 +105,7 @@ private:
cm::optional<cmDuration> Timeout;
TimeoutReason TimeoutReason_ = TimeoutReason::Normal;
std::chrono::steady_clock::time_point StartTime;
std::chrono::system_clock::time_point SystemStartTime;
cmDuration TotalTime;
bool ReadHandleClosed = false;
bool ProcessHandleClosed = false;
+26 -14
View File
@@ -10,6 +10,7 @@
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <functional>
#include <initializer_list>
#include <iostream>
#include <map>
@@ -50,6 +51,8 @@
#include "cmExecutionStatus.h"
#include "cmGeneratedFileStream.h"
#include "cmGlobalGenerator.h"
#include "cmInstrumentation.h"
#include "cmInstrumentationQuery.h"
#include "cmJSONState.h"
#include "cmList.h"
#include "cmListFileCache.h"
@@ -2623,23 +2626,32 @@ int cmCTest::Run(std::vector<std::string> const& args)
}
#endif
// now what should cmake do? if --build-and-test was specified then
// we run the build and test handler and return
if (cmakeAndTest) {
return this->RunCMakeAndTest();
}
cmInstrumentation instrumentation(
cmSystemTools::GetCurrentWorkingDirectory());
std::function<int()> doTest = [this, &cmakeAndTest, &runScripts,
&processSteps]() -> int {
// now what should cmake do? if --build-and-test was specified then
// we run the build and test handler and return
if (cmakeAndTest) {
return this->RunCMakeAndTest();
}
// -S, -SP, and/or -SP was specified
if (!runScripts.empty()) {
return this->RunScripts(runScripts);
}
// -S, -SP, and/or -SP was specified
if (!runScripts.empty()) {
return this->RunScripts(runScripts);
}
// -D, -T, and/or -M was specified
if (processSteps) {
return this->ProcessSteps();
}
// -D, -T, and/or -M was specified
if (processSteps) {
return this->ProcessSteps();
}
return this->ExecuteTests();
return this->ExecuteTests();
};
int ret = instrumentation.InstrumentCommand("ctest", args,
[doTest]() { return doTest(); });
instrumentation.CollectTimingData(cmInstrumentationQuery::Hook::PostTest);
return ret;
}
int cmCTest::RunScripts(
+2
View File
@@ -52,6 +52,7 @@
#include "cmInstallCommand.h"
#include "cmInstallFilesCommand.h"
#include "cmInstallTargetsCommand.h"
#include "cmInstrumentationCommand.h"
#include "cmLinkDirectoriesCommand.h"
#include "cmListCommand.h"
#include "cmMacroCommand.h"
@@ -301,6 +302,7 @@ void GetProjectCommands(cmState* state)
state->AddBuiltinCommand("remove_definitions", cmRemoveDefinitionsCommand);
state->AddBuiltinCommand("source_group", cmSourceGroupCommand);
state->AddBuiltinCommand("cmake_file_api", cmFileAPICommand);
state->AddBuiltinCommand("cmake_instrumentation", cmInstrumentationCommand);
state->AddDisallowedCommand(
"export_library_dependencies", cmExportLibraryDependenciesCommand,
+10
View File
@@ -154,6 +154,16 @@ void cmCustomCommand::SetUsesTerminal(bool b)
this->UsesTerminal = b;
}
void cmCustomCommand::SetRole(const std::string& role)
{
this->Role = role;
}
const std::string& cmCustomCommand::GetRole() const
{
return this->Role;
}
bool cmCustomCommand::GetCommandExpandLists() const
{
return this->CommandExpandLists;
+5
View File
@@ -132,6 +132,10 @@ public:
const std::string& GetTarget() const;
void SetTarget(const std::string& target);
/** Set/Get the custom command rolee */
const std::string& GetRole() const;
void SetRole(const std::string& role);
/** Record if the custom command can be used for code generation. */
bool GetCodegen() const { return Codegen; }
void SetCodegen(bool b) { Codegen = b; }
@@ -148,6 +152,7 @@ private:
std::string WorkingDirectory;
std::string Depfile;
std::string JobPool;
std::string Role;
bool JobserverAware = false;
bool HaveComment = false;
bool EscapeAllowMakeVars = false;
+9
View File
@@ -75,6 +75,15 @@ cmExperimental::FeatureData LookupTable[] = {
{},
cmExperimental::TryCompileCondition::Never,
false },
// Instrumentation
{ "Instrumentation",
"a37d1069-1972-4901-b9c9-f194aaf2b6e0",
"CMAKE_EXPERIMENTAL_INSTRUMENTATION",
"CMake's support for collecting instrumentation data is experimental. It "
"is meant only for experimentation and feedback to CMake developers.",
{},
cmExperimental::TryCompileCondition::Never,
false },
};
static_assert(sizeof(LookupTable) / sizeof(LookupTable[0]) ==
static_cast<size_t>(cmExperimental::Feature::Sentinel),
+1
View File
@@ -23,6 +23,7 @@ public:
ImportPackageInfo,
ExportPackageInfo,
ExportBuildDatabase,
Instrumentation,
Sentinel,
};
+1
View File
@@ -2306,6 +2306,7 @@ cmGeneratorTarget::GetClassifiedFlagsForSource(cmSourceFile const* sf,
vars.CMTargetName = this->GetName().c_str();
vars.CMTargetType = cmState::GetTargetTypeName(this->GetType()).c_str();
vars.Language = lang.c_str();
auto const sfPath = this->LocalGenerator->ConvertToOutputFormat(
sf->GetFullPath(), cmOutputConverter::SHELL);
+4
View File
@@ -3119,6 +3119,7 @@ void cmGlobalGenerator::AddGlobalTarget_Install(
gti.Message = "Install the project...";
gti.UsesTerminal = true;
gti.StdPipesUTF8 = true;
gti.Role = "install";
cmCustomCommandLine singleLine;
if (this->GetPreinstallTargetName()) {
gti.Depends.emplace_back(this->GetPreinstallTargetName());
@@ -3157,6 +3158,7 @@ void cmGlobalGenerator::AddGlobalTarget_Install(
if (const char* install_local = this->GetInstallLocalTargetName()) {
gti.Name = install_local;
gti.Message = "Installing only the local directory...";
gti.Role = "install";
gti.UsesTerminal =
!this->GetCMakeInstance()->GetState()->GetGlobalPropertyAsBool(
"INSTALL_PARALLEL");
@@ -3177,6 +3179,7 @@ void cmGlobalGenerator::AddGlobalTarget_Install(
gti.Name = install_strip;
gti.Message = "Installing the project stripped...";
gti.UsesTerminal = true;
gti.Role = "install";
gti.CommandLines.clear();
cmCustomCommandLine stripCmdLine = singleLine;
@@ -3437,6 +3440,7 @@ void cmGlobalGenerator::CreateGlobalTarget(GlobalTargetInfo const& gti,
cc.SetWorkingDirectory(gti.WorkingDir.c_str());
cc.SetStdPipesUTF8(gti.StdPipesUTF8);
cc.SetUsesTerminal(gti.UsesTerminal);
cc.SetRole(gti.Role);
target.AddPostBuildCommand(std::move(cc));
if (!gti.Message.empty()) {
target.SetProperty("EchoString", gti.Message);
+1
View File
@@ -745,6 +745,7 @@ protected:
bool UsesTerminal = false;
cmTarget::PerConfig PerConfig = cmTarget::PerConfig::Yes;
bool StdPipesUTF8 = false;
std::string Role;
};
void CreateDefaultGlobalTargets(std::vector<GlobalTargetInfo>& targets);
+17 -2
View File
@@ -21,6 +21,7 @@
#include "cmCryptoHash.h"
#include "cmGeneratedFileStream.h"
#include "cmInstrumentation.h"
#include "cmJSONState.h"
#include "cmProcessOutput.h"
#include "cmStringAlgorithms.h"
@@ -102,13 +103,27 @@ std::vector<std::vector<std::string>> cmInstallScriptHandler::GetCommands()
return this->commands;
}
int cmInstallScriptHandler::Install(unsigned int j)
int cmInstallScriptHandler::Install(unsigned int j,
cmInstrumentation& instrumentation)
{
cm::uv_loop_ptr loop;
loop.init();
std::vector<InstallScript> scripts;
scripts.reserve(this->commands.size());
for (auto const& cmd : this->commands) {
std::vector<std::string> instrument_arg;
if (instrumentation.HasQuery()) {
instrument_arg = { cmSystemTools::GetCTestCommand(),
"--instrument",
"--command-type",
"install",
"--build-dir",
this->binaryDir,
"--" };
}
for (auto& cmd : this->commands) {
cmd.insert(cmd.begin(), instrument_arg.begin(), instrument_arg.end());
scripts.emplace_back(cmd);
}
std::size_t working = 0;
+3 -1
View File
@@ -12,6 +12,8 @@
#include "cmUVProcessChain.h"
#include "cmUVStream.h"
class cmInstrumentation;
class cmInstallScriptHandler
{
public:
@@ -19,7 +21,7 @@ public:
cmInstallScriptHandler(std::string, std::string, std::string,
std::vector<std::string>&);
bool IsParallel();
int Install(unsigned int j);
int Install(unsigned int j, cmInstrumentation& instrumentation);
std::vector<std::vector<std::string>> GetCommands() const;
class InstallScript
{
+493
View File
@@ -0,0 +1,493 @@
#include "cmInstrumentation.h"
#include <chrono>
#include <ctime>
#include <iomanip>
#include <memory>
#include <set>
#include <sstream>
#include <utility>
#include <cm/optional>
#include <cm3p/json/writer.h>
#include "cmsys/Directory.hxx"
#include "cmsys/FStream.hxx"
#include <cmsys/SystemInformation.hxx>
#include "cmCryptoHash.h"
#include "cmExperimental.h"
#include "cmInstrumentationQuery.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmTimestamp.h"
cmInstrumentation::cmInstrumentation(const std::string& binary_dir,
bool clear_generated)
{
const std::string uuid =
cmExperimental::DataForFeature(cmExperimental::Feature::Instrumentation)
.Uuid;
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 =
cmStrCat(configDir.value(), "/instrumentation-", uuid, "/v1");
}
this->LoadQueries();
}
void cmInstrumentation::LoadQueries()
{
if (cmSystemTools::FileExists(cmStrCat(this->timingDirv1, "/query"))) {
this->hasQuery =
this->ReadJSONQueries(cmStrCat(this->timingDirv1, "/query")) ||
this->ReadJSONQueries(cmStrCat(this->timingDirv1, "/query/generated"));
}
if (!this->userTimingDirv1.empty() &&
cmSystemTools::FileExists(cmStrCat(this->userTimingDirv1, "/query"))) {
this->hasQuery = this->hasQuery ||
this->ReadJSONQueries(cmStrCat(this->userTimingDirv1, "/query"));
}
}
cmInstrumentation::cmInstrumentation(
const std::string& 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(const std::string& directory)
{
cmsys::Directory d;
std::string json = ".json";
bool result = false;
if (d.Load(directory)) {
for (unsigned int i = 0; i < d.GetNumberOfFiles(); i++) {
std::string fpath = d.GetFilePath(i);
if (fpath.rfind(json) == (fpath.size() - json.size())) {
result = true;
this->ReadJSONQuery(fpath);
}
}
}
return result;
}
void cmInstrumentation::ReadJSONQuery(const std::string& file)
{
auto query = cmInstrumentationQuery();
query.ReadJSON(file, this->errorMsg, this->queries, this->hooks,
this->callbacks);
}
void cmInstrumentation::WriteJSONQuery()
{
Json::Value root;
root["version"] = 1;
root["queries"] = Json::arrayValue;
for (auto const& query : this->queries) {
root["queries"].append(cmInstrumentationQuery::QueryString[query]);
}
root["hooks"] = Json::arrayValue;
for (auto const& hook : this->hooks) {
root["hooks"].append(cmInstrumentationQuery::HookString[hook]);
}
root["callbacks"] = Json::arrayValue;
for (auto const& callback : this->callbacks) {
root["callbacks"].append(callback);
}
cmsys::Directory d;
int n = 0;
if (d.Load(cmStrCat(this->timingDirv1, "/query/generated"))) {
n = (int)d.GetNumberOfFiles() - 2; // Don't count '.' or '..'
}
this->WriteInstrumentationJson(root, "query/generated",
cmStrCat("query-", n, ".json"));
}
void cmInstrumentation::ClearGeneratedQueries()
{
std::string dir = cmStrCat(this->timingDirv1, "/query/generated");
if (cmSystemTools::FileIsDirectory(dir)) {
cmSystemTools::RemoveADirectory(dir);
}
}
bool cmInstrumentation::HasQuery()
{
return this->hasQuery;
}
bool cmInstrumentation::HasQuery(cmInstrumentationQuery::Query query)
{
return (this->queries.find(query) != this->queries.end());
}
int cmInstrumentation::CollectTimingData(cmInstrumentationQuery::Hook hook)
{
// Don't run collection if hook is disabled
if (hook != cmInstrumentationQuery::Hook::Manual &&
this->hooks.find(hook) == this->hooks.end()) {
return 0;
}
// Touch index file immediately to claim snippets
const std::string& directory = cmStrCat(this->timingDirv1, "/data");
std::string const& file_name =
cmStrCat("index-", ComputeSuffixTime(), ".json");
std::string index_path = cmStrCat(directory, "/", file_name);
cmSystemTools::Touch(index_path, true);
// Gather Snippets
using snippet = std::pair<std::string, std::string>;
std::vector<snippet> files;
cmsys::Directory d;
std::string last_index;
if (d.Load(directory)) {
for (unsigned int i = 0; i < d.GetNumberOfFiles(); i++) {
std::string fpath = d.GetFilePath(i);
std::string fname = d.GetFile(i);
if (fname.rfind('.', 0) == 0) {
continue;
}
if (fname == file_name) {
continue;
}
if (fname.rfind("index-", 0) == 0) {
if (last_index.empty()) {
last_index = fpath;
} else {
int compare;
cmSystemTools::FileTimeCompare(fpath, last_index, &compare);
if (compare == 1) {
last_index = fpath;
}
}
}
files.push_back(snippet(std::move(fname), std::move(fpath)));
}
}
// Build Json Object
Json::Value index(Json::objectValue);
index["snippets"] = Json::arrayValue;
index["hook"] = cmInstrumentationQuery::HookString[hook];
index["dataDir"] = directory;
index["buildDir"] = this->binaryDir;
index["version"] = 1;
if (this->HasQuery(cmInstrumentationQuery::Query::StaticSystemInformation)) {
this->InsertStaticSystemInformation(index);
}
for (auto const& file : files) {
if (last_index.empty()) {
index["snippets"].append(file.first);
} else {
int compare;
cmSystemTools::FileTimeCompare(file.second, last_index, &compare);
if (compare == 1) {
index["snippets"].append(file.first);
}
}
}
this->WriteInstrumentationJson(index, "data", file_name);
// Execute callbacks
for (auto& cb : this->callbacks) {
cmSystemTools::RunSingleCommand(cmStrCat(cb, " \"", index_path, "\""),
nullptr, nullptr, nullptr, nullptr,
cmSystemTools::OUTPUT_PASSTHROUGH);
}
// Delete files
for (auto const& f : index["snippets"]) {
cmSystemTools::RemoveFile(cmStrCat(directory, "/", f.asString()));
}
cmSystemTools::RemoveFile(index_path);
return 0;
}
void cmInstrumentation::InsertDynamicSystemInformation(
Json::Value& root, const std::string& prefix)
{
cmsys::SystemInformation info;
Json::Value data;
info.RunCPUCheck();
info.RunMemoryCheck();
if (!root.isMember("dynamicSystemInformation")) {
root["dynamicSystemInformation"] = Json::objectValue;
}
root["dynamicSystemInformation"][cmStrCat(prefix, "HostMemoryUsed")] =
(double)info.GetHostMemoryUsed();
root["dynamicSystemInformation"][cmStrCat(prefix, "CPULoadAverage")] =
info.GetLoadAverage();
}
void cmInstrumentation::GetDynamicSystemInformation(double& memory,
double& load)
{
cmsys::SystemInformation info;
Json::Value data;
info.RunCPUCheck();
info.RunMemoryCheck();
memory = (double)info.GetHostMemoryUsed();
load = info.GetLoadAverage();
}
void cmInstrumentation::InsertStaticSystemInformation(Json::Value& root)
{
cmsys::SystemInformation info;
info.RunCPUCheck();
info.RunOSCheck();
info.RunMemoryCheck();
Json::Value infoRoot;
infoRoot["familyId"] = info.GetFamilyID();
infoRoot["hostname"] = info.GetHostname();
infoRoot["is64Bits"] = info.Is64Bits();
infoRoot["modelId"] = info.GetModelID();
infoRoot["numberOfLogicalCPU"] = info.GetNumberOfLogicalCPU();
infoRoot["numberOfPhysicalCPU"] = info.GetNumberOfPhysicalCPU();
infoRoot["OSName"] = info.GetOSName();
infoRoot["OSPlatform"] = info.GetOSPlatform();
infoRoot["OSRelease"] = info.GetOSRelease();
infoRoot["OSVersion"] = info.GetOSVersion();
infoRoot["processorAPICID"] = info.GetProcessorAPICID();
infoRoot["processorCacheSize"] = info.GetProcessorCacheSize();
infoRoot["processorClockFrequency"] =
(double)info.GetProcessorClockFrequency();
infoRoot["processorName"] = info.GetExtendedProcessorName();
infoRoot["totalPhysicalMemory"] =
static_cast<Json::Value::UInt64>(info.GetTotalPhysicalMemory());
infoRoot["totalVirtualMemory"] =
static_cast<Json::Value::UInt64>(info.GetTotalVirtualMemory());
infoRoot["vendorID"] = info.GetVendorID();
infoRoot["vendorString"] = info.GetVendorString();
root["staticSystemInformation"] = infoRoot;
}
void cmInstrumentation::InsertTimingData(
Json::Value& root, std::chrono::steady_clock::time_point steadyStart,
std::chrono::system_clock::time_point systemStart)
{
uint64_t timeStart = std::chrono::duration_cast<std::chrono::milliseconds>(
systemStart.time_since_epoch())
.count();
uint64_t duration = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - steadyStart)
.count();
root["timeStart"] = static_cast<Json::Value::UInt64>(timeStart);
root["duration"] = static_cast<Json::Value::UInt64>(duration);
}
void cmInstrumentation::WriteInstrumentationJson(Json::Value& root,
const std::string& subdir,
const std::string& file_name)
{
Json::StreamWriterBuilder wbuilder;
wbuilder["indentation"] = "\t";
std::unique_ptr<Json::StreamWriter> JsonWriter =
std::unique_ptr<Json::StreamWriter>(wbuilder.newStreamWriter());
const std::string& directory = cmStrCat(this->timingDirv1, "/", subdir);
cmSystemTools::MakeDirectory(directory);
cmsys::ofstream ftmp(cmStrCat(directory, "/", file_name).c_str());
JsonWriter->write(root, &ftmp);
ftmp << "\n";
ftmp.close();
}
int cmInstrumentation::InstrumentTest(
const std::string& name, const std::string& command,
const std::vector<std::string>& args, int64_t result,
std::chrono::steady_clock::time_point steadyStart,
std::chrono::system_clock::time_point systemStart)
{
// Store command info
Json::Value root(this->preTestStats);
std::string command_str = cmStrCat(command, ' ', GetCommandStr(args));
root["version"] = 1;
root["command"] = command_str;
root["role"] = "test";
root["testName"] = name;
root["binaryDir"] = this->binaryDir;
root["result"] = static_cast<Json::Value::Int64>(result);
// Post-Command
this->InsertTimingData(root, steadyStart, systemStart);
if (this->HasQuery(
cmInstrumentationQuery::Query::DynamicSystemInformation)) {
this->InsertDynamicSystemInformation(root, "after");
}
std::string const& file_name =
cmStrCat("test-", this->ComputeSuffixHash(command_str),
this->ComputeSuffixTime(), ".json");
this->WriteInstrumentationJson(root, "data", file_name);
return 1;
}
void cmInstrumentation::GetPreTestStats()
{
if (this->HasQuery(
cmInstrumentationQuery::Query::DynamicSystemInformation)) {
this->InsertDynamicSystemInformation(this->preTestStats, "before");
}
}
int cmInstrumentation::InstrumentCommand(
std::string command_type, const std::vector<std::string>& command,
const std::function<int()>& callback,
cm::optional<std::map<std::string, std::string>> options,
cm::optional<std::map<std::string, std::string>> arrayOptions,
bool reloadQueriesAfterCommand)
{
// Always begin gathering data for configure in case cmake_instrumentation
// command creates a query
if (!this->hasQuery && !reloadQueriesAfterCommand) {
return callback();
}
// Store command info
Json::Value root(Json::objectValue);
Json::Value commandInfo(Json::objectValue);
std::string command_str = GetCommandStr(command);
root["command"] = command_str;
root["version"] = 1;
// Pre-Command
auto steady_start = std::chrono::steady_clock::now();
auto system_start = std::chrono::system_clock::now();
double preConfigureMemory = 0;
double preConfigureLoad = 0;
if (this->HasQuery(
cmInstrumentationQuery::Query::DynamicSystemInformation)) {
this->InsertDynamicSystemInformation(root, "before");
} else if (reloadQueriesAfterCommand) {
this->GetDynamicSystemInformation(preConfigureMemory, preConfigureLoad);
}
// Execute Command
int ret = callback();
root["result"] = ret;
// Exit early if configure didn't generate a query
if (reloadQueriesAfterCommand) {
this->LoadQueries();
if (!this->hasQuery) {
return ret;
}
if (this->HasQuery(
cmInstrumentationQuery::Query::DynamicSystemInformation)) {
root["dynamicSystemInformation"] = Json::objectValue;
root["dynamicSystemInformation"]["beforeHostMemoryUsed"] =
preConfigureMemory;
root["dynamicSystemInformation"]["beforeCPULoadAverage"] =
preConfigureLoad;
}
}
// Post-Command
this->InsertTimingData(root, steady_start, system_start);
if (this->HasQuery(
cmInstrumentationQuery::Query::DynamicSystemInformation)) {
this->InsertDynamicSystemInformation(root, "after");
}
// Gather additional data
if (options.has_value()) {
for (auto const& item : options.value()) {
if (item.first == "role" && !item.second.empty()) {
command_type = item.second;
} else if (!item.second.empty()) {
root[item.first] = item.second;
}
}
}
if (arrayOptions.has_value()) {
for (auto const& item : arrayOptions.value()) {
root[item.first] = Json::arrayValue;
std::stringstream ss(item.second);
std::string element;
while (getline(ss, element, ',')) {
root[item.first].append(element);
}
if (item.first == "outputs") {
root["outputSizes"] = Json::arrayValue;
for (auto const& output : root["outputs"]) {
root["outputSizes"].append(
static_cast<Json::Value::UInt64>(cmSystemTools::FileLength(
cmStrCat(this->binaryDir, "/", output.asCString()))));
}
}
}
}
root["role"] = command_type;
root["binaryDir"] = this->binaryDir;
// Write Json
std::string const& file_name =
cmStrCat(command_type, "-", this->ComputeSuffixHash(command_str),
this->ComputeSuffixTime(), ".json");
this->WriteInstrumentationJson(root, "data", file_name);
return ret;
}
std::string cmInstrumentation::GetCommandStr(
const std::vector<std::string>& args)
{
std::string command_str;
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, " ");
}
}
return command_str;
}
std::string cmInstrumentation::ComputeSuffixHash(
std::string const& command_str)
{
cmCryptoHash hasher(cmCryptoHash::AlgoSHA3_256);
std::string hash = hasher.HashString(command_str);
hash.resize(20, '0');
return hash;
}
std::string cmInstrumentation::ComputeSuffixTime()
{
std::chrono::milliseconds ms =
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch());
std::chrono::seconds s =
std::chrono::duration_cast<std::chrono::seconds>(ms);
std::time_t ts = s.count();
std::size_t tms = ms.count() % 1000;
cmTimestamp cmts;
std::ostringstream ss;
ss << cmts.CreateTimestampFromTimeT(ts, "%Y-%m-%dT%H-%M-%S", true) << '-'
<< std::setfill('0') << std::setw(4) << tms;
return ss.str();
}
+78
View File
@@ -0,0 +1,78 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file Copyright.txt or https://cmake.org/licensing for details. */
#pragma once
#include "cmConfigure.h" // IWYU pragma: keep
#include <chrono>
#include <functional>
#include <map>
#include <set>
#include <string>
#include <vector>
#include <cm/optional>
#include <cm3p/json/value.h>
#include <stdint.h>
#include "cmInstrumentationQuery.h"
class cmInstrumentation
{
public:
// Read Queries
cmInstrumentation(const std::string& binary_dir,
bool clear_generated = false);
// Create Query
cmInstrumentation(const std::string& binary_dir,
std::set<cmInstrumentationQuery::Query>& queries,
std::set<cmInstrumentationQuery::Hook>& hooks,
std::string& callback);
int InstrumentCommand(
std::string command_type, const std::vector<std::string>& command,
const std::function<int()>& callback,
cm::optional<std::map<std::string, std::string>> options = cm::nullopt,
cm::optional<std::map<std::string, std::string>> arrayOptions =
cm::nullopt,
bool reloadQueriesAfterCommand = false);
int InstrumentTest(const std::string& name, const std::string& command,
const std::vector<std::string>& args, int64_t result,
std::chrono::steady_clock::time_point steadyStart,
std::chrono::system_clock::time_point systemStart);
void GetPreTestStats();
void LoadQueries();
bool HasQuery();
bool HasQuery(cmInstrumentationQuery::Query);
bool ReadJSONQueries(const std::string& directory);
void ReadJSONQuery(const std::string& file);
void WriteJSONQuery();
int CollectTimingData(cmInstrumentationQuery::Hook hook);
std::string errorMsg;
private:
void WriteInstrumentationJson(Json::Value& index,
const std::string& directory,
const std::string& file_name);
static void InsertStaticSystemInformation(Json::Value& index);
static void GetDynamicSystemInformation(double& memory, double& load);
static void InsertDynamicSystemInformation(Json::Value& index,
const std::string& instant);
static void InsertTimingData(
Json::Value& root, std::chrono::steady_clock::time_point steadyStart,
std::chrono::system_clock::time_point systemStart);
void ClearGeneratedQueries();
bool HasQueryFile(const std::string& file);
static std::string GetCommandStr(const std::vector<std::string>& args);
static std::string ComputeSuffixHash(std::string const& command_str);
static std::string ComputeSuffixTime();
std::string binaryDir;
std::string timingDirv1;
std::string userTimingDirv1;
std::set<cmInstrumentationQuery::Query> queries;
std::set<cmInstrumentationQuery::Hook> hooks;
std::vector<std::string> callbacks;
std::vector<std::string> queryFiles;
Json::Value preTestStats;
bool hasQuery = false;
};
+149
View File
@@ -0,0 +1,149 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file Copyright.txt or https://cmake.org/licensing for details. */
#include "cmInstrumentationCommand.h"
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <functional>
#include <set>
#include <cmext/string_view>
#include "cmArgumentParser.h"
#include "cmArgumentParserTypes.h"
#include "cmExecutionStatus.h"
#include "cmExperimental.h"
#include "cmInstrumentation.h"
#include "cmInstrumentationQuery.h"
#include "cmMakefile.h"
#include "cmStringAlgorithms.h"
namespace {
bool isCharDigit(char ch)
{
return std::isdigit(static_cast<unsigned char>(ch));
}
bool validateVersion(const std::string& key, const std::string& versionString,
int& version, cmExecutionStatus& status)
{
if (!std::all_of(versionString.begin(), versionString.end(), isCharDigit)) {
status.SetError(cmStrCat("given a non-integer ", key, "."));
return false;
}
version = std::atoi(versionString.c_str());
if (version != 1) {
status.SetError(cmStrCat(
"QUERY subcommand given an unsupported ", key, " \"", versionString,
"\" (the only currently supported version is 1)."));
return false;
}
return true;
}
template <typename E>
std::function<bool(const std::string&, E&)> EnumParser(
const std::vector<std::string> toString)
{
return [toString](const std::string& value, E& out) -> bool {
for (size_t i = 0; i < toString.size(); ++i) {
if (value == toString[i]) {
out = (E)i;
return true;
}
}
return false;
};
}
}
bool cmInstrumentationCommand(std::vector<std::string> const& args,
cmExecutionStatus& status)
{
// if (status->GetMakefile().GetPropertyKeys) {
if (!cmExperimental::HasSupportEnabled(
status.GetMakefile(), cmExperimental::Feature::Instrumentation)) {
status.SetError(
"requires the experimental Instrumentation flag to be enabled");
return false;
}
if (args.empty()) {
status.SetError("must be called with arguments.");
return false;
}
struct Arguments : public ArgumentParser::ParseResult
{
ArgumentParser::NonEmpty<std::string> ApiVersion;
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;
};
static auto const parser = cmArgumentParser<Arguments>{}
.Bind("API_VERSION"_s, &Arguments::ApiVersion)
.Bind("DATA_VERSION"_s, &Arguments::DataVersion)
.Bind("QUERIES"_s, &Arguments::Queries)
.Bind("HOOKS"_s, &Arguments::Hooks)
.Bind("CALLBACK"_s, &Arguments::Callback);
std::vector<std::string> unparsedArguments;
Arguments const arguments = parser.Parse(args, &unparsedArguments);
if (arguments.MaybeReportError(status.GetMakefile())) {
return true;
}
if (!unparsedArguments.empty()) {
status.SetError("given unknown argument \"" + unparsedArguments.front() +
"\".");
return false;
}
int apiVersion;
int dataVersion;
if (!validateVersion("API_VERSION", arguments.ApiVersion, apiVersion,
status) ||
!validateVersion("DATA_VERSION", arguments.DataVersion, dataVersion,
status)) {
return false;
}
std::set<cmInstrumentationQuery::Query> queries;
auto queryParser = EnumParser<cmInstrumentationQuery::Query>(
cmInstrumentationQuery::QueryString);
for (auto const& arg : arguments.Queries) {
cmInstrumentationQuery::Query query;
if (!queryParser(arg, query)) {
status.SetError(
cmStrCat("given invalid argument to QUERIES \"", arg, "\""));
return false;
}
queries.insert(query);
}
std::set<cmInstrumentationQuery::Hook> hooks;
auto hookParser = EnumParser<cmInstrumentationQuery::Hook>(
cmInstrumentationQuery::HookString);
for (auto const& arg : arguments.Hooks) {
cmInstrumentationQuery::Hook hook;
if (!hookParser(arg, hook)) {
status.SetError(
cmStrCat("given invalid argument to HOOKS \"", arg, "\""));
return false;
}
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();
return true;
}
+13
View File
@@ -0,0 +1,13 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file Copyright.txt or https://cmake.org/licensing for details. */
#pragma once
#include "cmConfigure.h" // IWYU pragma: keep
#include <string>
#include <vector>
class cmExecutionStatus;
bool cmInstrumentationCommand(std::vector<std::string> const& args,
cmExecutionStatus& status);
+114
View File
@@ -0,0 +1,114 @@
#include "cmInstrumentationQuery.h"
#include <algorithm>
#include <ctime>
#include <functional>
#include <iostream>
#include <iterator>
#include <set>
#include <vector>
#include <cmext/string_view>
#include <cm3p/json/value.h>
#include "cmJSONHelpers.h"
#include "cmStringAlgorithms.h"
const std::vector<std::string> cmInstrumentationQuery::QueryString{
"staticSystemInformation", "dynamicSystemInformation"
};
const std::vector<std::string> cmInstrumentationQuery::HookString{
"postGenerate", "preBuild", "postBuild", "preCMakeBuild",
"postCMakeBuild", "postTest", "postInstall", "manual"
};
namespace ErrorMessages {
using ErrorGenerator =
std::function<void(const Json::Value*, cmJSONState* state)>;
ErrorGenerator ErrorGeneratorBuilder(const std::string& errorMessage)
{
return [errorMessage](const Json::Value* value, cmJSONState* state) -> void {
state->AddErrorAtValue(errorMessage, value);
};
};
static ErrorGenerator InvalidArray = ErrorGeneratorBuilder("Invalid Array");
JsonErrors::ErrorGenerator InvalidRootQueryObject(
JsonErrors::ObjectError errorType, const Json::Value::Members& extraFields)
{
return JsonErrors::INVALID_NAMED_OBJECT(
[](const Json::Value*, cmJSONState*) -> std::string {
return "root object";
})(errorType, extraFields);
}
};
using JSONHelperBuilder = cmJSONHelperBuilder;
template <typename E>
static std::function<bool(E&, const Json::Value*, cmJSONState*)> EnumHelper(
const std::vector<std::string> toString, const std::string& type)
{
return [toString, type](E& out, const Json::Value* value,
cmJSONState* state) -> bool {
for (size_t i = 0; i < toString.size(); ++i) {
if (value->asString() == toString[i]) {
out = (E)i;
return true;
}
}
state->AddErrorAtValue(
cmStrCat("Not a valid ", type, ": \"", value->asString(), "\""), value);
return false;
};
}
static auto const QueryHelper = EnumHelper<cmInstrumentationQuery::Query>(
cmInstrumentationQuery::QueryString, "query");
static auto const QueryListHelper =
JSONHelperBuilder::Vector<cmInstrumentationQuery::Query>(
ErrorMessages::InvalidArray, QueryHelper);
static auto const HookHelper = EnumHelper<cmInstrumentationQuery::Hook>(
cmInstrumentationQuery::HookString, "hook");
static auto const HookListHelper =
JSONHelperBuilder::Vector<cmInstrumentationQuery::Hook>(
ErrorMessages::InvalidArray, HookHelper);
static auto const CallbackHelper = JSONHelperBuilder::String();
static auto const CallbackListHelper = JSONHelperBuilder::Vector<std::string>(
ErrorMessages::InvalidArray, CallbackHelper);
static auto const VersionHelper = JSONHelperBuilder::Int();
using QueryRoot = cmInstrumentationQuery::QueryJSONRoot;
static auto const QueryRootHelper =
JSONHelperBuilder::Object<QueryRoot>(ErrorMessages::InvalidRootQueryObject,
false)
.Bind("version"_s, &QueryRoot::version, VersionHelper, true)
.Bind("queries"_s, &QueryRoot::queries, QueryListHelper, false)
.Bind("hooks"_s, &QueryRoot::hooks, HookListHelper, false)
.Bind("callbacks"_s, &QueryRoot::callbacks, CallbackListHelper, false);
bool cmInstrumentationQuery::ReadJSON(const std::string& filename,
std::string& errorMessage,
std::set<Query>& queries,
std::set<Hook>& hooks,
std::vector<std::string>& callbacks)
{
Json::Value root;
this->parseState = cmJSONState(filename, &root);
if (!this->parseState.errors.empty()) {
std::cerr << this->parseState.GetErrorMessage(true) << std::endl;
return false;
}
if (!QueryRootHelper(this->queryRoot, &root, &this->parseState)) {
errorMessage = this->parseState.GetErrorMessage(true);
return false;
}
std::move(this->queryRoot.queries.begin(), this->queryRoot.queries.end(),
std::inserter(queries, queries.end()));
std::move(this->queryRoot.hooks.begin(), this->queryRoot.hooks.end(),
std::inserter(hooks, hooks.end()));
std::move(this->queryRoot.callbacks.begin(), this->queryRoot.callbacks.end(),
std::back_inserter(callbacks));
return true;
}
+49
View File
@@ -0,0 +1,49 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file Copyright.txt or https://cmake.org/licensing for details. */
#pragma once
#include <set>
#include <string>
#include <vector>
#include "cmJSONState.h"
class cmInstrumentationQuery
{
public:
enum Query
{
StaticSystemInformation,
DynamicSystemInformation
};
static const std::vector<std::string> QueryString;
enum Hook
{
PostGenerate,
PreBuild,
PostBuild,
PreCMakeBuild,
PostCMakeBuild,
PostTest,
PostInstall,
Manual
};
static const std::vector<std::string> HookString;
struct QueryJSONRoot
{
std::vector<cmInstrumentationQuery::Query> queries;
std::vector<cmInstrumentationQuery::Hook> hooks;
std::vector<std::string> callbacks;
int version;
};
cmInstrumentationQuery() = default;
bool ReadJSON(const std::string& file, std::string& errorMessage,
std::set<Query>& queries, std::set<Hook>& hooks,
std::vector<std::string>& callbacks);
QueryJSONRoot queryRoot;
cmJSONState parseState;
};
+10 -5
View File
@@ -912,14 +912,19 @@ std::string cmLocalNinjaGenerator::MakeCustomLauncher(
std::string output;
const std::vector<std::string>& outputs = ccg.GetOutputs();
if (!outputs.empty()) {
output = outputs[0];
if (ccg.GetWorkingDirectory().empty()) {
output = this->MaybeRelativeToCurBinDir(output);
for (size_t i = 0; i < outputs.size(); ++i) {
output = cmStrCat(output,
this->ConvertToOutputFormat(
ccg.GetWorkingDirectory().empty()
? this->MaybeRelativeToCurBinDir(outputs[i])
: outputs[i],
cmOutputConverter::SHELL));
if (i != outputs.size() - 1) {
output = cmStrCat(output, ",");
}
output = this->ConvertToOutputFormat(output, cmOutputConverter::SHELL);
}
vars.Output = output.c_str();
vars.Role = ccg.GetCC().GetRole().c_str();
auto rulePlaceholderExpander = this->CreateRulePlaceholderExpander();
+10 -6
View File
@@ -996,15 +996,19 @@ void cmLocalUnixMakefileGenerator3::AppendCustomCommand(
cmState::GetTargetTypeName(target->GetType()).c_str();
std::string output;
const std::vector<std::string>& outputs = ccg.GetOutputs();
if (!outputs.empty()) {
output = outputs[0];
if (workingDir.empty()) {
output = this->MaybeRelativeToCurBinDir(output);
for (size_t i = 0; i < outputs.size(); ++i) {
output = cmStrCat(output,
this->ConvertToOutputFormat(
ccg.GetWorkingDirectory().empty()
? this->MaybeRelativeToCurBinDir(outputs[i])
: outputs[i],
cmOutputConverter::SHELL));
if (i != outputs.size() - 1) {
output = cmStrCat(output, ",");
}
output =
this->ConvertToOutputFormat(output, cmOutputConverter::SHELL);
}
vars.Output = output.c_str();
vars.Role = ccg.GetCC().GetRole().c_str();
launcher = val;
rulePlaceholderExpander->ExpandRuleVariables(this, launcher, vars);
+6
View File
@@ -270,6 +270,12 @@ std::string cmRulePlaceholderExpander::ExpandVariable(
return this->OutputConverter->ConvertToOutputFormat(
cmSystemTools::GetCMakeCommand(), cmOutputConverter::SHELL);
}
if (variable == "ROLE") {
if (this->ReplaceValues->Role) {
return this->ReplaceValues->Role;
}
return "";
}
auto compIt = this->Compilers.find(variable);
+1
View File
@@ -73,6 +73,7 @@ public:
const char* Fatbinary = nullptr;
const char* RegisterFile = nullptr;
const char* Launcher = nullptr;
const char* Role = nullptr;
};
// Expand rule variables in CMake of the type found in language rules
+74 -2
View File
@@ -81,6 +81,8 @@
# include "cmConfigureLog.h"
# include "cmFileAPI.h"
# include "cmGraphVizWriter.h"
# include "cmInstrumentation.h"
# include "cmInstrumentationQuery.h"
# include "cmVariableWatch.h"
#endif
@@ -929,6 +931,7 @@ enum class ListPresets
// Parse the args
void cmake::SetArgs(const std::vector<std::string>& args)
{
this->cmdArgs = args;
bool haveToolset = false;
bool havePlatform = false;
bool haveBArg = false;
@@ -2604,9 +2607,28 @@ int cmake::ActualConfigure()
// 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);
return 1;
}
std::function<int()> doConfigure = [this]() -> int {
this->GlobalGenerator->Configure();
return 0;
};
int ret = instrumentation.InstrumentCommand(
"configure", this->cmdArgs, [doConfigure]() { return doConfigure(); },
cm::nullopt, cm::nullopt, true);
if (ret != 0) {
return ret;
}
#else
this->GlobalGenerator->Configure();
#endif
auto endTime = std::chrono::steady_clock::now();
// configure result
if (this->GetWorkingMode() == cmake::NORMAL_MODE) {
std::ostringstream msg;
if (cmSystemTools::GetErrorOccurredFlag()) {
@@ -2650,6 +2672,7 @@ int cmake::ActualConfigure()
}
const auto& mf = this->GlobalGenerator->GetMakefiles()[0];
if (mf->IsOn("CTEST_USE_LAUNCHERS") &&
!this->State->GetGlobalProperty("RULE_LAUNCH_COMPILE")) {
cmSystemTools::Error(
@@ -2658,6 +2681,37 @@ int cmake::ActualConfigure()
"Did you forget to include(CTest) in the toplevel "
"CMakeLists.txt ?");
}
// Setup launchers for instrumentation
#if !defined(CMAKE_BOOTSTRAP)
instrumentation.LoadQueries();
if (instrumentation.HasQuery()) {
std::string launcher;
if (mf->IsOn("CTEST_USE_LAUNCHERS")) {
launcher =
cmStrCat("\"", cmSystemTools::GetCTestCommand(), "\" --launch ");
} else {
launcher =
cmStrCat("\"", cmSystemTools::GetCTestCommand(), "\" --instrument ");
}
std::string common_args =
cmStrCat(" --target-name <TARGET_NAME> ", "--build-dir \"",
this->State->GetBinaryDirectory(), "\" ");
this->State->SetGlobalProperty(
"RULE_LAUNCH_COMPILE",
cmStrCat(
launcher, "--command-type compile", common_args,
"--output <OBJECT> --source <SOURCE> --language <LANGUAGE> -- "));
this->State->SetGlobalProperty(
"RULE_LAUNCH_LINK",
cmStrCat(launcher, "--command-type link", common_args,
"--output <TARGET> --target-type <TARGET_TYPE> ",
"--language <LANGUAGE> -- "));
this->State->SetGlobalProperty(
"RULE_LAUNCH_CUSTOM",
cmStrCat(launcher, "--command-type custom", common_args,
"--output \"<OUTPUT>\" --role <ROLE> -- "));
}
#endif
this->State->SaveVerificationScript(this->GetHomeOutputDirectory(),
this->Messenger.get());
@@ -2945,15 +2999,29 @@ int cmake::Generate()
return -1;
}
auto startTime = std::chrono::steady_clock::now();
#if !defined(CMAKE_BOOTSTRAP)
auto profilingRAII = this->CreateProfilingEntry("project", "generate");
#endif
cmInstrumentation instrumentation(this->State->GetBinaryDirectory());
std::function<int()> doGenerate = [this]() -> int {
if (!this->GlobalGenerator->Compute()) {
return -1;
}
this->GlobalGenerator->Generate();
return 0;
};
auto startTime = std::chrono::steady_clock::now();
int ret = instrumentation.InstrumentCommand(
"generate", this->cmdArgs, [doGenerate]() { return doGenerate(); });
if (ret != 0) {
return ret;
}
#else
if (!this->GlobalGenerator->Compute()) {
return -1;
}
this->GlobalGenerator->Generate();
#endif
auto endTime = std::chrono::steady_clock::now();
{
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(endTime -
@@ -2963,6 +3031,10 @@ int cmake::Generate()
<< ms.count() / 1000.0L << "s)";
this->UpdateProgress(msg.str(), -1);
}
#if !defined(CMAKE_BOOTSTRAP)
instrumentation.CollectTimingData(
cmInstrumentationQuery::Hook::PostGenerate);
#endif
if (!this->GraphVizFile.empty()) {
std::cout << "Generate graphviz: " << this->GraphVizFile << '\n';
this->GenerateGraphViz(this->GraphVizFile);
+1
View File
@@ -754,6 +754,7 @@ protected:
void GenerateGraphViz(const std::string& fileName) const;
private:
std::vector<std::string> cmdArgs;
std::string CMakeWorkingDirectory;
ProgressCallbackType ProgressCallback;
WorkingMode CurrentWorkingMode = NORMAL_MODE;
+54 -24
View File
@@ -28,6 +28,8 @@
#include "cmDocumentationEntry.h"
#include "cmGlobalGenerator.h"
#include "cmInstallScriptHandler.h"
#include "cmInstrumentation.h"
#include "cmInstrumentationQuery.h"
#include "cmList.h"
#include "cmMakefile.h"
#include "cmMessageMetadata.h"
@@ -703,11 +705,27 @@ int do_build(int ac, char const* const* av)
cmakemainProgressCallback(msg, prog, &cm);
});
cmInstrumentation instrumentation(dir);
if (!instrumentation.errorMsg.empty()) {
cmSystemTools::Error(instrumentation.errorMsg);
return 1;
}
cmBuildOptions buildOptions(cleanFirst, false, resolveMode);
return cm.Build(jobs, std::move(dir), std::move(targets), std::move(config),
std::move(nativeOptions), buildOptions, verbose, presetName,
listPresets);
std::function<int()> doBuild = [&cm, &jobs, &dir, &targets, &config,
&nativeOptions, &buildOptions, &verbose,
&presetName, &listPresets]() {
return cm.Build(jobs, dir, std::move(targets), std::move(config),
std::move(nativeOptions), buildOptions, verbose,
presetName, listPresets);
};
instrumentation.CollectTimingData(
cmInstrumentationQuery::Hook::PreCMakeBuild);
std::vector<std::string> cmd;
cm::append(cmd, av, av + ac);
int ret = instrumentation.InstrumentCommand("cmakeBuild", cmd, doBuild);
instrumentation.CollectTimingData(
cmInstrumentationQuery::Hook::PostCMakeBuild);
return ret;
#endif
}
@@ -952,6 +970,7 @@ int do_install(int ac, char const* const* av)
args.emplace_back("-P");
cmInstrumentation instrumentation(dir);
auto handler = cmInstallScriptHandler(dir, component, config, args);
int ret = 0;
if (!jobs && handler.IsParallel()) {
@@ -966,27 +985,38 @@ int do_install(int ac, char const* const* av)
}
}
}
if (handler.IsParallel()) {
ret = handler.Install(jobs);
} else {
for (auto const& cmd : handler.GetCommands()) {
cmake cm(cmake::RoleScript, cmState::Script);
cmSystemTools::SetMessageCallback(
[&cm](const std::string& msg, const cmMessageMetadata& md) {
cmakemainMessageCallback(msg, md, &cm);
});
cm.SetProgressCallback([&cm](const std::string& msg, float prog) {
cmakemainProgressCallback(msg, prog, &cm);
});
cm.SetHomeDirectory("");
cm.SetHomeOutputDirectory("");
cm.SetDebugOutputOn(verbose);
cm.SetWorkingMode(cmake::SCRIPT_MODE);
ret = int(bool(cm.Run(cmd)));
}
}
return int(ret > 0);
std::function<int()> doInstall = [&handler, &verbose, &jobs,
&instrumentation]() -> int {
int ret_ = 0;
if (handler.IsParallel()) {
ret_ = handler.Install(jobs, instrumentation);
} else {
for (auto const& cmd : handler.GetCommands()) {
cmake cm(cmake::RoleScript, cmState::Script);
cmSystemTools::SetMessageCallback(
[&cm](const std::string& msg, const cmMessageMetadata& md) {
cmakemainMessageCallback(msg, md, &cm);
});
cm.SetProgressCallback([&cm](const std::string& msg, float prog) {
cmakemainProgressCallback(msg, prog, &cm);
});
cm.SetHomeDirectory("");
cm.SetHomeOutputDirectory("");
cm.SetDebugOutputOn(verbose);
cm.SetWorkingMode(cmake::SCRIPT_MODE);
ret_ = int(bool(cm.Run(cmd)));
}
}
return int(ret_ > 0);
};
std::vector<std::string> cmd;
cm::append(cmd, av, av + ac);
ret = instrumentation.InstrumentCommand(
"cmakeInstall", cmd, [doInstall]() { return doInstall(); });
instrumentation.CollectTimingData(cmInstrumentationQuery::Hook::PostInstall);
return ret;
#endif
}
+14 -1
View File
@@ -12,6 +12,8 @@
#include "cmConsoleBuf.h"
#include "cmDocumentation.h"
#include "cmDocumentationEntry.h"
#include "cmInstrumentation.h"
#include "cmInstrumentationQuery.h"
#include "cmSystemTools.h"
#include "CTest/cmCTestLaunch.h"
@@ -179,7 +181,18 @@ int main(int argc, char const* const* argv)
// Dispatch 'ctest --launch' mode directly.
if (argc >= 2 && strcmp(argv[1], "--launch") == 0) {
return cmCTestLaunch::Main(argc, argv);
return cmCTestLaunch::Main(argc, argv, cmCTestLaunch::Op::Normal);
}
// Dispatch 'ctest --instrument' mode directly.
if (argc >= 2 && strcmp(argv[1], "--instrument") == 0) {
return cmCTestLaunch::Main(argc, argv, cmCTestLaunch::Op::Instrument);
}
// Dispatch 'ctest --collect-instrumentation' mode directly.
if (argc == 3 && strcmp(argv[1], "--collect-instrumentation") == 0) {
return cmInstrumentation(argv[2]).CollectTimingData(
cmInstrumentationQuery::Hook::Manual);
}
if (cmSystemTools::GetLogicalWorkingDirectory().empty()) {
+3
View File
@@ -413,6 +413,9 @@ if(CMAKE_USE_SYSTEM_JSONCPP)
endif()
add_RunCMake_test(FileAPI -DPython_EXECUTABLE=${Python_EXECUTABLE}
-DCMAKE_CXX_COMPILER_ID=${CMAKE_CXX_COMPILER_ID})
if("${CMAKE_GENERATOR}" MATCHES "Unix Makefiles|Ninja")
add_RunCMake_test(Instrumentation)
endif()
add_RunCMake_test(ConfigDir)
add_RunCMake_test(FindBoost)
add_RunCMake_test(FindLua)
@@ -1,3 +1,6 @@
if (NOT EXISTS ${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/reply)
set(RunCMake_TEST_FAILED "Failed to read FileAPI query from user config directory")
endif()
if (NOT EXISTS ${RunCMake_TEST_BINARY_DIR}/.cmake/instrumentation-a37d1069-1972-4901-b9c9-f194aaf2b6e0/v1/data)
set(RunCMake_TEST_FAILED "Failed to read Instrumentation query from user config directory")
endif()
@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.30)
project(${RunCMake_TEST} NONE)
include(${RunCMake_TEST}.cmake)
@@ -0,0 +1,114 @@
cmake_minimum_required(VERSION 3.30)
include(RunCMake)
function(instrument test)
# Set Paths Variables
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"
"CHECK_SCRIPT;CONFIGURE_ARG" "" ${ARGN})
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test})
set(uuid "a37d1069-1972-4901-b9c9-f194aaf2b6e0")
set(v1 ${RunCMake_TEST_BINARY_DIR}/.cmake/instrumentation-${uuid}/v1)
set(query_dir ${CMAKE_CURRENT_LIST_DIR}/query)
# Clear previous instrumentation data
# We can't use RunCMake_TEST_NO_CLEAN 0 because we preserve queries placed in the build tree after
file(REMOVE_RECURSE ${RunCMake_TEST_BINARY_DIR})
# Set hook command
set(static_query_hook_arg 0)
if (ARGS_STATIC_QUERY)
set(static_query_hook_arg 1)
endif()
set(GET_HOOK "\\\"${CMAKE_COMMAND}\\\" -P \\\"${RunCMake_SOURCE_DIR}/hook.cmake\\\" ${static_query_hook_arg}")
# Load query JSON and cmake (with cmake_instrumentation(...)) files
set(query ${query_dir}/${test}.json.in)
set(cmake_file ${query_dir}/${test}.cmake)
if (EXISTS ${query})
file(MAKE_DIRECTORY ${v1}/query)
configure_file(${query} ${v1}/query/${test}.json)
elseif (EXISTS ${cmake_file})
list(APPEND ARGS_CONFIGURE_ARG "-DINSTRUMENT_COMMAND_FILE=${cmake_file}")
endif()
# Configure generated query files to compare CMake output
if (ARGS_COPY_QUERIES)
file(MAKE_DIRECTORY ${RunCMake_TEST_BINARY_DIR}/query)
set(generated_queries "0;1;2")
foreach(n ${generated_queries})
configure_file(
"${query_dir}/generated/query-${n}.json.in"
"${RunCMake_TEST_BINARY_DIR}/query/query-${n}.json"
)
endforeach()
endif()
# Configure Test Case
set(RunCMake_TEST_NO_CLEAN 1)
if (ARGS_NO_WARN)
list(APPEND ARGS_CONFIGURE_ARG "-Wno-dev")
endif()
set(RunCMake_TEST_SOURCE_DIR ${RunCMake_SOURCE_DIR}/project)
run_cmake_with_options(${test} ${ARGS_CONFIGURE_ARG})
# Follow-up Commands
if (ARGS_BUILD)
run_cmake_command(${test}-build ${CMAKE_COMMAND} --build . --config Debug)
endif()
if (ARGS_INSTALL)
run_cmake_command(${test}-install ${CMAKE_COMMAND} --install . --prefix install --config Debug)
endif()
if (ARGS_TEST)
run_cmake_command(${test}-test ${CMAKE_CTEST_COMMAND} . -C Debug)
endif()
if (ARGS_MANUAL_HOOK)
run_cmake_command(${test}-index ${CMAKE_CTEST_COMMAND} --collect-instrumentation .)
endif()
# Run Post-Test Checks
# Check scripts need to run after ALL run_cmake_command have finished
if (ARGS_CHECK_SCRIPT)
set(RunCMake-check-file ${ARGS_CHECK_SCRIPT})
set(RunCMake_CHECK_ONLY 1)
run_cmake(${test}-verify)
unset(RunCMake-check-file)
unset(RunCMake_CHECK_ONLY)
endif()
endfunction()
# Bad Queries
instrument(bad-query)
instrument(bad-hook)
instrument(empty)
instrument(bad-version)
# Verify Hooks Run and Index File
instrument(hooks-1 BUILD INSTALL TEST STATIC_QUERY)
instrument(hooks-2 BUILD INSTALL TEST)
instrument(hooks-no-callbacks MANUAL_HOOK)
# Check data file contents
instrument(no-query BUILD INSTALL TEST
CHECK_SCRIPT check-data-dir.cmake)
instrument(dynamic-query BUILD INSTALL TEST DYNAMIC_QUERY
CHECK_SCRIPT check-data-dir.cmake)
instrument(both-query BUILD INSTALL TEST DYNAMIC_QUERY
CHECK_SCRIPT check-data-dir.cmake)
# cmake_instrumentation command
instrument(cmake-command
COPY_QUERIES NO_WARN DYNAMIC_QUERY
CHECK_SCRIPT check-generated-queries.cmake)
instrument(cmake-command-data
COPY_QUERIES NO_WARN BUILD INSTALL TEST DYNAMIC_QUERY
CHECK_SCRIPT check-data-dir.cmake)
instrument(cmake-command-bad-api-version NO_WARN)
instrument(cmake-command-bad-data-version NO_WARN)
instrument(cmake-command-missing-version NO_WARN)
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)
@@ -0,0 +1 @@
1
@@ -0,0 +1,4 @@
^CMake Error: +
Error: @3,13: Not a valid hook: "bad hook"
"hooks": \["bad hook", "postGenerate", "preCMakeBuild", "postCMakeBuild", "postInstall"\]
\^$
@@ -0,0 +1 @@
1
@@ -0,0 +1,4 @@
^CMake Error: +
Error: @3,42: Not a valid query: "bad query"
"queries": \["staticSystemInformation", "bad query"\]
\^$
@@ -0,0 +1,115 @@
include(${CMAKE_CURRENT_LIST_DIR}/verify-snippet.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/json.cmake)
file(GLOB snippets ${v1}/data/*)
if (NOT snippets)
add_error("No snippet files generated")
endif()
set(FOUND_SNIPPETS "")
foreach(snippet ${snippets})
read_json(${snippet} contents)
# Verify snippet file is valid
verify_snippet(${snippet} ${contents})
# Append to list of collected snippet roles
if (NOT role IN_LIST FOUND_SNIPPETS)
list(APPEND FOUND_SNIPPETS ${role})
endif()
# Verify target
string(JSON target ERROR_VARIABLE noTarget GET ${contents} target)
if (NOT target MATCHES NOTFOUND)
set(targets "main;lib;customTarget;TARGET_NAME")
if (NOT ${target} IN_LIST targets)
snippet_error(${snippet} "Unexpected target: ${target}")
endif()
endif()
# Verify output
string(JSON result GET ${contents} result)
if (NOT ${result} EQUAL 0)
snippet_error(${snippet} "Compile command had non-0 result")
endif()
# Verify contents of compile-* Snippets
if (snippet MATCHES ^compile-)
string(JSON target GET ${contents} target)
string(JSON source GET ${contents} source)
string(JSON language GET ${contents} language)
if (NOT language MATCHES "C\\+\\+")
snippet_error(${snippet} "Expected C++ compile language")
endif()
if (NOT source MATCHES "${target}.cxx$")
snippet_error(${snippet} "Unexpected source file")
endif()
endif()
# Verify contents of link-* Snippets
if (snippet MATCHES ^link-)
string(JSON target GET ${contents} target)
string(JSON targetType GET ${contents} targetType)
if (target MATCHES main)
if (NOT targetType MATCHES "EXECUTABLE")
snippet_error(${snippet} "Expected EXECUTABLE, target type was ${targetType}")
endif()
endif()
if (target MATCHES lib)
if (NOT targetType MATCHES "STATIC_LIBRARY")
snippet_error(${snippet} "Expected STATIC_LIBRARY, target type was ${targetType}")
endif()
endif()
endif()
# Verify contents of custom-* Snippets
if (snippet MATCHES ^custom-)
string(JSON outputs GET ${contents} outputs)
if (NOT output1 MATCHES "output1" OR NOT output2 MATCHES "output2")
snippet_error(${snippet} "Custom command missing outputs")
endif()
endif()
# Verify contents of test-* Snippets
if (snippet MATCHES ^test-)
string(JSON testName GET ${contents} testName)
if (NOT testName EQUAL "test")
snippet_error(${snippet} "Unexpected testName: ${testName}")
endif()
endif()
endforeach()
# Verify that listed snippets match expected roles
set(EXPECTED_SNIPPETS configure generate)
if (ARGS_BUILD)
list(APPEND EXPECTED_SNIPPETS compile link custom cmakeBuild)
endif()
if (ARGS_TEST)
list(APPEND EXPECTED_SNIPPETS ctest test)
endif()
if (ARGS_INSTALL)
list(APPEND EXPECTED_SNIPPETS cmakeInstall)
if (ARGS_INSTALL_PARALLEL)
list(APPEND EXPECTED_SNIPPETS install)
endif()
endif()
foreach(role ${EXPECTED_SNIPPETS})
list(FIND FOUND_SNIPPETS ${role} found)
if (${found} EQUAL -1)
add_error("No snippet files of role \"${role}\" were found in ${v1}")
endif()
endforeach()
foreach(role ${FOUND_SNIPPETS})
list(FIND EXPECTED_SNIPPETS ${role} found)
if (${found} EQUAL -1)
add_error("Found unexpected snippet file of role \"${role}\" in ${v1}")
endif()
endforeach()
# Verify test/install artifacts
if (ARGS_INSTALL AND NOT EXISTS ${RunCMake_TEST_BINARY_DIR}/install)
add_error("ctest --instrument launcher failed to install the project")
endif()
if (ARGS_TEST AND NOT EXISTS ${RunCMake_TEST_BINARY_DIR}/Testing)
add_error("ctest --instrument launcher failed to test the project")
endif()
@@ -0,0 +1,17 @@
include(${CMAKE_CURRENT_LIST_DIR}/json.cmake)
macro(check_generated_json n)
set(expected_file ${RunCMake_TEST_BINARY_DIR}/query/query-${n}.json)
set(generated_file ${v1}/query/generated/query-${n}.json)
read_json(${expected_file} expected)
read_json(${generated_file} generated)
string(JSON equal EQUAL ${expected} ${generated})
if (NOT equal)
set(RunCMake_TEST_FAILED
"Generated JSON ${generated}\nNot equal to expected ${expected}"
)
endif()
endmacro()
foreach(n ${generated_queries})
check_generated_json(${n})
endforeach()
@@ -0,0 +1,6 @@
CMake Error at [^
]*\(cmake_instrumentation\):
cmake_instrumentation QUERY subcommand given an unsupported API_VERSION "0"
\(the only currently supported version is 1\).
Call Stack \(most recent call first\):
CMakeLists.txt:6 \(include\)
@@ -0,0 +1 @@
1
@@ -0,0 +1,5 @@
CMake Error at [^
]* \(cmake_instrumentation\):
cmake_instrumentation given unknown argument "UNKNOWN_ARG".
Call Stack \(most recent call first\):
CMakeLists.txt:6 \(include\)
@@ -0,0 +1,5 @@
CMake Error at [^
]*\(cmake_instrumentation\):
cmake_instrumentation given a non-integer DATA_VERSION.
Call Stack \(most recent call first\):
CMakeLists.txt:6 \(include\)
@@ -0,0 +1,6 @@
CMake Error at [^
]*\(cmake_instrumentation\):
cmake_instrumentation QUERY subcommand given an unsupported DATA_VERSION ""
\(the only currently supported version is 1\).
Call Stack \(most recent call first\):
CMakeLists.txt:6 \(include\)
@@ -0,0 +1,2 @@
CMake Error at CMakeLists\.txt:37 \(cmake_instrumentation\):
cmake_instrumentation given a non-integer DATA_VERSION\.
@@ -0,0 +1,3 @@
CMake Error at CMakeLists\.txt:44 \(cmake_instrumentation\):
cmake_instrumentation given an unsupported API_VERSION "0" \(the only
currently supported version is 1\)\.
@@ -0,0 +1 @@
1
@@ -0,0 +1,4 @@
^CMake Error: +
Error: @1,1: Missing required field "version" in root object
{
\^$
+70
View File
@@ -0,0 +1,70 @@
include(${CMAKE_CURRENT_LIST_DIR}/json.cmake)
# Test CALLBACK script. Prints output information and verifies index file
# Called as: cmake -P hook.cmake [CheckForStaticQuery?] [index.json]
set(index ${CMAKE_ARGV4})
if (NOT ${CMAKE_ARGV3})
set(hasStaticInfo "UNEXPECTED")
endif()
read_json(${index} contents)
string(JSON hook GET ${contents} hook)
# Output is verified by *-stdout.txt files that the HOOK is run
message(STATUS ${hook})
# Not a check-*.cmake script, this is called as an instrumentation CALLBACK
set(ERROR_MESSAGE "")
macro(add_error error)
string(APPEND ERROR_MESSAGE "${error}\n")
endmacro()
macro(has_key key json)
cmake_parse_arguments(ARG "UNEXPECTED" "" "" ${ARGN})
unset(missingKey)
string(JSON ${key} ERROR_VARIABLE missingKey GET ${json} ${key})
if (NOT ARG_UNEXPECTED AND NOT "${missingKey}" MATCHES NOTFOUND)
add_error("\nKey \"${key}\" not in index:\n${json}")
elseif(ARG_UNEXPECTED AND "${missingKey}" MATCHES NOTFOUND)
add_error("\nUnexpected key \"${key}\" in index:\n${json}")
endif()
endmacro()
has_key(version ${contents})
has_key(buildDir ${contents})
has_key(dataDir ${contents})
has_key(snippets ${contents})
if (NOT ${version} EQUAL 1)
add_error("Version must be 1, got: ${version}")
endif()
string(JSON length LENGTH ${snippets})
math(EXPR length ${length}-1)
foreach(i RANGE ${length})
string(JSON filename GET ${snippets} ${i})
if (NOT EXISTS ${dataDir}/${filename})
add_error("Listed snippet: ${dataDir}/${filename} does not exist")
endif()
endforeach()
has_key(staticSystemInformation ${contents} ${hasStaticInfo})
has_key(OSName ${staticSystemInformation} ${hasStaticInfo})
has_key(OSPlatform ${staticSystemInformation} ${hasStaticInfo})
has_key(OSRelease ${staticSystemInformation} ${hasStaticInfo})
has_key(OSVersion ${staticSystemInformation} ${hasStaticInfo})
has_key(familyId ${staticSystemInformation} ${hasStaticInfo})
has_key(hostname ${staticSystemInformation} ${hasStaticInfo})
has_key(is64Bits ${staticSystemInformation} ${hasStaticInfo})
has_key(modelId ${staticSystemInformation} ${hasStaticInfo})
has_key(numberOfLogicalCPU ${staticSystemInformation} ${hasStaticInfo})
has_key(numberOfPhysicalCPU ${staticSystemInformation} ${hasStaticInfo})
has_key(processorAPICID ${staticSystemInformation} ${hasStaticInfo})
has_key(processorCacheSize ${staticSystemInformation} ${hasStaticInfo})
has_key(processorClockFrequency ${staticSystemInformation} ${hasStaticInfo})
has_key(processorName ${staticSystemInformation} ${hasStaticInfo})
has_key(totalPhysicalMemory ${staticSystemInformation} ${hasStaticInfo})
has_key(totalVirtualMemory ${staticSystemInformation} ${hasStaticInfo})
has_key(vendorID ${staticSystemInformation} ${hasStaticInfo})
has_key(vendorString ${staticSystemInformation} ${hasStaticInfo})
if (NOT ERROR_MESSAGE MATCHES "^$")
message(FATAL_ERROR ${ERROR_MESSAGE})
endif()
@@ -0,0 +1 @@
^\-\- preCMakeBuild
@@ -0,0 +1 @@
.*\-\- postInstall$
@@ -0,0 +1 @@
.*\-\- postCMakeBuild$
@@ -0,0 +1,5 @@
.*\-\- Configuring done[^
]*
\-\- Generating done[^
]*
\-\- postGenerate.*
@@ -0,0 +1 @@
.*\-\- postTest$
@@ -0,0 +1 @@
^\-\- manual$
@@ -0,0 +1,8 @@
macro(read_json filename outvar)
file(READ ${filename} contents)
# string(JSON *) will fail if JSON file contains any forward-slash paths
string(REGEX REPLACE "[\\]([a-zA-Z0-9 ])" "/\\1" contents ${contents})
# string(JSON *) will fail if JSON file contains any escaped quotes \"
string(REPLACE "\\\"" "'" contents ${contents})
set(${outvar} ${contents})
endmacro()
@@ -0,0 +1,22 @@
cmake_minimum_required(VERSION 3.30)
project(instrumentation)
enable_testing()
if (EXISTS ${INSTRUMENT_COMMAND_FILE})
set(CMAKE_EXPERIMENTAL_INSTRUMENTATION "a37d1069-1972-4901-b9c9-f194aaf2b6e0")
include(${INSTRUMENT_COMMAND_FILE})
endif()
add_executable(main main.cxx)
add_library(lib lib.cxx)
target_link_libraries(main lib)
add_custom_command(
COMMAND ${CMAKE_COMMAND} -E true
OUTPUT output1 output2
)
add_custom_target(customTarget ALL
DEPENDS output1
)
add_test(NAME test COMMAND $<TARGET_FILE:main>)
install(TARGETS main)
set_target_properties(main PROPERTIES LABELS "label1;label2")
set_target_properties(lib PROPERTIES LABELS "label3")
@@ -0,0 +1,4 @@
int lib()
{
return 0;
}
@@ -0,0 +1 @@
int lib();
@@ -0,0 +1,5 @@
#include "lib.h"
int main()
{
return lib();
}
@@ -0,0 +1,4 @@
{
"version": 1,
"hooks": ["bad hook", "postGenerate", "preCMakeBuild", "postCMakeBuild", "postInstall"]
}
@@ -0,0 +1,4 @@
{
"version": 1,
"queries": ["staticSystemInformation", "bad query"]
}
@@ -0,0 +1,3 @@
{
"version": 0
}
@@ -0,0 +1,7 @@
{
"version": 1,
"queries": [
"staticSystemInformation",
"dynamicSystemInformation"
]
}
@@ -0,0 +1,3 @@
cmake_instrumentation(
API_VERSION 0
)
@@ -0,0 +1,5 @@
cmake_instrumentation(
API_VERSION 1
DATA_VERSION 1
UNKNOWN_ARG
)
@@ -0,0 +1,4 @@
cmake_instrumentation(
API_VERSION 1
DATA_VERSION NOT_AN_INT
)
@@ -0,0 +1,5 @@
cmake_instrumentation(
API_VERSION 1
DATA_VERSION 1
QUERIES dynamicSystemInformation
)
@@ -0,0 +1,3 @@
cmake_instrumentation(
API_VERSION 1
)
@@ -0,0 +1,6 @@
set_property(GLOBAL PROPERTY INSTALL_PARALLEL ON)
cmake_instrumentation(
API_VERSION 1
DATA_VERSION 1
QUERIES dynamicSystemInformation
)
@@ -0,0 +1,20 @@
# Query 0
cmake_instrumentation(
API_VERSION 1
DATA_VERSION 1
)
# Query 1
cmake_instrumentation(
API_VERSION 1
DATA_VERSION 1
HOOKS postGenerate
CALLBACK "\"${CMAKE_COMMAND}\" -E echo callback1"
)
# Query 2
cmake_instrumentation(
API_VERSION 1
DATA_VERSION 1
HOOKS postCMakeBuild
QUERIES staticSystemInformation dynamicSystemInformation
CALLBACK "\"${CMAKE_COMMAND}\" -E echo callback2"
)
@@ -0,0 +1,7 @@
{
"version": 1,
"queries": [
"staticSystemInformation",
"dynamicSystemInformation"
]
}
@@ -0,0 +1,2 @@
{
}
@@ -0,0 +1,6 @@
{
"callbacks" : [],
"hooks" : [],
"queries" : [],
"version": 1
}
@@ -0,0 +1,12 @@
{
"callbacks" :
[
"\"@CMAKE_COMMAND@\" -E echo callback1"
],
"hooks" :
[
"postGenerate"
],
"queries" : [],
"version" : 1
}
@@ -0,0 +1,16 @@
{
"callbacks" :
[
"\"@CMAKE_COMMAND@\" -E echo callback2"
],
"hooks" :
[
"postCMakeBuild"
],
"queries" :
[
"staticSystemInformation",
"dynamicSystemInformation"
],
"version": 1
}
@@ -0,0 +1,6 @@
{
"version": 1,
"hooks": ["preCMakeBuild", "postInstall"],
"callbacks": ["@GET_HOOK@"],
"queries": ["staticSystemInformation"]
}
@@ -0,0 +1,5 @@
{
"version": 1,
"hooks": ["postGenerate", "postCMakeBuild", "postTest"],
"callbacks": ["@GET_HOOK@"]
}
@@ -0,0 +1,4 @@
{
"version": 1,
"callbacks": ["@GET_HOOK@"]
}
@@ -0,0 +1,3 @@
{
"version": 1
}
@@ -0,0 +1,100 @@
# Performs generic (non-project specific) validation of v1 Snippet File Contents
macro(add_error error)
string(APPEND RunCMake_TEST_FAILED "${error}\n")
endmacro()
macro(snippet_error snippet error)
add_error("Error in snippet file ${snippet}:\n${error}")
endmacro()
macro(has_key snippet json key)
string(JSON data ERROR_VARIABLE missingKey GET ${json} ${key})
if (NOT ${missingKey} MATCHES NOTFOUND)
snippet_error(${snippet} "Missing ${key}")
endif()
endmacro()
macro(has_not_key snippet json key)
string(JSON data ERROR_VARIABLE missingKey GET ${json} ${key})
if (${missingKey} MATCHES NOTFOUND)
snippet_error(${snippet} "Has unexpected ${key}")
endif()
endmacro()
macro(snippet_has_fields snippet contents)
has_key(${snippet} ${contents} command)
has_key(${snippet} ${contents} role)
has_key(${snippet} ${contents} result)
if (snippet MATCHES ^link-*)
has_key(${snippet} ${contents} target)
has_key(${snippet} ${contents} outputs)
has_key(${snippet} ${contents} outputSizes)
has_key(${snippet} ${contents} targetType)
elseif (snippet MATCHES ^compile-*)
has_key(${snippet} ${contents} target)
has_key(${snippet} ${contents} outputs)
has_key(${snippet} ${contents} outputSizes)
has_key(${snippet} ${contents} source)
has_key(${snippet} ${contents} language)
elseif (snippet MATCHES ^custom-*)
has_key(${snippet} ${contents} target)
has_key(${snippet} ${contents} outputs)
has_key(${snippet} ${contents} outputSizes)
elseif (snippet MATCHES ^test-*)
has_key(${snippet} ${contents} testName)
endif()
if(ARGS_DYNAMIC_QUERY)
has_key(${snippet} ${contents} dynamicSystemInformation)
string(JSON dynamicSystemInfo ERROR_VARIABLE noInfo GET ${contents} dynamicSystemInformation)
if (noInfo MATCHES NOTFOUND)
has_key(${snippet} ${dynamicSystemInfo} beforeCPULoadAverage)
has_key(${snippet} ${dynamicSystemInfo} beforeHostMemoryUsed)
has_key(${snippet} ${dynamicSystemInfo} beforeCPULoadAverage)
has_key(${snippet} ${dynamicSystemInfo} beforeHostMemoryUsed)
endif()
else()
has_not_key(${snippet} ${contents} dynamicSystemInformation)
string(JSON dynamicSystemInfo ERROR_VARIABLE noInfo GET ${contents} dynamicSystemInformation)
if (noInfo MATCHES NOTFOUND)
has_not_key(${snippet} ${dynamicSystemInfo} beforeCPULoadAverage)
has_not_key(${snippet} ${dynamicSystemInfo} beforeHostMemoryUsed)
has_not_key(${snippet} ${dynamicSystemInfo} beforeCPULoadAverage)
has_not_key(${snippet} ${dynamicSystemInfo} beforeHostMemoryUsed)
endif()
endif()
endmacro()
macro(snippet_valid_timing contents)
string(JSON start GET ${contents} timeStart)
string(JSON duration GET ${contents} duration)
if (${start} LESS 0)
snippet_error(${snippet} "Negative time start: ${start}")
endif()
if (${duration} LESS 0)
snippet_error(${snippet} "Negative duration: ${end}")
endif()
endmacro()
macro(verify_snippet snippet contents)
snippet_has_fields(${snippet} ${contents})
snippet_valid_timing(${contents})
string(JSON version GET ${contents} version)
if (NOT ${version} EQUAL 1)
snippet_error(${snippet} "Version must be 1, got: ${version}")
endif()
string(JSON role GET ${contents} role)
get_filename_component(filename ${snippet} NAME)
if (NOT ${filename} MATCHES ^${role}-)
snippet_error(${snippet} "Role \"${role}\" doesn't match snippet filename")
endif()
string(JSON outputs ERROR_VARIABLE noOutputs GET ${contents} outputs)
if (NOT outputs MATCHES NOTFOUND)
string(JSON outputSizes ERROR_VARIABLE noOutputSizes GET ${contents} outputSizes)
list(LENGTH outputs outputsLen)
list(LENGTH outputSizes outputSizesLen)
if (outputSizes MATCHES NOTFOUND OR NOT outputsLen EQUAL outputSizesLen)
snippet_error(${snippet} "outputs and outputSizes do not match")
endif()
endif()
endmacro()