diff --git a/.gitlab/benchmarks/bp-runner.microbenchmarks.fail-on-breach.yml b/.gitlab/benchmarks/bp-runner.microbenchmarks.fail-on-breach.yml index 36f3d76e434..d8e37a236c0 100644 --- a/.gitlab/benchmarks/bp-runner.microbenchmarks.fail-on-breach.yml +++ b/.gitlab/benchmarks/bp-runner.microbenchmarks.fail-on-breach.yml @@ -1152,3 +1152,13 @@ experiments: thresholds: - execution_time < 0.37 ms - max_rss_usage < 38.75 MB + + # forktime + - name: forktime-baseline + thresholds: + - execution_time < 2.30 ms + - max_rss_usage < 33 MB + - name: forktime-configured + thresholds: + - execution_time < 10 ms + - max_rss_usage < 60 MB diff --git a/.gitlab/benchmarks/microbenchmarks.yml b/.gitlab/benchmarks/microbenchmarks.yml index cdec0873e83..945798f785c 100644 --- a/.gitlab/benchmarks/microbenchmarks.yml +++ b/.gitlab/benchmarks/microbenchmarks.yml @@ -169,7 +169,7 @@ microbenchmarks: SCENARIOS: - "span tracer core_api set_http_meta telemetry_add_metric otel_span otel_sdk_span recursive_computation sampling_rule_matches" - "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" - - "packages_package_for_root_module_mapping packages_update_imported_dependencies" + - "packages_package_for_root_module_mapping packages_update_imported_dependencies fork_time" - CPUS_PER_RUN: "2" SCENARIOS: - "django_simple flask_simple flask_sqli errortracking_django_simple errortracking_flask_sqli startup" diff --git a/benchmarks/fork_time/config.yaml b/benchmarks/fork_time/config.yaml new file mode 100644 index 00000000000..1e8e75012bd --- /dev/null +++ b/benchmarks/fork_time/config.yaml @@ -0,0 +1,4 @@ +baseline: + configure: false +configured: + configure: true diff --git a/benchmarks/fork_time/requirements_scenario.txt b/benchmarks/fork_time/requirements_scenario.txt new file mode 100644 index 00000000000..37f779e6a15 --- /dev/null +++ b/benchmarks/fork_time/requirements_scenario.txt @@ -0,0 +1 @@ +flask>=2.0.0 diff --git a/benchmarks/fork_time/scenario.py b/benchmarks/fork_time/scenario.py new file mode 100644 index 00000000000..d30a290b169 --- /dev/null +++ b/benchmarks/fork_time/scenario.py @@ -0,0 +1,81 @@ +""" +Benchmark fork time overhead with ddtrace. + +Measures the time to fork a process when ddtrace is imported and configured +in the parent process (simulating gunicorn preload mode with a real Flask app). +""" + +import multiprocessing +import os +import time + +import bm + + +def _child_process(conn, parent_fork_time): + """Child process + + Measures fork overhead by recording when the child starts executing + and comparing to when the parent initiated the fork. + """ + fork_overhead = time.perf_counter() - parent_fork_time + conn.send(fork_overhead) + conn.close() + + +class ForkTime(bm.Scenario): + """Measure fork overhead with ddtrace.""" + + configure: bool + + cprofile_loops: int = 0 # Fork benchmarks don't work well with cprofile + + def __post_init__(self): + self._setup_done = False + + def _one_time_setup(self): + """Run ddtrace and Flask setup exactly once per process. + + _pyperf is called once per warmup/value by pyperf. Moving setup here + prevents accumulating Flask app instances and ddtrace instrumentation + state across calls, which would artificially inflate fork times. + """ + if self._setup_done: + return + + if self.configure: + os.environ["DD_TRACE_ENABLED"] = "true" + os.environ["DD_SERVICE"] = "fork-benchmark" + os.environ["DD_ENV"] = "benchmark" + + import ddtrace.auto # noqa: F401 + + self._setup_flask() + self._setup_done = True + + def _setup_flask(self): + from flask import Flask + + try: + app = Flask(__name__) # noqa: F841 + + @app.route("/") + def hello(): + return "Hello" + except ImportError: + pass + + def _pyperf(self, loops: int) -> float: + self._one_time_setup() + + total = 0.0 + for _ in range(loops): + parent_conn, child_conn = multiprocessing.Pipe() + fork_start = time.perf_counter() + p = multiprocessing.Process(target=_child_process, args=(child_conn, fork_start)) + p.start() + fork_overhead = parent_conn.recv() + p.join() + parent_conn.close() + total += fork_overhead + return total