-
Notifications
You must be signed in to change notification settings - Fork 536
Expand file tree
/
Copy pathtest_native_heap_gotter.py
More file actions
203 lines (159 loc) · 8.06 KB
/
Copy pathtest_native_heap_gotter.py
File metadata and controls
203 lines (159 loc) · 8.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
"""Smoke tests for the native (C/C++) heap profiling activator.
The activator (``ddtrace.internal.datadog.profiling.heap_gotter``) is fail-closed
and must behave correctly whether or not the opt-in gotter cdylib was built into
the wheel (``DD_PROFILING_NATIVE_HEAP_ENABLED=1`` at build time):
* If the library is absent (the default), ``install()``/``is_installed()`` are
no-ops returning ``False``.
* If present (a native-heap build on Linux), ``install()`` patches the process
GOT and ``is_installed()`` flips to ``True`` and stays there (idempotent).
Proving that the ``ddheap`` USDT probes actually *fire* requires attaching the
Full Host eBPF profiler (or a ``test-support`` build exposing the hook-hit
counter) and is validated in the staging dogfood, not here.
"""
import sys
import pytest
@pytest.mark.skipif(sys.platform != "linux", reason="native heap gotter is Linux-only")
@pytest.mark.subprocess
def test_native_heap_gotter_smoke() -> None:
# Runs in a fresh subprocess: install() patches the process GOT permanently,
# so we must not do it in the shared test interpreter.
from ddtrace.internal.datadog.profiling import heap_gotter
if not heap_gotter.is_available:
assert heap_gotter.install() is False
assert heap_gotter.is_installed() is False
assert heap_gotter.live_heap_enabled() is False
else:
assert heap_gotter.is_installed() is False
assert heap_gotter.install() is True
assert heap_gotter.is_installed() is True
assert heap_gotter.install() is True # idempotent
# Default gotter builds enable the live-heap Cargo feature (ddheap:free).
assert heap_gotter.live_heap_enabled() is True
blobs: list[tuple[str, int]] = []
for i in range(200):
blobs.append(("x" * 4096, i))
assert len(blobs) == 200
@pytest.mark.skipif(sys.platform != "linux", reason="native heap gotter is Linux-only")
@pytest.mark.subprocess
def test_native_heap_gotter_fork_install_and_allocations() -> None:
"""dlopen + install, then fork and keep allocating in parent and child.
Exercises the gunicorn/uWSGI-shaped path where the activator may run before
fork and again in the child. When the cdylib is present, GOT overrides are
inherited; when absent, install() stays a no-op. Either way, fork + alloc
must not crash.
"""
import os
from ddtrace.internal.datadog.profiling import heap_gotter
# Import already dlopen'd (or fail-closed). Arm in the parent.
armed = heap_gotter.install()
if heap_gotter.is_available:
assert armed is True
assert heap_gotter.is_installed() is True
else:
assert armed is False
assert heap_gotter.is_installed() is False
parent_blobs: list[tuple[str, int]] = [("x" * 4096, i) for i in range(50)]
pid = os.fork()
if pid == 0:
try:
# Child inherits mapping/GOT when armed; `_armed` skips re-entering the cdylib.
assert isinstance(heap_gotter.install(), bool)
if heap_gotter.is_available:
assert heap_gotter.is_installed() is True
child_blobs = [("y" * 4096, i) for i in range(100)]
assert len(child_blobs) == 100
os._exit(0)
except Exception:
os._exit(1)
else:
_, status = os.waitpid(pid, 0)
assert not os.WIFSIGNALED(status), f"Child crashed with signal {os.WTERMSIG(status)}"
assert os.WEXITSTATUS(status) == 0
parent_blobs.append(("z" * 4096, 99))
assert len(parent_blobs) == 51
# Parent stays `_armed`; further install() calls skip the native path.
assert isinstance(heap_gotter.install(), bool)
@pytest.mark.subprocess(env=dict(DD_PROFILING_ENABLED="true"))
def test_profiler_start_native_heap_install_idempotent_on_restart() -> None:
"""A second profiler start (e.g. uWSGI worker) calls install() again; `_armed` skips native re-entry."""
from unittest import mock
from ddtrace.internal.datadog.profiling import heap_gotter
from ddtrace.internal.settings.profiling import config as profiling_config
profiling_config.native_heap.enabled = True # pyright: ignore[reportAttributeAccessIssue]
with mock.patch.object(heap_gotter, "install", return_value=True) as install:
from ddtrace.profiling.profiler import Profiler
prof: Profiler = Profiler()
prof.start()
try:
assert install.call_count == 1
# Stop + start again (same path as a fresh worker start after fork).
# Do not call _start_service() on a running instance — collectors are
# already RUNNING and would raise ServiceStatusError.
prof.stop(flush=False)
prof.start()
assert install.call_count == 2
finally:
prof.stop(flush=False)
@pytest.mark.subprocess(env=dict(DD_PROFILING_ENABLED="true"))
def test_profiler_start_arms_native_heap_when_enabled() -> None:
"""Starting the profiler with native heap enabled invokes the activator.
Cross-platform: we force the config flag on (the import-time availability
gate would otherwise disable it when the cdylib is absent) and patch the
activator, so this exercises only the profiler wiring, not the real library.
"""
from unittest import mock
from ddtrace.internal.datadog.profiling import heap_gotter
from ddtrace.internal.settings.profiling import config as profiling_config
# Force on regardless of whether the cdylib shipped in this build.
profiling_config.native_heap.enabled = True # pyright: ignore[reportAttributeAccessIssue]
with mock.patch.object(heap_gotter, "install", return_value=True) as install:
from ddtrace.profiling.profiler import Profiler
prof: Profiler = Profiler()
prof.start()
try:
assert install.called, "profiler start should arm native heap profiling when enabled"
finally:
prof.stop(flush=False)
@pytest.mark.subprocess(env=dict(DD_PROFILING_ENABLED="true"))
def test_profiler_start_skips_native_heap_when_disabled() -> None:
"""With native heap disabled, the profiler must not import the activator.
This guards the zero-overhead promise of the disabled path: no import of
heap_gotter (and therefore no dlopen of the gotter cdylib) when the feature
is off. Assert via ``sys.modules`` so the test itself does not trigger the
import-time load.
"""
import sys
from ddtrace.internal.settings.profiling import config as profiling_config
profiling_config.native_heap.enabled = False # pyright: ignore[reportAttributeAccessIssue]
module_name = "ddtrace.internal.datadog.profiling.heap_gotter"
assert module_name not in sys.modules
from ddtrace.profiling.profiler import Profiler
prof: Profiler = Profiler()
prof.start()
try:
assert module_name not in sys.modules, "profiler must not import heap_gotter when native heap is disabled"
finally:
prof.stop(flush=False)
@pytest.mark.subprocess(
env=dict(DD_PROFILING_ENABLED="true"),
# install() failures are logged with exc_info=True, so stderr is expected.
err=lambda s: "Failed to arm native heap profiling" in s and "RuntimeError: boom" in s,
)
def test_profiler_start_survives_native_heap_install_error() -> None:
"""A failure while arming native heap profiling must not break the profiler.
Arming is best-effort: if install() raises, profiler startup swallows it and
the profiler still comes up.
"""
from unittest import mock
from ddtrace.internal.datadog.profiling import heap_gotter
from ddtrace.internal.settings.profiling import config as profiling_config
profiling_config.native_heap.enabled = True # pyright: ignore[reportAttributeAccessIssue]
with mock.patch.object(heap_gotter, "install", side_effect=RuntimeError("boom")) as install:
from ddtrace.profiling.profiler import Profiler
prof: Profiler = Profiler()
prof.start() # must not raise
try:
assert install.called
assert prof.status.value == "running"
finally:
prof.stop(flush=False)