Skip to content

Commit f2351b7

Browse files
authored
chore: lazy context wrapping (#15295)
## Description We implement a lazy context wrapping mechanism whereby the bytecode context wrapping is performed on first invocation of the function. Note that this still requires the original function to be instrumented via bytecode manipulations. ## Additional Notes The motivation for this change is to allow for a more lightweight bytecode instrumentation to trigger a more expensive one only when needed. This is typically when an instrumented function is called for the first time.
1 parent 7369a78 commit f2351b7

File tree

2 files changed

+84
-0
lines changed

2 files changed

+84
-0
lines changed

ddtrace/internal/wrapping/context.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@
1313
from bytecode import Bytecode
1414

1515
from ddtrace.internal.assembly import Assembly
16+
from ddtrace.internal.forksafe import Lock
1617
from ddtrace.internal.utils.inspection import link_function_to_code
18+
from ddtrace.internal.wrapping import WrappedFunction
19+
from ddtrace.internal.wrapping import Wrapper
20+
from ddtrace.internal.wrapping import is_wrapped_with
21+
from ddtrace.internal.wrapping import unwrap
22+
from ddtrace.internal.wrapping import wrap
1723

1824

1925
T = t.TypeVar("T")
@@ -406,6 +412,44 @@ def unwrap(self) -> None:
406412
_UniversalWrappingContext.extract(f).unregister(self)
407413

408414

415+
class LazyWrappingContext(WrappingContext):
416+
def __init__(self, f: FunctionType):
417+
super().__init__(f)
418+
419+
self._trampoline: t.Optional[Wrapper] = None
420+
self._trampoline_lock = Lock()
421+
422+
def wrap(self) -> None:
423+
"""Perform the bytecode wrapping on first invocation."""
424+
with (tl := self._trampoline_lock):
425+
if self._trampoline is not None:
426+
return
427+
428+
def trampoline(_, args, kwargs):
429+
with tl:
430+
f = t.cast(WrappedFunction, self.__wrapped__)
431+
if is_wrapped_with(self.__wrapped__, trampoline):
432+
f = unwrap(f, trampoline)
433+
super(LazyWrappingContext, self).wrap()
434+
return f(*args, **kwargs)
435+
436+
wrap(self.__wrapped__, trampoline)
437+
438+
self._trampoline = trampoline
439+
440+
def unwrap(self) -> None:
441+
with self._trampoline_lock:
442+
if self._trampoline is None:
443+
return
444+
445+
if self.is_wrapped(self.__wrapped__):
446+
super().unwrap()
447+
else:
448+
unwrap(t.cast(WrappedFunction, self.__wrapped__), self._trampoline)
449+
450+
self._trampoline = None
451+
452+
409453
class ContextWrappedFunction(Protocol):
410454
"""A wrapped function."""
411455

tests/internal/test_wrapping.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from ddtrace.internal.wrapping import is_wrapped_with
1111
from ddtrace.internal.wrapping import unwrap
1212
from ddtrace.internal.wrapping import wrap
13+
from ddtrace.internal.wrapping.context import LazyWrappingContext
1314
from ddtrace.internal.wrapping.context import WrappingContext
1415
from ddtrace.internal.wrapping.context import _UniversalWrappingContext
1516

@@ -926,3 +927,42 @@ def foo():
926927

927928
new_method_count = len([_ for _ in gc.get_objects() if type(_).__name__ == "method"])
928929
assert new_method_count <= method_count + 1
930+
931+
932+
def test_wrapping_context_lazy():
933+
free = 42
934+
935+
def foo():
936+
return free
937+
938+
class DummyLazyWrappingContext(LazyWrappingContext):
939+
def __init__(self, f):
940+
super().__init__(f)
941+
942+
self.count = 0
943+
944+
def __enter__(self):
945+
self.count += 1
946+
return super().__enter__()
947+
948+
(wc := DummyLazyWrappingContext(foo)).wrap()
949+
950+
assert not DummyLazyWrappingContext.is_wrapped(foo)
951+
952+
for _ in range(n := 10):
953+
assert foo() == free
954+
955+
assert DummyLazyWrappingContext.is_wrapped(foo)
956+
957+
assert wc.count == n
958+
959+
wc.count = 0
960+
961+
wc.unwrap()
962+
963+
for _ in range(10):
964+
assert not DummyLazyWrappingContext.is_wrapped(foo)
965+
966+
assert foo() == free
967+
968+
assert wc.count == 0

0 commit comments

Comments
 (0)