Diagnose and fix slow base venv build times caused by unnecessary recompilation of native extensions (CMake, Cython, Rust) across test environment builds. Use when CI base venv builds are slow, when ext_cache isn't saving time, or when investigating warm build regressions.
Read Debugging Build Performance for cache architecture, timing variables, and source locations. Use an isolated checkout for cold-build experiments; clearing compiled artifacts in the user's working tree can disrupt unrelated work.
scripts/run-tests --list <changed-files>. For a CI wheel, use the
job's build command, container, interpreter, and flags; see .gitlab/scripts/build-wheel-helpers.sh._DD_DEBUG_EXT=1 and _DD_DEBUG_EXT_FILE=<output-path> in the build process's environment
to capture timings. For a local editable install, rebuild with scripts/run-tests -r --venv <hash>.
The default container runner does not forward these variables from the host. For example,
temporarily set them in testrunner.environment in docker-compose.base.yml in the isolated
checkout, using an output path under /home/bits/project so it is visible on the host.
Remove those diagnostic settings after measuring; they are not part of the fix.scripts/ext_cache.py restore/save behavior and setup.py skip conditions. The Python
used for cache hashing must match the build interpreter because extension suffixes differ.
To test ext_cache itself, save a completed build, clear only the generated outputs under
investigation in the isolated checkout, restore the cache, then rebuild with matching settings.
run-tests does not perform that cache save/restore cycle; a warm in-tree rebuild alone
does not verify cache restoration.Report measured phase times and the evidence for invalidation. Avoid universal wall-clock or per-extension thresholds: hardware, dependency downloads, and build configuration affect timing.
Run these commands from the isolated checkout's repository root in the build's environment.
For local container tests, enter it with scripts/ddtest; for wheel experiments, use the wheel
build's container or environment. Set build_python to the interpreter used by that build, not the host
Python. ext_cache.py invokes setup.py with its own interpreter; it needs the build dependencies
even when the build used an isolated environment. This Linux bootstrap follows
.gitlab/scripts/build-wheel-helpers.sh; align dependency versions and build flags with the run
being investigated (requirements are in pyproject.toml). In the Bash example below,
cache_output_args=() selects editable outputs; use cache_output_args=(--build-lib) for wheel
outputs. Keep that choice consistent for hashing, saving, restoring, and the subsequent rebuild.
build_python=/path/to/selected/build/python
cache_output_args=()
cache_python() {
uv run --no-project --python "$build_python" \
--with setuptools --with cython --with 'setuptools-rust<2' \
--with 'cmake>=3.24.2,<3.28' --with 'patchelf>=0.17.0.0' python "$@"
}
cache_hashes() {
cache_python - "$@" <<'PY'
import sys
from scripts import ext_cache
ext_cache.INPLACE = "--build-lib" not in sys.argv[1:]
extensions, dependencies = ext_cache.invoke_ext_hashes()
for entry in sorted(extensions):
print("#EXTHASH:", entry)
for name, digest, path in sorted(dependencies):
print("#SHAREDEPINFO:", (name, digest, str(path)))
PY
}
mkdir -p .cache/ext-cache-probe
cache_hashes "${cache_output_args[@]}"
cache_python scripts/ext_cache.py --root .cache/ext-cache-probe "${cache_output_args[@]}" saveSave only after a successful build. Inspect the reported targets and saved files, then remove only the generated outputs being tested in this isolated checkout. Do not delete source inputs or the cache. Restore from the same helper shell:
cache_python scripts/ext_cache.py --root .cache/ext-cache-probe "${cache_output_args[@]}" restoreRepeat the build that produced the saved artifacts:
scripts/run-tests -r --venv <hash> from the host for container tests.uv build --wheel; run-tests in CI
installs a prebuilt wheel and does not rebuild its extensions.Check the restore messages, resulting artifacts, skip/compile messages, and phase timings.
Use --no-shared-deps only when intentionally excluding shared C++ dependency trees from the
experiment. Keep invoke_ext_hashes() in the helper: it clears generated .eggs before each
setup invocation so repeated probes can run reliably. Saving prunes old cache generations;
the isolated checkout and probe cache keep that work scoped.
With unchanged sources and settings, compare repeated hashes using the same helper. Preserve returned records and stderr, but compare just hash records. The helper captures setup stdout internally; collect rebuild logs separately for skip/compile evidence.
(
set -e -o pipefail
for cache_pass in 1 2; do
cache_hashes "${cache_output_args[@]}" > ".cache/ext-cache-probe/hash-$cache_pass.log" 2>&1
awk '/^#(EXTHASH|SHAREDEPINFO):/' ".cache/ext-cache-probe/hash-$cache_pass.log" |
LC_ALL=C sort > ".cache/ext-cache-probe/hash-$cache_pass.txt"
test -s ".cache/ext-cache-probe/hash-$cache_pass.txt"
done
diff -u .cache/ext-cache-probe/hash-1.txt .cache/ext-cache-probe/hash-2.txt
)If records change, inspect ExtensionHashes.run and CMakeExtension.get_sources in setup.py
for generated files entering the source set or changing configuration. A stable hash with no
cached output may mean the artifact was never saved or was pruned; it does not prove a hash bug.
For an unexpected rebuild, replace the example paths below with one candidate output and its
actual input files from the relevant setup.py skip check. Non-CMake checks substitute a .pyx
sibling for each source when present and include every ddtrace/**/*.pxd. CMake checks use
get_sources() (including extra source directories) plus dependency artifacts. Check both
in-place and build-directory output candidates; missing CMake dependencies can force a build.
cache_python - path/to/output.so path/to/input.pyx path/to/declaration.pxd <<'PY'
from pathlib import Path
import sys
output = Path(sys.argv[1])
if not output.is_file():
raise SystemExit(f"Missing output: {output}")
for source in sorted({Path(name) for name in sys.argv[2:]}):
if not source.is_file():
print(f"Missing input: {source}")
elif source.stat().st_mtime > output.stat().st_mtime:
print(f"Newer input: {source}")
PYNo reported offenders only rules out those inputs for that output. Also inspect forced builds,
DD_CMAKE_INCREMENTAL_BUILD, and the actual compile command before concluding caching failed.
DataDog/dd-trace-py
4adf35e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.