Skip to content

Commit a82e9b1

Browse files
authored
ci: run benchmark configs in parallel (#15364)
This change parallelizes the microbenchmark jobs to make more efficient use of CI time and compute resources
1 parent 09f2961 commit a82e9b1

File tree

5 files changed

+141
-49
lines changed

5 files changed

+141
-49
lines changed

.gitlab/benchmarks/bp-runner.microbenchmarks.fail-on-breach.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -726,19 +726,19 @@ experiments:
726726
- name: iastpropagation-no-propagation
727727
thresholds:
728728
- execution_time < 0.06 ms
729-
- max_rss_usage < 40.50 MB
729+
- max_rss_usage < 42.00 MB
730730
- name: iastpropagation-propagation_enabled
731731
thresholds:
732732
- execution_time < 0.19 ms
733-
- max_rss_usage < 40.00 MB
733+
- max_rss_usage < 42.00 MB
734734
- name: iastpropagation-propagation_enabled_100
735735
thresholds:
736736
- execution_time < 2.30 ms
737-
- max_rss_usage < 40.00 MB
737+
- max_rss_usage < 42.00 MB
738738
- name: iastpropagation-propagation_enabled_1000
739739
thresholds:
740740
- execution_time < 34.55 ms
741-
- max_rss_usage < 40.00 MB
741+
- max_rss_usage < 42.00 MB
742742

743743
# otelsdkspan
744744
- name: otelsdkspan-add-event

.gitlab/benchmarks/microbenchmarks.yml

Lines changed: 30 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ variables:
1010
PACKAGE_IMAGE: registry.ddbuild.io/images/mirror/pypa/manylinux2014_x86_64:2025-04-12-5990e2d
1111
GITHUB_CLI_IMAGE: registry.ddbuild.io/images/dd-octo-sts-ci-base:2025.06-1
1212
BENCHMARKING_BRANCH: dd-trace-py
13-
BENCHMARKING_COMMIT_SHA: e7bbac96e1ae9bfb5f8906dcdf103b08f5ca0805
13+
BENCHMARKING_COMMIT_SHA: 32681a9f805f4d62cf6bd7d205ddeb83ab72288d
1414

1515
.benchmarks:
1616
stage: test
@@ -24,8 +24,6 @@ variables:
2424
timeout: 30m
2525
dependencies: [ "baseline:build", "candidate" ]
2626
script: |
27-
export REPORTS_DIR="$(pwd)/reports/" && (mkdir "${REPORTS_DIR}" || :)
28-
2927
if [[ -n "$CI_JOB_TOKEN" ]];
3028
then
3129
git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.ddbuild.io/DataDog/".insteadOf "https://github.com/DataDog/"
@@ -34,18 +32,28 @@ variables:
3432
(cd /platform && git reset --hard "${BENCHMARKING_COMMIT_SHA}")
3533
export PATH="$PATH:/platform/steps"
3634
37-
capture-hardware-software-info.sh
35+
for SCENARIO in $(echo "$SCENARIOS" | tr -s '[:space:]' ' ');
36+
do
37+
export REPORTS_DIR="$(pwd)/reports/${SCENARIO}/" && (mkdir -p "${REPORTS_DIR}" || :)
3838
39-
if [[ $SCENARIO =~ ^flask_* || $SCENARIO =~ ^django_* ]];
40-
then
41-
BP_SCENARIO=$SCENARIO bp-runner "${CI_PROJECT_DIR:-.}/.gitlab/benchmarks/bp-runner.yml" --debug -t
42-
else
43-
run-benchmarks.sh
44-
fi
39+
capture-hardware-software-info.sh
40+
41+
if [[ $SCENARIO =~ ^flask_* || $SCENARIO =~ ^django_* ]];
42+
then
43+
BP_SCENARIO=$SCENARIO bp-runner "${CI_PROJECT_DIR:-.}/.gitlab/benchmarks/bp-runner.yml" --debug -t
44+
else
45+
run-benchmarks.sh
46+
fi
4547
46-
analyze-results.sh
48+
# Join all config results into a single results.json
49+
.gitlab/benchmarks/steps/combine-results.sh "/artifacts/${CI_JOB_ID}-${SCENARIO}/candidate/"
50+
.gitlab/benchmarks/steps/combine-results.sh "/artifacts/${CI_JOB_ID}-${SCENARIO}/baseline/"
4751
48-
upload-results-to-s3.sh || :
52+
analyze-results.sh
53+
upload-results-to-s3.sh || :
54+
# Copy converted JSON reports to common location
55+
cp $REPORTS_DIR/*.converted.json $(pwd)/reports/
56+
done
4957
5058
# We have to move artifacts to ${CI_PROJECT_DIR} if we want to attach as GitLab artifact
5159
cp -R /artifacts ${CI_PROJECT_DIR}/
@@ -146,40 +154,24 @@ candidate:
146154
microbenchmarks:
147155
extends: .benchmarks
148156
parallel:
157+
# DEV: The organization into these groups is mostly arbitrary, based on observed runtimes and
158+
# trying to keep total runtime per job <10 minutes
149159
matrix:
150-
- SCENARIO:
151-
- "span"
152-
- "tracer"
153-
- "sampling_rule_matches"
154-
- "set_http_meta"
155-
- "django_simple"
156-
- "flask_simple"
157-
- "flask_sqli"
158-
- "core_api"
159-
- "otel_span"
160-
- "otel_sdk_span"
161-
- "appsec_iast_aspects"
162-
- "appsec_iast_aspects_ospath"
163-
- "appsec_iast_aspects_re_module"
164-
- "appsec_iast_aspects_split"
160+
- CPUS_PER_RUN: "1"
161+
SCENARIOS:
162+
- "span tracer core_api set_http_meta telemetry_add_metric otel_span otel_sdk_span recursive_computation sampling_rule_matches"
163+
- "http_propagation_extract http_propagation_inject rate_limiter appsec_iast_aspects appsec_iast_aspects_ospath appsec_iast_aspects_re_module appsec_iast_aspects_split appsec_iast_propagation"
164+
- "packages_package_for_root_module_mapping packages_update_imported_dependencies"
165+
- CPUS_PER_RUN: "2"
166+
SCENARIOS:
167+
- "django_simple flask_simple flask_sqli errortracking_django_simple errortracking_flask_sqli"
165168
# Flaky timeouts on starting up
166169
# - "appsec_iast_django_startup"
167-
# TOOD: Re-enable when this issue is resolved:
168-
- "appsec_iast_propagation"
169-
- "errortracking_django_simple"
170170
# They take a long time to run and frequently time out
171171
# TODO: Make benchmarks faster, or run less frequently, or as macrobenchmarks
172172
# - "appsec_iast_django_startup"
173-
- "errortracking_flask_sqli"
174173
# Flaky. Timeout errors
175174
# - "encoder"
176-
- "http_propagation_extract"
177-
- "http_propagation_inject"
178-
- "rate_limiter"
179-
- "packages_package_for_root_module_mapping"
180-
- "packages_update_imported_dependencies"
181-
- "recursive_computation"
182-
- "telemetry_add_metric"
183175
# They take a long time to run, and now need the agent running
184176
# TODO: Make benchmarks faster, or run less frequently, or as macrobenchmarks
185177
# - "startup"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env bash
2+
set -exo pipefail
3+
4+
ARTIFACTS_DIR="${1}"
5+
6+
# Combine all the individual results into a single results fule.
7+
# We need:
8+
# - to merge all the benchmarks into a single list
9+
# - to keep only one copy of the metadata, removing fields that are per-benchmark specific
10+
# - add benchmark specific metadata into each benchmark entry
11+
jq -s '
12+
map(
13+
. as $file
14+
| .benchmarks |= map(
15+
.metadata = ($file.metadata | { name, loops, cpu_affinity, cpu_config, cpu_freq } )
16+
)
17+
| {
18+
benchmarks: .benchmarks,
19+
leftover_meta: (.metadata | del(.name, .loops, .cpu_affinity, .cpu_config, .cpu_freq))
20+
}
21+
)
22+
|
23+
{
24+
benchmarks: (map(.benchmarks) | add),
25+
metadata: (first | .leftover_meta)
26+
}
27+
' $ARTIFACTS_DIR/results.*.json > "${ARTIFACTS_DIR}/results.json"

benchmarks/base/run.py

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
import json
44
import os
5+
import queue
56
import subprocess
67
import sys
8+
import threading
9+
from typing import Any
10+
from typing import Optional
711

812
import yaml
913

@@ -16,13 +20,39 @@ def read_config(path):
1620
return yaml.load(fp, Loader=yaml.FullLoader)
1721

1822

19-
def run(scenario_py, cname, cvars, output_dir):
23+
def cpu_affinity_to_cpu_groups(cpu_affinity: str, cpus_per_run: int) -> list[list[int]]:
24+
# CPU_AFFINITY is a comma-separated list of CPU IDs and ranges
25+
# 6-11
26+
# 6-11,14,15
27+
# 6-11,13-15,16,18,20-21
28+
cpu_ids: list[int] = []
29+
for part in cpu_affinity.split(","):
30+
if "-" in part:
31+
start, end = part.split("-")
32+
cpu_ids.extend(range(int(start), int(end) + 1))
33+
else:
34+
cpu_ids.append(int(part))
35+
36+
if len(cpu_ids) % cpus_per_run != 0:
37+
raise ValueError(f"CPU count {len(cpu_ids)} not divisible by CPUS_PER_RUN={cpus_per_run}")
38+
cpu_groups = [cpu_ids[i : i + cpus_per_run] for i in range(0, len(cpu_ids), cpus_per_run)]
39+
return cpu_groups
40+
41+
42+
def run(scenario_py: str, cname: str, cvars: dict[str, Any], output_dir: str, cpus: Optional[list[int]] = None):
43+
cmd: list[str] = []
44+
45+
if cpus:
46+
# Use taskset to set CPU affinity
47+
cpu_list_str = ",".join(str(cpu) for cpu in cpus)
48+
cmd += ["taskset", "-c", cpu_list_str]
49+
2050
if SHOULD_PROFILE:
2151
# viztracer won't create the missing directory itself
2252
viztracer_output_dir = os.path.join(output_dir, "viztracer")
2353
os.makedirs(viztracer_output_dir, exist_ok=True)
2454

25-
cmd = [
55+
cmd += [
2656
"viztracer",
2757
"--minimize_memory",
2858
"--min_duration",
@@ -33,14 +63,14 @@ def run(scenario_py, cname, cvars, output_dir):
3363
os.path.join(output_dir, "viztracer", "{}.json".format(cname)),
3464
]
3565
else:
36-
cmd = ["python"]
66+
cmd += ["python"]
3767

3868
cmd += [
3969
scenario_py,
4070
# necessary to copy PYTHONPATH for venvs
4171
"--copy-env",
42-
"--append",
43-
os.path.join(output_dir, "results.json"),
72+
"--output",
73+
os.path.join(output_dir, f"results.{cname}.json"),
4474
"--name",
4575
cname,
4676
]
@@ -72,5 +102,45 @@ def run(scenario_py, cname, cvars, output_dir):
72102
config = {k: v for k, v in config.items() if k in allowed_configs}
73103
print("Filtering to configs: {}".format(", ".join(sorted(config.keys()))))
74104

105+
CPU_AFFINITY = os.environ.get("CPU_AFFINITY")
106+
107+
# No CPU affinity specified, run sequentially
108+
if not CPU_AFFINITY:
109+
for cname, cvars in config.items():
110+
run("scenario.py", cname, cvars, output_dir)
111+
sys.exit(0)
112+
113+
CPUS_PER_RUN = int(os.environ.get("CPUS_PER_RUN", "1"))
114+
cpu_groups = cpu_affinity_to_cpu_groups(CPU_AFFINITY, CPUS_PER_RUN)
115+
116+
print(f"Running with CPU affinity: {CPU_AFFINITY}")
117+
print(f"CPUs per run: {CPUS_PER_RUN}")
118+
print(f"CPU groups: {list(cpu_groups)}")
119+
120+
job_queue = queue.Queue()
121+
cpu_queue = queue.Queue()
122+
123+
def worker(cpu_queue: queue.Queue, job_queue: queue.Queue):
124+
while job_queue.qsize() > 0:
125+
cname, cvars = job_queue.get(timeout=1)
126+
127+
cpus = cpu_queue.get()
128+
print(f"Starting run {cname} on CPUs {cpus}")
129+
run("scenario.py", cname, cvars, output_dir, cpus=cpus)
130+
print(f"Finished run {cname}")
131+
cpu_queue.put(cpus)
132+
75133
for cname, cvars in config.items():
76-
run("scenario.py", cname, cvars, output_dir)
134+
job_queue.put((cname, cvars))
135+
136+
workers = []
137+
print(f"Starting {len(cpu_groups)} worker threads")
138+
for cpus in cpu_groups:
139+
cpu_queue.put(cpus)
140+
t = threading.Thread(target=worker, args=(cpu_queue, job_queue))
141+
t.start()
142+
workers.append(t)
143+
144+
for t in workers:
145+
t.join()
146+
print("All runs completed.")

ddtrace/contrib/internal/trace_utils_base.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,10 @@ def _set_url_tag(integration_config: IntegrationConfig, span: Span, url: str, qu
150150
# users should set ``DD_TRACE_HTTP_CLIENT_TAG_QUERY_STRING=False``. This case should be
151151
# removed when config.global_query_string_obfuscation_disabled is removed (v3.0).
152152
span._set_tag_str(http.URL, url)
153-
elif getattr(config._obfuscation_query_string_pattern, "pattern", None) == b"":
153+
elif (
154+
config._obfuscation_query_string_pattern is None
155+
or getattr(config._obfuscation_query_string_pattern, "pattern", None) == b""
156+
):
154157
# obfuscation is disabled when DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP=""
155158
span._set_tag_str(http.URL, strip_query_string(url))
156159
else:

0 commit comments

Comments
 (0)