New CMake Instrumentation Feature Provides Detailed Timing of Builds

April 25, 2025
Visualization of CMake’s own test suite running. Generated using CMake Instrumentation data

Overview

CMake 4.0 introduces a new experimental feature: build process instrumentation. This feature enables detailed tracking of the entire CMake workflow—including configure time, build execution, testing, and installation—providing developers and teams with actionable insights into build performance. With fine-grained metrics such as target-specific compile times and resource usage, this system is ideal for identifying bottlenecks and areas for improvement. By enabling telemetry with this feature, projects can gain insight into build performance over time, across teams or CI systems. Instrumentation hooks allow integration with third-party tools by supporting user-defined callbacks, making it easy to extend and customize the data collection pipeline.

This blog will introduce the new feature and readers should gain an understanding of how to use CMake Instrumentation to gain insight into project performance.

Visualization of CMake’s own test suite running. Generated using CMake Instrumentation data
Visualization of CMake’s own test suite running. Generated using CMake Instrumentation data

Using CMake Instrumentation

There are three main steps necessary to make use of the Instrumentation feature.

1. Enable the Experimental Instrumentation Feature

Because this feature is still experimental, you need to set an experimental flag in the CMake project in order to enable it. (Note, this value will change with future CMake releases, see docs for the version you are using for the correct value.)

set(CMAKE_EXPERIMENTAL_INSTRUMENTATION a37d1069-1972-4901-b9c9-f194aaf2b6e0)

2. Create Instrumentation Queries

There are two types of queries a user can write to enable collection of instrumentation data.

Project queries should be defined in the project’s CMake code with the cmake_instrumentation command.

cmake_instrumentation(
    API_VERSION 1
    DATA_VERSION 1
    QUERIES dynamicSystemInformation staticSystemInformation
    HOOKS postCMakeBuild postInstall postTest
    CALLBACK ${Python_EXECUTABLE} ${CMAKE_SOURCE_DIR}/instrument.py
)

The parameters include an API_VERSION and DATA_VERSION (both are always 1 for now) to control the version protocol. A QUERIES parameter defines optional data to collect that would be omitted by default. HOOKS is a list of triggers at which the data should be collated and parsed. CALLBACK specifies a user-defined callback command to parse the data at each HOOK. If you want to control the indexing time manually, leave out the HOOK parameter and call ctest --collect-instrumentation <buildDir> manually.

User queries are queries that will apply to all of a user’s CMake projects. These take the form of JSON files placed in a user’s CMake config directory under instrumentation-a37d1069-1972-4901-b9c9-f194aaf2b6e0/v1/query. The long UUID in this path is the same as the UUID we set in Step 1. When the feature is no longer experimental, the path will be shortened to instrumentation/v1/query.

{
    "version": 1,
    "queries": ["dynamicSystemInformation", "staticSystemInformation"]
    "hooks": ["postCMakeBuild", "postInstall", "postTest"]
    "callbacks": ["/path/to/python /path/to/instrument.py"]
)

The keys correspond to those in the cmake_instrumentation command.

3. Define Callback Functions to Read the Data

CMake passes one additional argument to the callback functions provided by instrumentation queries: the path to an instrumentation index file. The index file serves as an entry point to reading the data. After every callback has been executed, CMake cleans up the data output files on its own.

Example

We’ve put together a sample project with a callback script to serve as an example for writing your own instrumentation callbacks. It also includes a small test CMake project we will use to demonstrate this feature.

An Example Callback

The included callback script: instrument.py copies the instrumentation data to an instrumentation/ subdirectory of the build tree so it can be inspected after CMake removes it. Additionally, it uses a modified version of the ninjatracing project to generate a JSON file in the Google trace format so we can visualize the timing data.

from cmaketracing import cmaketracing
import json
import os
import shutil
import sys

if name == "main":
    # The instrumentation feature will pass the path to an index file to this callback
    index = sys.argv[1]

    # Get the buildDir and dataDir from the index file
    with open(index) as f:
        data = json.load(f)
        buildDir = data["buildDir"]
        dataDir = data["dataDir"]

    # Get a unique output directory name based on the index filename
    indexName = os.path.basename(index).split(".")[0]

    # Create an output directory that CMake won't clear, to copy the files into
    outputDir = os.path.join(buildDir, "instrumentation")
    indexDir = os.path.join(outputDir, indexName)
    for d in [outputDir, indexDir]:
        if not os.path.exists(d):
            os.makedirs(d)

    # Copy all the instrumentation data into the newly created output directory
    for snippet in data["snippets"]:
        shutil.copyfile(
            os.path.join(dataDir, snippet),
            os.path.join(indexDir, snippet)
        )
    shutil.copyfile(
        index,
        os.path.join(indexDir, os.path.basename(index))
    )

    # Generate a trace.json file to visualize timing data
    cmaketracing(index, os.path.join(indexDir, "trace.json"))

Collecting Instrumentation Data on our Example Project

After cloning the sample project, configure and build the included CMake project. From the example subdirectory, run:

cmake -Bbuild && cmake --build build -j 2

Because we enabled postCMakeBuild under our HOOKS in the CMakeLists.txt, indexing will occur after the cmake --build build command. Let’s take a look at the collected data under build/instrumentation/ in the build tree to see what it collected.

There should be one subdirectory named index-[timestamp] Whenever our instrument.py runs, it creates a new subdirectory here. The contents should look something like this:

cmakeBuild-ec74be4c6427cdd530652025-04-08T14-16-55-0748.json
compile-61687789b71f29134e3f2025-04-08T14-16-55-0669.json
compile-91446c32823d06f19c152025-04-08T14-16-54-0854.json
configure-d3db67f661db6d445eb42025-04-08T14-16-54-0003.json
custom-2a01d48fead2dab11a0a2025-04-08T14-16-54-0205.json
custom-55ddf43960e0561327032025-04-08T14-16-54-0552.json
custom-aa313e64139cc7624d1e2025-04-08T14-16-54-0278.json
custom-faabfa38a66e7d844c6c2025-04-08T14-16-54-0704.json
generate-d3db67f661db6d445eb42025-04-08T14-16-54-0023.json
index-2025-04-08T14-16-55-0748.json
link-02cbdbe042219f1edb3f2025-04-08T14-16-54-0890.json
link-ac2596d7733682d61b6e2025-04-08T14-16-55-0726.json
trace.json

The cmakeBuild snippet file will contain information about the overall cmake --build build command we ran. The compile, link, and custom snippet files contain information about each compile, link and custom command respectively. The configure and generate files correspond to, as you’d expect, the configure and generate steps that occurred as part of our cmake -Bbuild

Taking a closer look at compile-1db007c87f3dfbfed80f2025-04-08T10-23-07-0160.json, we can see what information it collected while compiling million.cxx

{
    "command" : "\"/usr/bin/c++\" … \"CMakeFiles/million.dir/million.cxx.o\" … \"-o\" \"CMakeFiles/million.dir/million.cxx.o\" …",
    "config" : "Debug",
    "duration" : 1303,
    "dynamicSystemInformation" :
    {
        "afterCPULoadAverage" : 0.44,
        "afterHostMemoryUsed" : 8907248.0,
        "beforeCPULoadAverage" : 0.44,
        "beforeHostMemoryUsed" : 8909804.0
    },
    "language" : "C++",
    "outputSizes" :
    [
        3040
    ],
    "outputs" :
    [
        "CMakeFiles/million.dir/million.cxx.o"
    ],
    "result" : 0,
    "role" : "compile",
    "source" : "…/test-instrumentation/million.cxx",
    "target" : "million",
    "timeStart" : 1744104808532,
    "version" : 1,
    "workingDir" : "…/test-instrumentation/build"
}

The index file in our output directory is a copy of the index file passed to instrument.py. Taking a look inside shows the general format of these files that user-defined callbacks should be expected to handle. If we enabled staticSystemInformation under QUERIES, that information would show up here as well.

{
    "buildDir" : "…/test-instrumentation/build",
    "dataDir" : "…/test-instrumentation/.cmake/instrumentation-a37d1069-1972-4901-b9c9-f194aaf2b6e0/v1/data",
    "hook" : "postCMakeBuild",
    "snippets" :
    [
        "link-6c1a64f6d803aea3e8a62025-04-08T10-23-07-0230.json",
        "compile-376bfdec8c017be9e6ff2025-04-08T10-23-07-0212.json",
        "custom-61211194d40f3836a9952025-04-08T10-23-05-0636.json",
        "custom-fd5c533371d8e7ea82dd2025-04-08T10-23-04-0608.json",
        "custom-bbcd9ed7685d2f869c9b2025-04-08T10-23-02-0580.json",
        "configure-128096b0a40c3622dad22025-04-08T10-23-01-0500.json",
        "cmakeBuild-9ec5874c81597e60f29a2025-04-08T10-23-07-0280.json",
        "link-93f30c0e06b1cd8cd6892025-04-08T10-23-07-0259.json",
        "compile-1db007c87f3dfbfed80f2025-04-08T10-23-07-0160.json",
        "generate-128096b0a40c3622dad22025-04-08T10-23-01-0540.json",
        "link-a298274557737f1963ff2025-04-08T10-23-07-0222.json",
        "custom-9b79dfaa10abdc2d82d42025-04-08T10-23-02-0584.json",
        "link-5f6174ff49dc20bb0bc12025-04-08T10-23-07-0236.json"
    ]
}

Finally, the generated trace.json can be opened in https://ui.perfetto.dev/ or about://tracing in a Chrome browser to see a visualization of our build:

Finally, the generated trace.json can be opened in https://ui.perfetto.dev/ or about://tracing in a Chrome browser to see a visualization of our build:

If you ran the configure and build steps individually, you might see a gap between the the configure and cmakeBuild blocks. We can also see easily in the visualization that our compilation of million.cxx and thousand.cxx depend on the completion of all the custom commands.

In addition to metrics about configure and build time, we can collect information about test and install. Certain snippet files contain special information. For example, the snippet data for each test will include the name of the test that ran. You can see what data is collected for which snippets in the documentation. Let’s run ctest --test-dir build -j 2 and look at the resulting trace.json file:

You can see what data is collected for which snippets in the documentation. Let’s run ctest -j 2 and look at the resulting trace.json file:

The sample project has CMake’s parallel install enabled (available since 3.31), we can visualize that too (cmake --install build -j 2):

The sample project has CMake’s parallel install enabled (available since 3.31), we can visualize that too (cmake --install -j 2):

Running The Example Callback on Another Project

We can copy the cmake_instrumentation call to another CMake project, replacing ${CMAKE_SOURCE_DIR}/instrument.py with the full path to the file. Be sure to add a find_package(Python) as well or change ${Python_EXECUTABLE}

Leave a Reply