Skip to content
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ CLAUDE.md @DataDog/apm-common-components-core
.gitlab-ci.yml @DataDog/apm-common-components-core
.gitlab/benchmarks.yml @DataDog/apm-common-components-core
.gitlab/fuzz.yml @DataDog/chaos-platform
.gitlab/impacted-crates.yml @DataDog/apm-common-components-core
benchmark/ @DataDog/apm-common-components-core
bin_tests/ @DataDog/libdatadog-profiling
build-common/ @DataDog/apm-common-components-core
Expand Down
1 change: 1 addition & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ variables:
description: "downstream jobs are triggered on this branch"

include:
- local: .gitlab/impacted-crates.yml
- local: .gitlab/benchmarks.yml
- local: .gitlab/fuzz.yml

Expand Down
115 changes: 103 additions & 12 deletions .gitlab/benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,60 @@ variables:
# The Dockerfile to this image is located at:
# https://github.com/DataDog/benchmarking-platform/tree/libdatadog/benchmarks

# Shared rules for the benchmark jobs: skip merge-queue and release/hotfix branches (whether they
# arrive as a branch pipeline or an external PR), and run the full suite on main. Release/hotfix are
# version bumps + merge-backs of code already benchmarked on its originating PR and on main, so
# re-running here would add nothing.
.benchmark_run_rules: &benchmark_run_rules
- if: '$CI_COMMIT_BRANCH =~ /^mq-working-branch-/'
when: never
- if: '$CI_COMMIT_BRANCH =~ /^(release|hotfix)\//'
when: never
- if: '$CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME =~ /^(release|hotfix)\//'
when: never
- if: '$CI_COMMIT_BRANCH == "main"'
interruptible: false
- interruptible: true

# The benchmark suite is sharded across parallel jobs to reduce wall-clock time. Each shard runs
# both candidate and baseline for its assigned crates on the same runner (so the comparison stays
# noise-controlled), then benchmarks_combine merges the shards' results into a single PR comment.
benchmarks:
parallel: 2
tags: ["runner:apm-k8s-tweaked-metal"]
needs: []
needs:
# Not created on main (which runs the full suite) -- hence optional.
- job: compute_impacted_crates
artifacts: true
optional: true
image:
name: $BASE_CI_IMAGE
rules:
# Don't run on merge-queue working branches
- if: '$CI_COMMIT_BRANCH =~ /^mq-working-branch-/'
when: never
- if: $CI_COMMIT_BRANCH == "main"
interruptible: false
- interruptible: true
rules: *benchmark_run_rules
timeout: 80m
script:
# Decide which crates THIS shard benchmarks. Runs first, while $CI_PROJECT_DIR is the libdatadog
# checkout (so ./benchmark/... and the compute_impacted_crates artifacts are available). The
# helper prints SKIP (nothing to do), FULL (whole workspace), or a space-separated crate list;
# BENCH_PACKAGES is consumed by benchmark/run_benchmarks_ci.sh.
- |
set -euo pipefail
DECISION="$(./benchmark/select_bench_packages.sh "${CI_NODE_INDEX:-1}" "${CI_NODE_TOTAL:-1}")"
case "$DECISION" in
SKIP)
echo "Shard ${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}: nothing to benchmark."
# Still emit an (empty) artifact dir so benchmarks_combine's needs:artifacts is satisfied.
mkdir -p "${CI_PROJECT_DIR}/reports-${CI_NODE_INDEX:-1}"
exit 0
;;
FULL)
echo "Shard ${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}: full-workspace benchmark run."
export BENCH_PACKAGES=""
;;
*)
export BENCH_PACKAGES="$DECISION"
echo "Shard ${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1} crates: $BENCH_PACKAGES"
;;
esac
- export ARTIFACTS_DIR="$(pwd)/reports" && (mkdir "${ARTIFACTS_DIR}" || :)
- git clone --branch libdatadog/benchmarks https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.ddbuild.io/DataDog/benchmarking-platform /platform && cd /platform
- ./steps/capture-hardware-software-info.sh
Expand Down Expand Up @@ -47,11 +87,22 @@ benchmarks:
- ./steps/analyze-results.sh
- "./steps/upload-results-to-s3.sh || :"
- "./steps/upload-results-to-benchmarking-api.sh || :"
- "./steps/post-pr-comment.sh || :"
# Fail loudly if the run produced no candidate results (e.g. a step exited 0 without actually
# benchmarking); a legitimate skip exits earlier and never reaches here.
- |
set -euo pipefail
shopt -s nullglob
candidate_results=("${ARTIFACTS_DIR}"/candidate/*/benchmark-candidate.converted.json)
if (( ${#candidate_results[@]} == 0 )); then
echo "ERROR: benchmark run produced no candidate results in ${ARTIFACTS_DIR}" >&2
exit 1
fi
# Hand this shard's reports to benchmarks_combine, which posts the single PR comment.
- mv "${ARTIFACTS_DIR}" "${CI_PROJECT_DIR}/reports-${CI_NODE_INDEX:-1}"
artifacts:
name: "reports"
name: "reports-${CI_NODE_INDEX}"
paths:
- reports/
- reports-*/
expire_in: 3 months
variables:
UPSTREAM_PROJECT_ID: $CI_PROJECT_ID # The ID of the current project. This ID is unique across all projects on the GitLab instance.
Expand All @@ -61,4 +112,44 @@ benchmarks:
UPSTREAM_REPO_URL: "https://github.com/DataDog/libdatadog" # The HTTP URL to the project's repository.

KUBERNETES_SERVICE_ACCOUNT_OVERWRITE: libdatadog
FF_USE_LEGACY_KUBERNETES_EXECUTION_STRATEGY: "true"

# Merges the parallel benchmark shards and posts a single PR comment. Each shard already uploaded its
# own results to S3 / the Benchmarking API with correct per-run metadata; this job only assembles the
# combined comment (pr-commenter uses --on-duplicate=replace, so exactly one job may comment).
benchmarks_combine:
tags: ["arch:amd64"]
needs:
- job: benchmarks
artifacts: true
optional: true
image:
name: $BASE_CI_IMAGE
rules: *benchmark_run_rules
script:
- |
if ! ls -d "${CI_PROJECT_DIR}"/reports-*/ >/dev/null 2>&1; then
echo "No shard reports produced -> nothing to combine."
exit 0
fi
- git clone --branch libdatadog/benchmarks https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.ddbuild.io/DataDog/benchmarking-platform /platform
- |
# Assemble the files post-pr-comment.sh reads by concatenating each shard's markdown.
export ARTIFACTS_DIR="${CI_PROJECT_DIR}/reports"
mkdir -p "${ARTIFACTS_DIR}/candidate" "${ARTIFACTS_DIR}/baseline"
cat "${CI_PROJECT_DIR}"/reports-*/comparison-baseline-vs-candidate.md > "${ARTIFACTS_DIR}/comparison-baseline-vs-candidate.md" 2>/dev/null || :
cat "${CI_PROJECT_DIR}"/reports-*/candidate/analysis-candidate.md > "${ARTIFACTS_DIR}/candidate/analysis-candidate.md" 2>/dev/null || :
cat "${CI_PROJECT_DIR}"/reports-*/baseline/analysis-baseline.md > "${ARTIFACTS_DIR}/baseline/analysis-baseline.md" 2>/dev/null || :
(cd /platform && ./steps/post-pr-comment.sh) || :
artifacts:
name: "reports-combined"
paths:
- reports/
expire_in: 3 months
variables:
UPSTREAM_PROJECT_ID: $CI_PROJECT_ID
UPSTREAM_PROJECT_NAME: $CI_PROJECT_NAME # libdatadog
UPSTREAM_BRANCH: $CI_COMMIT_REF_NAME
UPSTREAM_COMMIT_SHA: $CI_COMMIT_SHA
UPSTREAM_REPO_URL: "https://github.com/DataDog/libdatadog"

KUBERNETES_SERVICE_ACCOUNT_OVERWRITE: libdatadog
68 changes: 68 additions & 0 deletions .gitlab/impacted-crates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
# SPDX-License-Identifier: Apache-2.0

# Computes the set of crates impacted by a PR (the changed crates plus their transitive
# dependents) and publishes it for downstream test-stage jobs to consume.
#
# It reuses the crates-reporter tool that already backs GitHub CI (.github/actions/). That
# tool writes its results to the file named by $GITHUB_OUTPUT, so we point $GITHUB_OUTPUT at
# a temp file and parse it here -- no changes to the Rust code are required.
#
# Only the benchmarks job consumes this today; other test jobs can depend on it later.

variables:
BASE_CI_IMAGE: registry.ddbuild.io/ci/benchmarking-platform:libdatadog-benchmarks

compute_impacted_crates:
stage: test
tags: ["arch:amd64"]
image:
name: $BASE_CI_IMAGE
needs: []
rules:
# Nothing to compute where benchmarks won't consume it: main runs the full suite, and
# release/hotfix/merge-queue/scheduled pipelines don't run benchmarks at all.
- if: '$CI_COMMIT_BRANCH == "main"'
when: never
- if: '$CI_COMMIT_BRANCH =~ /^(release|hotfix)\//'
when: never
- if: '$CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME =~ /^(release|hotfix)\//'
when: never
- if: '$CI_COMMIT_BRANCH =~ /^mq-working-branch-/'
when: never
- if: '$CI_PIPELINE_SOURCE == "schedule"'
when: never
- when: on_success
variables:
# Full history so the three-dot merge-base diff against origin/main resolves.
GIT_DEPTH: 0
# A failure leaves the outputs absent; downstream jobs then fall back to running everything.
allow_failure: true
script:
# Always emit impacted-crates.env and changed_files.txt, even if crate detection fails -- that
# way the benchmarks job always receives its needed artifacts and falls back to the full suite
# (IMPACTED_STATUS=skipped) instead of erroring on a missing archive.
- |
export GITHUB_OUTPUT="$(mktemp)"
git fetch --no-tags origin main || :
if (cd .github/actions && cargo build --release -p crates-reporter) \
&& ./.github/actions/target/release/crates-reporter main; then
:
else
echo "status=skipped" >> "$GITHUB_OUTPUT"
fi
# Surface the crates-reporter outputs as dotenv variables for downstream jobs.
AFFECTED_CRATES=$(sed -n 's/^affected_crates=//p' "$GITHUB_OUTPUT" | tail -n1)
IMPACTED_STATUS=$(sed -n 's/^status=//p' "$GITHUB_OUTPUT" | tail -n1)
: "${AFFECTED_CRATES:=[]}"
: "${IMPACTED_STATUS:=skipped}"
printf 'AFFECTED_CRATES=%s\nIMPACTED_STATUS=%s\n' "$AFFECTED_CRATES" "$IMPACTED_STATUS" | tee impacted-crates.env
# Record the changed files so downstream jobs can make path-based decisions.
git diff --name-only origin/main...HEAD > changed_files.txt || : > changed_files.txt
artifacts:
when: always
reports:
dotenv: impacted-crates.env
paths:
- changed_files.txt
expire_in: 1 week
56 changes: 54 additions & 2 deletions benchmark/run_benchmarks_ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,61 @@ OUTPUT_DIR="${1:-}"

pushd "${PROJECT_DIR}" > /dev/null

# Run benchmarks
# The single source of truth for which crate-specific features each crate's benchmarks need.
# When scoping a run we must pass only the features for the selected crates (cargo errors on
# --features for a crate that isn't part of the selection).
bench_features_for_crate() {
case "$1" in
libdd-crashtracker) echo "libdd-crashtracker/benchmarking" ;;
libdd-sampling) echo "libdd-sampling/v04_span libdd-sampling/bench-internals" ;;
libdd-trace-utils) echo "libdd-trace-utils/bench-internals" ;;
*) echo "" ;;
esac
}

# Run benchmarks.
message "Running benchmarks"
cargo bench --workspace --features libdd-crashtracker/benchmarking,libdd-sampling/v04_span,libdd-sampling/bench-internals,libdd-trace-utils/bench-internals -- --warm-up-time 1 --measurement-time 5 --sample-size=200

# Crate metadata for THIS checkout. The candidate and baseline checkouts can have different members
# (e.g. a PR that adds a new benchmarked crate), so BENCH_PACKAGES is resolved against whichever
# checkout we're running in.
metadata="$(cargo metadata --no-deps --format-version 1)"

# BENCH_PACKAGES (optional, space-separated crate names) scopes the run to specific crates -- set by
# the GitLab benchmarks job so a PR only benchmarks the crates it impacts. When empty (e.g. a local
# run) default to every crate that declares a benchmark target, which is equivalent to --workspace.
if [[ -z "${BENCH_PACKAGES:-}" ]]; then
BENCH_PACKAGES="$(jq -r '.packages[] | select(any(.targets[]?; .kind[]? == "bench")) | .name' <<< "$metadata" | tr '\n' ' ')"
fi

# Build the package and feature arguments from a single code path so the feature set always comes
# from bench_features_for_crate (no separate hardcoded list to drift out of sync). Skip any crate not
# present in this checkout, so `cargo bench -p <crate>` can't fail on the baseline for a crate the PR
# only just added (the baseline simply has no counterpart to compare against, which is correct).
members="$(jq -r '.packages[].name' <<< "$metadata")"
package_args=()
features=()
for crate in ${BENCH_PACKAGES}; do
if ! grep -qxF "${crate}" <<< "$members"; then
message "Skipping '${crate}': not a member of this checkout"
continue
fi
package_args+=(-p "${crate}")
for feature in $(bench_features_for_crate "${crate}"); do
features+=("${feature}")
done
done

if (( ${#package_args[@]} == 0 )); then
message "No benchmarkable crates present in this checkout; nothing to run."
else
feature_args=()
if (( ${#features[@]} > 0 )); then
feature_args=(--features "$(IFS=,; echo "${features[*]}")")
fi
message "Benchmarking crates:${package_args[*]}"
cargo bench "${package_args[@]}" "${feature_args[@]}" -- --warm-up-time 1 --measurement-time 5 --sample-size=200
fi
message "Finished running benchmarks"

# Copy the benchmark results to the output directory
Expand Down
Loading