diff --git a/ddtrace/_trace/span.py b/ddtrace/_trace/span.py index fae163e748f..ae642b97a19 100644 --- a/ddtrace/_trace/span.py +++ b/ddtrace/_trace/span.py @@ -113,7 +113,7 @@ class Span(object): "duration_ns", # Internal attributes "_context", - "_local_root", + "_local_root_value", "_parent", "_ignored_exceptions", "_on_finish_callbacks", @@ -206,7 +206,7 @@ def __init__( self._events: List[SpanEvent] = [] self._parent: Optional["Span"] = None self._ignored_exceptions: Optional[List[Type[Exception]]] = None - self._local_root: Optional["Span"] = None + self._local_root_value: Optional["Span"] = None # None means this is the root span. self._store: Optional[Dict[str, Any]] = None def _ignore_exception(self, exc: Type[Exception]) -> None: @@ -595,6 +595,23 @@ def context(self) -> Context: self._context = Context(trace_id=self.trace_id, span_id=self.span_id, is_remote=False) return self._context + @property + def _local_root(self) -> "Span": + if self._local_root_value is None: + return self + return self._local_root_value + + @_local_root.setter + def _local_root(self, value: "Span") -> None: + if value is not self: + self._local_root_value = value + else: + self._local_root_value = None + + @_local_root.deleter + def _local_root(self) -> None: + del self._local_root_value + def link_span(self, context: Context, attributes: Optional[Dict[str, Any]] = None) -> None: """Defines a causal relationship between two spans""" if not context.trace_id or not context.span_id: diff --git a/ddtrace/_trace/tracer.py b/ddtrace/_trace/tracer.py index 493d161f058..bc4caea46f8 100644 --- a/ddtrace/_trace/tracer.py +++ b/ddtrace/_trace/tracer.py @@ -797,8 +797,6 @@ def _start_span( span._parent = parent span._local_root = parent._local_root - if span._local_root is None: - span._local_root = span for k, v in _get_metas_to_propagate(context): # We do not want to propagate AppSec propagation headers # to children spans, only across distributed spans @@ -815,7 +813,6 @@ def _start_span( span_api=span_api, on_finish=[self._on_span_finish], ) - span._local_root = span if config.report_hostname: span.set_tag_str(HOSTNAME_KEY, hostname.get_hostname()) diff --git a/ddtrace/propagation/http.py b/ddtrace/propagation/http.py index de3c2a91325..9448311a8ab 100644 --- a/ddtrace/propagation/http.py +++ b/ddtrace/propagation/http.py @@ -988,6 +988,7 @@ def parent_call(): span_context = non_active_span.context if hasattr(ddtrace, "tracer") and hasattr(ddtrace.tracer, "sample"): + root_span: Optional[Span] = None if non_active_span is not None: root_span = non_active_span._local_root else: diff --git a/releasenotes/notes/fix-span-unnecessary-gc.yaml b/releasenotes/notes/fix-span-unnecessary-gc.yaml new file mode 100644 index 00000000000..20ec741dd96 --- /dev/null +++ b/releasenotes/notes/fix-span-unnecessary-gc.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + tracing: Removes a reference cycle that caused unnecessary garbage collection for top-level spans. diff --git a/tests/tracer/test_tracer.py b/tests/tracer/test_tracer.py index 5e5a052751a..59bcbf345cb 100644 --- a/tests/tracer/test_tracer.py +++ b/tests/tracer/test_tracer.py @@ -2057,3 +2057,23 @@ def test_asm_standalone_configuration(): assert tracer._compute_stats is False # reset tracer values tracer.configure(appsec_enabled=False, appsec_standalone_enabled=False) + + +def test_gc_not_used_on_root_spans(): + tracer = ddtrace.Tracer() + gc.freeze() + + with tracer.trace("test-event"): + pass + + # There should be no more span objects lingering around. + assert not any(str(obj).startswith("