instrumentation: Add compileTrace option

Adds support for optionally saving and reporting trace files generated by
Clang's `-ftime-trace` option.
This commit is contained in:
Martin Duffy
2026-06-05 11:06:09 -04:00
committed by Brad King
parent 059170f19c
commit 0480222ff4
14 changed files with 268 additions and 59 deletions

View File

@@ -76,7 +76,7 @@ equivalent JSON query file.
API_VERSION 1
DATA_VERSION 1.0
HOOKS postGenerate preCMakeBuild postCMakeBuild
OPTIONS staticSystemInformation dynamicSystemInformation trace
OPTIONS staticSystemInformation dynamicSystemInformation compileTrace trace
CALLBACK ${CMAKE_COMMAND} -P /path/to/handle_data.cmake
CALLBACK ${CMAKE_COMMAND} -P /path/to/handle_data_2.cmake
CUSTOM_CONTENT myString STRING string
@@ -92,7 +92,7 @@ equivalent JSON query file.
"postGenerate", "preCMakeBuild", "postCMakeBuild"
],
"options": [
"staticSystemInformation", "dynamicSystemInformation", "trace"
"staticSystemInformation", "dynamicSystemInformation", "compileTrace", "trace"
],
"callbacks": [
"/path/to/cmake -P /path/to/handle_data.cmake",

View File

@@ -211,6 +211,11 @@ subdirectories:
trace file remains even after `Indexing`_ occurs and all `Callbacks`_ are
executed, until the next time `Indexing`_ occurs.
``data/compile-trace/``
A subset of the collected data, containing any trace files generated by
the compiler, when the ``compileTrace`` `option <v1 Query Files_>`_ is
enabled.
``cdash/``
Holds temporary files used internally to generate XML content to be submitted
to CDash.
@@ -308,6 +313,16 @@ key is required, but all other fields are optional.
Only available as of data version ``1.1``.
``compileTrace``
.. versionadded:: 4.4
Enables collection of JSON files generated by Clang's
``-ftime-trace`` option. When such files are created for an
instrumented compile command, they are copied into the instrumentation
``data`` directory and referenced from the compile snippet.
Only available as of data version ``1.1``.
``cdashSubmit``
Enables including instrumentation data in CDash. This is
equivalent to having the :envvar:`CTEST_USE_INSTRUMENTATION` environment
@@ -508,6 +523,16 @@ Snippet files have a filename with the syntax
or ``Debug``. Only included when ``role`` is one of: ``compile``, ``link``,
``custom``, ``install``, ``test``.
``traceFile``
.. versionadded:: 4.4
A path, relative to the instrumentation ``data`` directory, referencing a
copied JSON file generated by Clang's ``-ftime-trace`` option. Only
included when the ``compileTrace`` `option
<v1 Query Files_>`_ is enabled and ``role`` is
``compile``. If no JSON file was produced by the compile command, this value
is ``null``.
``dynamicSystemInformation``
Specifies the dynamic information collected about the host machine
CMake is being run from. Data is collected for every snippet file
@@ -558,6 +583,7 @@ Example:
"language" : "C++",
"outputs" : [ "CMakeFiles/main.dir/main.cxx.o" ],
"outputSizes" : [ 0 ],
"traceFile" : "compile-trace/main.cxx-<hash>.json",
"source" : "<src>/main.cxx",
"config" : "Debug",
"dynamicSystemInformation" :

View File

@@ -65,6 +65,7 @@
"enum": [
"staticSystemInformation",
"dynamicSystemInformation",
"compileTrace",
"cdashSubmit",
"cdashVerbose",
"trace",

View File

@@ -12,6 +12,7 @@
#include <cm/memory>
#include <cm/optional>
#include <cm/string_view>
#include <cmext/algorithm>
#include <cm3p/json/reader.h>
@@ -434,6 +435,7 @@ int cmInstrumentation::CollectTimingData(cmInstrumentationQuery::Hook hook)
// Delete old content and trace files
this->RemoveOldFiles("content");
this->RemoveOldFiles("compile-trace");
this->RemoveOldFiles("trace");
this->indexLock.Release();
@@ -651,6 +653,49 @@ int cmInstrumentation::InstrumentCommand(
if (!command_str.empty()) {
root["command"] = command_str;
}
root["role"] = command_type;
root["workingDir"] = cmSystemTools::GetLogicalWorkingDirectory();
if (data.has_value()) {
for (auto const& item : data.value()) {
if (item.first == "role" && !item.second.empty()) {
command_type = item.second;
root["role"] = command_type;
} else if (item.first == "showOnly") {
root[item.first] = item.second == "1" ? true : false;
} else if (!item.second.empty()) {
root[item.first] = item.second;
}
}
}
if (arrayData.has_value()) {
for (auto const& item : arrayData.value()) {
root[item.first] = Json::arrayValue;
std::stringstream ss(item.second);
std::string element;
while (getline(ss, element, ',')) {
root[item.first].append(element);
}
}
}
// Create empty config entry if config not found
if (!root.isMember("config") &&
(command_type == "compile" || command_type == "link" ||
command_type == "custom" || command_type == "install")) {
root["config"] = "";
}
// Check existing compile trace json to check for modifications
std::string compileTraceFile;
if (this->HasOption(cmInstrumentationQuery::Option::CompileTrace) &&
command_type == "compile") {
compileTraceFile = this->GetCompileTraceFile(
command, root["outputs"], root["workingDir"].asString());
}
long int oldCompileTraceTimestamp = !compileTraceFile.empty() &&
cmSystemTools::FileExists(compileTraceFile, true)
? cmSystemTools::ModifiedTime(compileTraceFile)
: -1;
// Pre-Command
auto steady_start = std::chrono::steady_clock::now();
@@ -699,52 +744,18 @@ int cmInstrumentation::InstrumentCommand(
this->InsertDynamicSystemInformation(root, "after");
}
// Gather additional data
if (data.has_value()) {
for (auto const& item : data.value()) {
if (item.first == "role" && !item.second.empty()) {
command_type = item.second;
} else if (item.first == "showOnly") {
root[item.first] = item.second == "1" ? true : false;
} else if (!item.second.empty()) {
root[item.first] = item.second;
}
}
}
// See SpawnBuildDaemon(); this data is currently meaningless for build.
root["result"] = command_type == "build" ? Json::nullValue : ret;
// Create empty config entry if config not found
if (!root.isMember("config") &&
(command_type == "compile" || command_type == "link" ||
command_type == "custom" || command_type == "install")) {
root["config"] = "";
}
if (arrayData.has_value()) {
for (auto const& item : arrayData.value()) {
if (item.first == "targetLabels" && command_type != "link") {
continue;
}
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()))));
}
}
// Output Sizes
if (root.isMember("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["workingDir"] = cmSystemTools::GetLogicalWorkingDirectory();
auto addCMakeContent = [this](Json::Value& root_) -> void {
std::string contentFile =
@@ -758,14 +769,25 @@ int cmInstrumentation::InstrumentCommand(
addCMakeContent(root);
}
// Write Json
cmsys::SystemInformation& info = this->GetSystemInformation();
// Compute file name properties
std::chrono::system_clock::time_point endTime =
system_start + std::chrono::milliseconds(root["duration"].asUInt64());
std::string const& file_name = cmStrCat(
command_type, '-',
this->ComputeSuffixHash(cmStrCat(command_str, info.GetProcessId())), '-',
this->ComputeSuffixTime(endTime), ".json");
cmsys::SystemInformation& info = this->GetSystemInformation();
std::string const commandHash =
this->ComputeSuffixHash(cmStrCat(command_str, info.GetProcessId()));
std::string const suffixTime = this->ComputeSuffixTime(endTime);
// Compile Trace
if (this->HasOption(cmInstrumentationQuery::Option::CompileTrace) &&
command_type == "compile") {
this->CollectCompileTraceFile(root, compileTraceFile,
oldCompileTraceTimestamp, commandHash,
suffixTime);
}
// Write JSON
std::string const& file_name =
cmStrCat(command_type, '-', commandHash, '-', suffixTime, ".json");
// Don't write configure snippet until generate time
if (command_type == "configure") {
@@ -1048,14 +1070,72 @@ void cmInstrumentation::PrepareDataForCDash(std::string const& data_dir,
}
std::string dst = cmStrCat(dst_dir, '/', snippet_str);
cmsys::Status copied = cmSystemTools::CopyFileAlways(snippet_path, dst);
if (!copied) {
if (!cmSystemTools::CopyFileAlways(snippet_path, dst)) {
error_msg = cmStrCat("Failed to copy ", snippet_path, " to ", dst);
cmSystemTools::Error(error_msg);
}
}
}
std::string cmInstrumentation::GetCompileTraceFile(
std::vector<std::string> const& command, Json::Value const& outputs,
std::string const& workingDir)
{
cm::string_view const prefix = "-ftime-trace=";
std::string traceFile;
for (auto it = command.rbegin(); it != command.rend(); ++it) {
std::string const& arg = *it;
if (cmHasPrefix(arg, prefix)) {
traceFile = arg.substr(prefix.size());
}
}
if (traceFile.empty() && !outputs.empty()) {
std::string outputPath = outputs[0].asString();
cm::string_view ext =
cmSystemTools::GetFilenameLastExtensionView(outputPath);
if (!outputPath.empty() && !ext.empty()) {
traceFile = cmStrCat(
outputPath.substr(0, outputPath.size() - ext.size()), ".json");
}
}
if (!cmSystemTools::FileIsFullPath(traceFile)) {
traceFile = cmStrCat(workingDir, '/', traceFile);
}
return traceFile;
}
void cmInstrumentation::CollectCompileTraceFile(Json::Value& root,
std::string traceFile,
long int oldTimestamp,
std::string const& commandHash,
std::string const& suffixTime)
{
if (traceFile.empty()) {
root["traceFile"] = Json::nullValue;
return;
}
if (!cmSystemTools::FileExists(traceFile, true) ||
cmSystemTools::ModifiedTime(traceFile) == oldTimestamp) {
root["traceFile"] = Json::nullValue;
return;
}
cm::string_view ext = cmSystemTools::GetFilenameLastExtensionView(traceFile);
std::string candidateName = cmSystemTools::GetFilenameName(traceFile);
std::string copiedName =
cmStrCat(candidateName.substr(0, candidateName.size() - ext.size()), '-',
commandHash, '-', suffixTime, ext);
std::string const copiedFile = cmStrCat("compile-trace/", copiedName);
std::string const destination = cmStrCat(this->dataDir, '/', copiedFile);
cmSystemTools::MakeDirectory(cmSystemTools::GetFilenamePath(destination));
if (!cmSystemTools::CopyFileAlways(traceFile, destination)) {
cmSystemTools::Error(cmStrCat("Failed to copy compile trace file ",
traceFile, " to ", destination));
return;
}
root["traceFile"] = copiedFile;
}
void cmInstrumentation::WriteTraceFile(Json::Value const& index,
std::string const& trace_name)
{

View File

@@ -119,6 +119,14 @@ private:
static bool IsInstrumentableTargetType(cmStateEnums::TargetType type);
void PrepareDataForCDash(std::string const& data_dir,
std::string const& index_path);
static std::string GetCompileTraceFile(
std::vector<std::string> const& command, Json::Value const& outputs,
std::string const& workingDir);
void CollectCompileTraceFile(Json::Value& root, std::string traceFile,
long int oldTimestamp,
std::string const& commandHash,
std::string const& suffixTime);
void RemoveCompileTraceFile(Json::Value const& snippetData);
void RemoveOldFiles(std::string const& dataSubdir);
void WriteTraceFile(Json::Value const& index, std::string const& trace_name);
Json::Value BuildTraceEvent(std::vector<uint64_t>& workers,

View File

@@ -18,6 +18,7 @@ std::vector<std::string> const cmInstrumentationQuery::OptionString{
"staticSystemInformation",
"dynamicSystemInformation",
"captureOutput",
"compileTrace",
"cdashSubmit",
"cdashVerbose",
"trace"

View File

@@ -17,6 +17,7 @@ public:
StaticSystemInformation,
DynamicSystemInformation,
CaptureOutput,
CompileTrace,
CDashSubmit,
CDashVerbose,
Trace

View File

@@ -440,6 +440,9 @@ add_RunCMake_test(FileAPI -DPython_EXECUTABLE=${Python_EXECUTABLE}
-DCMake_TEST_JSON_SCHEMA=${CMake_TEST_JSON_SCHEMA})
if(CMAKE_GENERATOR MATCHES "Make|Ninja|FASTBuild")
add_RunCMake_test(Instrumentation -DPython_EXECUTABLE=${Python_EXECUTABLE}
-DCMAKE_C_COMPILER_ID=${CMAKE_C_COMPILER_ID}
-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
-DCMAKE_C_COMPILER_VERSION=${CMAKE_C_COMPILER_VERSION}
-DCMake_TEST_JSON_SCHEMA=${CMake_TEST_JSON_SCHEMA})
endif()
add_RunCMake_test(ConfigDir)

View File

@@ -17,6 +17,8 @@ function(instrument test)
"STATIC_QUERY"
"DYNAMIC_QUERY"
"CAPTURE_OUTPUT_QUERY"
"COMPILE_TRACE_QUERY"
"COMPILE_TRACE_QUERY_NULL"
"TRACE_QUERY"
"MANUAL_HOOK"
"PRESERVE_DATA"
@@ -25,7 +27,7 @@ function(instrument test)
"FAIL"
"BAD_QUERY"
)
cmake_parse_arguments(ARGS "${OPTIONS}" "CHECK_SCRIPT;CONFIGURE_ARG" "" ${ARGN})
cmake_parse_arguments(ARGS "${OPTIONS}" "CHECK_SCRIPT" "CONFIGURE_ARGS" ${ARGN})
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test})
set(v1 ${RunCMake_TEST_BINARY_DIR}/.cmake/instrumentation/v1)
set(v1 ${v1} PARENT_SCOPE)
@@ -56,6 +58,8 @@ function(instrument test)
if (ARGS_TRACE_QUERY)
set(trace_query_hook_arg 1)
endif()
set(ARGS_COMPILE_TRACE_QUERY ${ARGS_COMPILE_TRACE_QUERY} PARENT_SCOPE)
set(ARGS_COMPILE_TRACE_QUERY_NULL ${ARGS_COMPILE_TRACE_QUERY_NULL} PARENT_SCOPE)
set(GET_HOOK
"\\\"${CMAKE_COMMAND}\\\""
"-DSTATIC_QUERY=${static_query_hook_arg}"
@@ -86,7 +90,7 @@ function(instrument test)
set(cmake_file "${query_dir}/default.cmake")
endif()
endif()
list(APPEND ARGS_CONFIGURE_ARG "-DINSTRUMENT_COMMAND_FILE=${cmake_file}")
list(APPEND ARGS_CONFIGURE_ARGS "-DINSTRUMENT_COMMAND_FILE=${cmake_file}")
endif()
set(copy_loc ${RunCMake_TEST_BINARY_DIR}/query)
@@ -112,10 +116,10 @@ function(instrument test)
# Configure Test Case
set(RunCMake_TEST_NO_CLEAN 1)
if (ARGS_FAIL)
list(APPEND ARGS_CONFIGURE_ARG "-DFAIL=ON")
list(APPEND ARGS_CONFIGURE_ARGS "-DFAIL=ON")
endif()
if (ARGS_DISABLE_TEST)
list(APPEND ARGS_CONFIGURE_ARG "-DDISABLE_TEST=ON")
list(APPEND ARGS_CONFIGURE_ARGS "-DDISABLE_TEST=ON")
endif()
set(RunCMake_TEST_SOURCE_DIR ${RunCMake_SOURCE_DIR}/project)
if(NOT RunCMake_GENERATOR_IS_MULTI_CONFIG)
@@ -141,7 +145,7 @@ function(instrument test)
unset(RunCMake_QUIET_ERROR)
endif()
if (NOT ARGS_NO_CONFIGURE)
run_cmake_with_options(${test} ${ARGS_CONFIGURE_ARG} ${maybe_CMAKE_BUILD_TYPE})
run_cmake_with_options(${test} ${ARGS_CONFIGURE_ARGS} ${maybe_CMAKE_BUILD_TYPE})
endif()
# Follow-up Commands
@@ -275,7 +279,7 @@ instrument(cmake-command-parallel-install
BUILD INSTALL TEST INSTALL_PARALLEL DYNAMIC_QUERY
CHECK_SCRIPT check-data-dir.cmake)
instrument(cmake-command-initial-cache
CONFIGURE_ARG "-C ${RunCMake_BINARY_DIR}/initial.cmake"
CONFIGURE_ARGS "-C ${RunCMake_BINARY_DIR}/initial.cmake"
)
instrument(cmake-command-resets-generated
COPY_QUERIES_GENERATED
@@ -303,11 +307,11 @@ instrument(cmake-command-workflow
# Test CUSTOM_CONTENT
instrument(cmake-command-custom-content
BUILD
CONFIGURE_ARG "-DN=1"
CONFIGURE_ARGS "-DN=1"
)
instrument(cmake-command-custom-content
BUILD PRESERVE_DATA
CONFIGURE_ARG "-DN=2"
CONFIGURE_ARGS "-DN=2"
CHECK_SCRIPT check-custom-content.cmake
)
set(indexDir ${v1}/data/index)
@@ -352,6 +356,53 @@ instrument(cmake-command-capture-output
CHECK_SCRIPT check-data-dir.cmake
)
# Test compile trace collection
if (CMAKE_C_COMPILER_ID STREQUAL "AppleClang")
if (CMAKE_C_COMPILER_VERSION VERSION_LESS 11.1)
set(Skip_COMPILE_TRACE_QUERY_Case 1)
elseif (CMAKE_C_COMPILER_VERSION VERSION_LESS 15)
set(Skip_COMPILE_TRACE_QUERY_ARG_Case 1)
endif()
elseif (CMAKE_C_COMPILER_ID STREQUAL "Clang")
if (CMAKE_C_COMPILER_VERSION VERSION_LESS 9)
set(Skip_COMPILE_TRACE_QUERY_Case 1)
elseif (CMAKE_C_COMPILER_VERSION VERSION_LESS 16)
set(Skip_COMPILE_TRACE_QUERY_ARG_Case 1)
endif()
else()
set(Skip_COMPILE_TRACE_QUERY_Case 1)
endif()
if("$ENV{CMAKE_OSX_ARCHITECTURES}" MATCHES "[;$]")
# `-ftime-trace` with multiple `-arch` puts the trace file in TMPDIR.
set(Skip_COMPILE_TRACE_QUERY_Case 1)
endif()
if(RunCMake_GENERATOR MATCHES "NMake")
# `-ftime-trace=` is hidden by `@<< ... <<` response file syntax.
set(Skip_COMPILE_TRACE_QUERY_ARG_Case 1)
endif()
if (NOT Skip_COMPILE_TRACE_QUERY_Case)
instrument(cmake-command-compile-trace
BUILD COMPILE_TRACE_QUERY
CONFIGURE_ARGS
"-DINSTRUMENT_COMPILE_TRACE=DEFAULT"
"-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}"
CHECK_SCRIPT check-data-dir.cmake
)
instrument(cmake-command-compile-trace-null
BUILD COMPILE_TRACE_QUERY_NULL
CHECK_SCRIPT check-data-dir.cmake
)
if (NOT Skip_COMPILE_TRACE_QUERY_ARG_Case)
instrument(cmake-command-compile-trace-explicit
BUILD COMPILE_TRACE_QUERY
CONFIGURE_ARGS
"-DINSTRUMENT_COMPILE_TRACE=EXPLICIT"
"-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}"
CHECK_SCRIPT check-data-dir.cmake
)
endif()
endif()
# Test make/ninja hooks
if(RunCMake_GENERATOR STREQUAL "FASTBuild")
# FIXME(#27184): This does not work for FASTBuild.

View File

@@ -73,6 +73,20 @@ foreach(snippet IN LISTS snippets)
)
endif()
endif()
if (ARGS_COMPILE_TRACE_QUERY)
json_has_key("${snippet}" "${contents}" traceFile)
string(JSON jsonFile GET "${contents}" traceFile)
if (NOT EXISTS "${v1}/data/${jsonFile}" OR IS_DIRECTORY "${v1}/data/${jsonFile}")
json_error("${snippet}" "Missing copied compile JSON file: ${jsonFile}")
else()
read_json("${v1}/data/${jsonFile}" trace_contents)
json_has_key("${v1}/data/${jsonFile}" "${trace_contents}" traceEvents)
endif()
elseif (ARGS_COMPILE_TRACE_QUERY_NULL)
json_assert_key("${snippet}" "${contents}" traceFile null)
else()
json_missing_key("${snippet}" "${contents}" traceFile)
endif()
endif()
# Verify contents of link-* Snippets

View File

@@ -8,6 +8,15 @@ endif()
add_executable(main main.c)
add_library(lib lib.c)
target_link_libraries(main lib)
if (INSTRUMENT_COMPILE_TRACE STREQUAL "DEFAULT")
target_compile_options(main PRIVATE "-ftime-trace")
target_compile_options(lib PRIVATE "-ftime-trace")
elseif (INSTRUMENT_COMPILE_TRACE STREQUAL "EXPLICIT")
target_compile_options(main PRIVATE "-ftime-trace=main.json")
target_compile_options(lib PRIVATE "-ftime-trace=lib.json")
endif()
add_custom_command(TARGET main POST_BUILD
COMMAND ${CMAKE_COMMAND} -E true
)

View File

@@ -0,0 +1,5 @@
cmake_instrumentation(
API_VERSION 1
DATA_VERSION 1
OPTIONS compileTrace
)

View File

@@ -0,0 +1,5 @@
cmake_instrumentation(
API_VERSION 1
DATA_VERSION 1
OPTIONS compileTrace
)

View File

@@ -0,0 +1,5 @@
cmake_instrumentation(
API_VERSION 1
DATA_VERSION 1
OPTIONS compileTrace
)