Skip to content

chore(tracing): emit five CPython GC collection and pause runtime metrics (PROF-15855) - #19937

Draft
vlad-scherbich wants to merge 8 commits into
mainfrom
vlad/runtime-gc-metrics
Draft

chore(tracing): emit five CPython GC collection and pause runtime metrics (PROF-15855)#19937
vlad-scherbich wants to merge 8 commits into
mainfrom
vlad/runtime-gc-metrics

Conversation

@vlad-scherbich

@vlad-scherbich vlad-scherbich commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Description

With DD_RUNTIME_METRICS_ENABLED (default off), each flush adds five DogStatsD metrics: runtime.python.gc.collections.gen{0,1,2} (gc.get_stats() collections) and runtime.python.gc.pause.time / runtime.python.gc.pause.max (STW pause ns). GCPauseMonitor is one refcounted gc.callbacks subscriber; snapshot on flush. Existing runtime.python.gc.count.gen{0,1,2} stay allocation counters. RuntimeCollectorsIterable.stop() uninstalls the callback.

Testing

Unit tests drive gc.collect() through the monitor and collector to pin callback lifetime, the five names, and a zeroed window on the next flush.

Risks

+5 series per existing runtime.python.gc.count.gen0 tag combo. Default-off; Datadog-internal already has the flag on.

Additional Notes

KR0.5.1 fleet-scan metrics. add_listener is unused here for profiling — this PR does not emit STW timeline events.

Layer Meaning Which PR
Armed at runtime callback when runtime metrics on this PR
Observable 5 DogStatsD names this PR
Pause timeline start→stop events (DD_PROFILING_GC_ENABLED) later: vlad/profiling-gc-pause-events

On main: T Kowalski #19190 sampled GC frames (DD_PROFILING_STACK_GC_ENABLED) — not pause duration, not Henrik. Next PR (stacked here) adds exact gc.callbacks events for the Henrik STW timeline; do not P1 until after this fleet scan. Cut names (pause.time.gen*, collected.*, uncollectable.*) live on vlad/runtime-gc-metrics-future only.

@datadog-datadog-us1-prod

datadog-datadog-us1-prod Bot commented Aug 28, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 11f00bc | Docs | View more details | Give us feedback!

@cit-pr-commenter-54b7da

Copy link
Copy Markdown

Codeowners resolved as

Resolved from the full PR diff against main using the target branch CODEOWNERS file.
CODEOWNERS team requests not listed below are not required by the current file set.

ddtrace/internal/runtime/constants.py                                   @DataDog/apm-sdk-capabilities-python
ddtrace/internal/runtime/gc_monitor.py                                  @DataDog/apm-sdk-capabilities-python
ddtrace/internal/runtime/metric_collectors.py                           @DataDog/apm-sdk-capabilities-python
ddtrace/internal/runtime/runtime_metrics.py                             @DataDog/apm-sdk-capabilities-python
releasenotes/notes/runtime-python-gc-pause-metrics-c3e8a1b2d4f6.yaml    @DataDog/apm-python
tests/tracer/runtime/test_gc_monitor.py                                 @DataDog/apm-sdk-capabilities-python
tests/tracer/runtime/test_metric_collectors.py                          @DataDog/apm-sdk-capabilities-python

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Aug 28, 2026

Copy link
Copy Markdown

Circular import analysis

⚠️ Existing circular imports

There are 3 circular imports that already exist on the base branch and have not been changed by this PR.

ddtrace.errortracking._handled_exceptions.bytecode_injector -> ddtrace.errortracking._handled_exceptions.callbacks -> ddtrace.errortracking._handled_exceptions.collector -> ddtrace.errortracking._handled_exceptions.bytecode_reporting -> ddtrace.errortracking._handled_exceptions.bytecode_injector
ddtrace.llmobs -> ddtrace.llmobs._evaluators -> ddtrace.llmobs._evaluators.format -> ddtrace.llmobs._experiment -> ddtrace.llmobs
ddtrace.appsec._asm_request_context -> ddtrace.appsec._iast._iast_request_context_base -> ddtrace.appsec._iast._iast_env -> ddtrace.appsec._iast.reporter -> ddtrace.appsec._exploit_prevention.stack_traces -> ddtrace.appsec._asm_request_context

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Aug 28, 2026

Copy link
Copy Markdown

Dependency direction analysis

⚠️ Existing dependency direction violations

There are 240 dependency direction violations that already exist on the base branch and have not been changed by this PR.

Show existing violations (showing 5 of 240 highest severity)
ddtrace.internal.tracemethods -×-> ddtrace.trace  (internal-core -> product:tracing, score=135)
ddtrace.appsec._contrib.django -×-> ddtrace.trace  (product:appsec -> product:tracing, score=133)
ddtrace.internal.opentelemetry.trace -×-> ddtrace.trace  (product:opentelemetry -> product:tracing, score=133)
ddtrace.profiling.collector.stack -×-> ddtrace.trace  (product:profiling -> product:tracing, score=133)
ddtrace.llmobs._integrations.bedrock -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=133)

To see all violations, download the layers-base.json and layers-pr.json artifacts from this CI job and run:

uv run --script scripts/import-analysis/layers.py compare layers-base.json layers-pr.json

@vlad-scherbich vlad-scherbich changed the title feat(tracing): emit CPython GC pause and collection runtime metrics feat(tracing): emit CPython GC pause and collection runtime metrics (/PROF-15855) Aug 28, 2026
@vlad-scherbich vlad-scherbich changed the title feat(tracing): emit CPython GC pause and collection runtime metrics (/PROF-15855) feat(tracing): emit CPython GC pause and collection runtime metrics (PROF-15855) Aug 28, 2026
@pr-commenter

pr-commenter Bot commented Aug 28, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-08-29 17:37:45

Comparing candidate commit 11f00bc in PR branch vlad/runtime-gc-metrics with baseline commit b960e5d in branch main.

📊 Benchmarking dashboard

Found 0 performance improvements and 3 performance regressions! Performance is the same for 374 metrics, 9 unstable metrics, 5 flaky benchmarks without significant changes.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

scenario:httppropagationinject-ids_only

  • 🟥 execution_time [+2.481µs; +2.632µs] or [+14.669%; +15.567%]

scenario:telemetryaddmetric-1-count-metric-1-times

  • 🟥 execution_time [+653.947ns; +680.785ns] or [+24.202%; +25.195%]

scenario:tracer-small

  • 🟥 execution_time [+26.773µs; +29.264µs] or [+8.216%; +8.980%]

Unstable benchmarks

These benchmarks have a confidence interval too wide to call a change; treat them as noise rather than signal.

scenario:coreapiscenario-context_with_data_listeners

  • unstable execution_time [-680.328ns; +773.842ns] or [-6.238%; +7.096%]

scenario:coreapiscenario-core_dispatch_1_listener

  • unstable execution_time [-33.683ns; +32.773ns] or [-5.503%; +5.354%]

scenario:coreapiscenario-core_dispatch_50_listeners

  • unstable execution_time [-1338.614ns; +1943.436ns] or [-7.977%; +11.581%]

scenario:coreapiscenario-core_dispatch_exception_listeners

  • unstable execution_time [-672.710ns; +1862.810ns] or [-5.250%; +14.539%]

scenario:coreapiscenario-core_dispatch_listeners

  • unstable execution_time [-263.486ns; +382.258ns] or [-7.281%; +10.563%]

scenario:coreapiscenario-core_dispatch_no_args_listeners

  • unstable execution_time [-181.833ns; +323.322ns] or [-6.342%; +11.276%]

scenario:coreapiscenario-core_dispatch_with_results_1_listener

  • unstable execution_time [-74.866ns; +73.495ns] or [-6.365%; +6.249%]

scenario:coreapiscenario-core_dispatch_with_results_50_listeners

  • unstable execution_time [-4060.055ns; +3842.218ns] or [-9.969%; +9.435%]

scenario:coreapiscenario-core_dispatch_with_results_listeners

  • unstable execution_time [-897.978ns; +702.709ns] or [-10.892%; +8.523%]

Known flaky benchmarks

These benchmarks are marked as flaky and will not trigger a failure. Modify FLAKY_BENCHMARKS_REGEX to control which benchmarks are marked as flaky.

Known flaky benchmarks without significant changes:

  • scenario:errortrackingflasksqli-baseline
  • scenario:flasksimple-iast-get
  • scenario:sethttpmeta-all-enabled
  • scenario:span-start
  • scenario:telemetryaddmetric-record-100-metrics

@vlad-scherbich
vlad-scherbich requested a lite review from Copilot August 28, 2026 20:08
@vlad-scherbich

Copy link
Copy Markdown
Contributor Author

@codex review

@vlad-scherbich vlad-scherbich changed the title feat(tracing): emit CPython GC pause and collection runtime metrics (PROF-15855) feat(tracing): emit five CPython GC collection and pause runtime metrics (PROF-15855) Aug 28, 2026
@vlad-scherbich vlad-scherbich changed the title feat(tracing): emit five CPython GC collection and pause runtime metrics (PROF-15855) chore(tracing): emit five CPython GC collection and pause runtime metrics (PROF-15855) Aug 28, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new CPython GC stop-the-world (STW) pause-time metrics and per-generation collection-count deltas to the existing runtime metrics pipeline, enabling fleet-wide GC impact visibility via DogStatsD when runtime metrics are enabled.

Changes:

  • Introduces a process-wide gc.callbacks-based pause monitor with refcounted install and snapshot/reset semantics.
  • Extends GC runtime metrics to emit per-generation gc.get_stats() collection deltas plus pause total/max (ns) per flush interval.
  • Updates runtime-metrics shutdown to stop collectors, adds/updates tests, and includes a release note.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
ddtrace/internal/runtime/gc_monitor.py New process-wide GC pause observer built on gc.callbacks, with snapshot/reset and listener support.
ddtrace/internal/runtime/metric_collectors.py Extends GC runtime collector to emit collection deltas and pause metrics; integrates the pause monitor.
ddtrace/internal/runtime/runtime_metrics.py Adds a collector stop() cascade and calls it during RuntimeWorker.disable().
ddtrace/internal/runtime/constants.py Defines new metric names and expands GC_RUNTIME_METRICS.
tests/tracer/runtime/test_gc_monitor.py New unit tests for monitor acquire/release, snapshot behavior, and listener plumbing.
tests/tracer/runtime/test_metric_collectors.py Updates GC collector tests to stop the collector and adds assertions for the new metrics.
releasenotes/notes/runtime-python-gc-pause-metrics-c3e8a1b2d4f6.yaml Documents the new runtime metrics feature.
Suppressed comments (1)

ddtrace/internal/runtime/metric_collectors.py:83

  • collect_fn snapshots the pause window after calling gc.get_stats()/allocating lists. Any GC triggered by those allocations can be counted as "pause" for the interval, and stop() can race with flush by changing _monitor between the check and the method call. Snapshot the monitor first using a local variable, then do the other reads/allocations.
        gc_mod: ModuleType = self.modules["gc"]

        counts: tuple[int, int, int] = gc_mod.get_count()
        collections: list[int] = _read_gc_collections(gc_mod)
        d_collections: list[int] = _delta(collections, self._prev_collections)

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread ddtrace/internal/runtime/gc_monitor.py
Comment thread ddtrace/internal/runtime/metric_collectors.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7172716e8f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ddtrace/internal/runtime/gc_monitor.py Outdated
Comment thread ddtrace/internal/runtime/metric_collectors.py Outdated
Comment thread ddtrace/internal/runtime/gc_monitor.py Outdated
Comment thread ddtrace/internal/runtime/runtime_metrics.py
Comment thread ddtrace/internal/runtime/metric_collectors.py Outdated
Clear in-flight starts on uninstall, unregister the collector fork hook,
and snapshot primitives before allocating so a reentrant GC cannot drop
or mismatch a pause.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Suppressed comments (3)

Previously missed (2) — in code that hasn't changed since the last review.

ddtrace/internal/runtime/gc_monitor.py:104

  • GCPauseMonitor.acquire() registers a forksafe hook (self.reset), but release() never unregisters it when the refcount drops to 0. This leaves a global fork hook installed even after the monitor is fully released, which contradicts the refcounted install semantics and adds unnecessary fork-time work.
            if self._refcount == 0:
                try:
                    gc.callbacks.remove(self._on_gc)
                except ValueError:
                    pass

tests/tracer/runtime/test_metric_collectors.py:228

  • Type annotation for collected uses tuple[str, str], but GCRuntimeMetricCollector.collect() returns numeric values (ints) for these metrics. Keeping the annotation accurate avoids confusing type checkers/readers.

This issue also appears on line 247 of the same file.

            collected: Optional[list[tuple[str, str]]] = collector.collect(GC_RUNTIME_METRICS)

tests/tracer/runtime/test_metric_collectors.py:247

  • Type annotation for collected uses tuple[str, str], but GCRuntimeMetricCollector.collect() returns numeric values (ints) for these metrics. Keeping the annotation accurate avoids confusing type checkers/readers.
            collected: Optional[list[tuple[str, str]]] = collector.collect(GC_RUNTIME_METRICS)

Comment thread ddtrace/internal/runtime/gc_monitor.py Outdated
A child that inherits _MONITOR_LOCK held across fork can deadlock the
next gc_pause_monitor() call.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (3)

Previously missed (2) — in code that hasn't changed since the last review.

ddtrace/internal/runtime/gc_monitor.py:106

  • GCPauseMonitor.acquire() registers self.reset as a forksafe hook, but release() never unregisters it when the refcount reaches 0. This means each standalone GCPauseMonitor() instance (e.g., in tests) is kept alive by forksafe._registry, and the registry grows over time (extra hooks run on every fork). Unregister the hook when the last acquire is released.
    def acquire(self) -> None:
        with self._lock:
            self._refcount += 1
            if self._refcount == 1:
                if self._on_gc not in gc.callbacks:
                    gc.callbacks.append(self._on_gc)
                if not self._fork_registered:
                    forksafe.register(self.reset)
                    self._fork_registered = True

    def release(self) -> None:
        with self._lock:
            if self._refcount <= 0:
                return
            self._refcount -= 1
            if self._refcount == 0:
                try:
                    gc.callbacks.remove(self._on_gc)
                except ValueError:
                    pass
                # Drop in-flight starts so a later re-acquire cannot pair a
                # new stop with a stale timestamp from before uninstall.
                self._start_ns = [0] * GEN_COUNT
                self._clear_window()

tests/tracer/runtime/test_metric_collectors.py:228

  • Type annotation mismatch: ValueCollector.collect() returns pairs where the value is numeric for runtime metrics collectors, so annotating this as tuple[str, str] is misleading and can break type-checking of tests. Annotate this as tuple[str, int] here (these GC metrics are ints).

This issue also appears on line 247 of the same file.

            collected: Optional[list[tuple[str, str]]] = collector.collect(GC_RUNTIME_METRICS)

tests/tracer/runtime/test_metric_collectors.py:247

  • Type annotation mismatch: this GC collector returns numeric values; annotate the collected metrics as tuple[str, int] instead of tuple[str, str] to keep the test type-correct.
            collected: Optional[list[tuple[str, str]]] = collector.collect(GC_RUNTIME_METRICS)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants