Skip to content

Commit ba29a15

Browse files
refactor(profiling): add type annotations to native-heap ownership partition
Annotate the Phase 2 ownership-partition additions: the BUILD_NATIVE_HEAP_GOTTER_TEST_SUPPORT build knob (setup.py), the _arm_native_heap/_start_service locals (profiler.py), and the module-level constants and remaining local variables in the partition and end-to-end handoff tests. set_native_heap_partition and the test_hook_hits activator additions in this PR were already fully annotated.
1 parent aadab54 commit ba29a15

4 files changed

Lines changed: 40 additions & 34 deletions

File tree

ddtrace/profiling/profiler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ def _arm_native_heap(self) -> bool:
398398
# live-heap (ddheap:free + retain flagging) is a build-time
399399
# property of the cdylib, not a runtime toggle; report which
400400
# mode was actually armed for observability.
401-
mode = "live-heap" if heap_gotter.live_heap_enabled() else "allocation-only"
401+
mode: str = "live-heap" if heap_gotter.live_heap_enabled() else "allocation-only"
402402
LOG.debug("Native heap profiling armed (GOT overrides installed, %s)", mode)
403403
return True
404404
LOG.debug("Native heap profiling requested but GOT overrides were not installed")
@@ -408,7 +408,7 @@ def _arm_native_heap(self) -> bool:
408408

409409
def _start_service(self) -> None:
410410
"""Start the profiler."""
411-
native_heap_armed = self._arm_native_heap()
411+
native_heap_armed: bool = self._arm_native_heap()
412412

413413
# Native-heap de-duplication (Phase 2 ownership partition): a process
414414
# must have exactly ONE producer per allocator domain, otherwise the same

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@
145145
# glibc-malloc tail the in-process sampler drops) without a live eBPF attach.
146146
# Never set for shipped wheels — it is strictly a test build knob and only has
147147
# any effect when BUILD_NATIVE_HEAP_GOTTER is also on.
148-
BUILD_NATIVE_HEAP_GOTTER_TEST_SUPPORT = os.getenv("DD_PROFILING_NATIVE_HEAP_TEST_SUPPORT", "0").lower() in (
148+
BUILD_NATIVE_HEAP_GOTTER_TEST_SUPPORT: bool = os.getenv("DD_PROFILING_NATIVE_HEAP_TEST_SUPPORT", "0").lower() in (
149149
"1",
150150
"yes",
151151
"on",

tests/profiling/collector/test_memalloc.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1585,12 +1585,12 @@ def test_obj_and_mem_domain_coexist(tmp_path: Path) -> None:
15851585

15861586
# Each buffer is far larger than pymalloc's 512B threshold, so it is served by
15871587
# the raw allocator (glibc malloc).
1588-
_PARTITION_LARGE_ALLOC_BYTES = 1024 * 1024
1588+
_PARTITION_LARGE_ALLOC_BYTES: int = 1024 * 1024
15891589
# Kept <= 256 so the ``range()`` loop variables stay in CPython's small-int
15901590
# cache: ``_allocate_large_buffers`` then makes *only* > 512B allocations, which
15911591
# makes the "large allocations are skipped" assertion deterministic.
1592-
_PARTITION_LARGE_ALLOC_COUNT = 64
1593-
_PARTITION_SMALL_ALLOC_COUNT = 200_000
1592+
_PARTITION_LARGE_ALLOC_COUNT: int = 64
1593+
_PARTITION_SMALL_ALLOC_COUNT: int = 200_000
15941594

15951595

15961596
def _allocate_large_buffers(store: list[object]) -> None:
@@ -1605,10 +1605,10 @@ def _allocate_small_objects(store: list[object]) -> None:
16051605

16061606
def test_native_heap_partition_skips_large_managed_allocations(tmp_path: Path) -> None:
16071607
"""With the partition on, > 512B OBJ allocations are skipped but <= 512B kept."""
1608-
output_filename = _setup_profiling_prelude(tmp_path, "test_native_heap_partition_skips_large")
1608+
output_filename: str = _setup_profiling_prelude(tmp_path, "test_native_heap_partition_skips_large")
16091609

16101610
store: list[object] = []
1611-
mc = memalloc.MemoryCollector(heap_sample_size=64 * 1024)
1611+
mc: memalloc.MemoryCollector = memalloc.MemoryCollector(heap_sample_size=64 * 1024)
16121612
memalloc.set_native_heap_partition(True)
16131613
try:
16141614
with mc:
@@ -1617,17 +1617,17 @@ def test_native_heap_partition_skips_large_managed_allocations(tmp_path: Path) -
16171617
mc.snapshot()
16181618
ddup.upload()
16191619

1620-
profile = pprof_utils.parse_newest_profile(output_filename)
1621-
heap_samples = pprof_utils.get_samples_with_value_type(profile, "heap-space")
1620+
profile: "pprof_pb2.Profile" = pprof_utils.parse_newest_profile(output_filename)
1621+
heap_samples: "list[pprof_pb2.Sample]" = pprof_utils.get_samples_with_value_type(profile, "heap-space")
16221622

16231623
# Small pool-served allocations (<= 512B) are invisible to the gotter, so
16241624
# the in-process sampler must keep sampling them.
1625-
small_count = _count_heap_samples_with_function(profile, heap_samples, "_allocate_small_objects")
1625+
small_count: int = _count_heap_samples_with_function(profile, heap_samples, "_allocate_small_objects")
16261626
assert small_count > 0, "small (<=512B) managed allocations must still be sampled when the partition is on"
16271627

16281628
# Large allocations (> 512B) hit glibc malloc and are owned by the gotter,
16291629
# so the in-process sampler must skip them entirely.
1630-
large_count = _count_heap_samples_with_function(profile, heap_samples, "_allocate_large_buffers")
1630+
large_count: int = _count_heap_samples_with_function(profile, heap_samples, "_allocate_large_buffers")
16311631
assert large_count == 0, (
16321632
f"large (>512B) managed allocations must be skipped when the partition is on (got {large_count})"
16331633
)
@@ -1640,10 +1640,10 @@ def test_native_heap_partition_skips_large_managed_allocations(tmp_path: Path) -
16401640

16411641
def test_native_heap_partition_disabled_samples_all_sizes(tmp_path: Path) -> None:
16421642
"""With the partition off (default, fail-safe), all sizes are sampled."""
1643-
output_filename = _setup_profiling_prelude(tmp_path, "test_native_heap_partition_disabled")
1643+
output_filename: str = _setup_profiling_prelude(tmp_path, "test_native_heap_partition_disabled")
16441644

16451645
store: list[object] = []
1646-
mc = memalloc.MemoryCollector(heap_sample_size=64 * 1024)
1646+
mc: memalloc.MemoryCollector = memalloc.MemoryCollector(heap_sample_size=64 * 1024)
16471647
# Default is off; set explicitly in case a prior test left it on.
16481648
memalloc.set_native_heap_partition(False)
16491649
with mc:
@@ -1652,12 +1652,12 @@ def test_native_heap_partition_disabled_samples_all_sizes(tmp_path: Path) -> Non
16521652
mc.snapshot()
16531653
ddup.upload()
16541654

1655-
profile = pprof_utils.parse_newest_profile(output_filename)
1656-
heap_samples = pprof_utils.get_samples_with_value_type(profile, "heap-space")
1655+
profile: "pprof_pb2.Profile" = pprof_utils.parse_newest_profile(output_filename)
1656+
heap_samples: "list[pprof_pb2.Sample]" = pprof_utils.get_samples_with_value_type(profile, "heap-space")
16571657

1658-
large_count = _count_heap_samples_with_function(profile, heap_samples, "_allocate_large_buffers")
1658+
large_count: int = _count_heap_samples_with_function(profile, heap_samples, "_allocate_large_buffers")
16591659
assert large_count > 0, "large (>512B) allocations must be sampled when the partition is off"
1660-
small_count = _count_heap_samples_with_function(profile, heap_samples, "_allocate_small_objects")
1660+
small_count: int = _count_heap_samples_with_function(profile, heap_samples, "_allocate_small_objects")
16611661
assert small_count > 0, "small (<=512B) allocations must be sampled when the partition is off"
16621662

16631663
del store

tests/profiling/test_native_heap_gotter.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,16 @@
1515
"""
1616

1717
import sys
18+
from typing import TYPE_CHECKING
1819

1920
import pytest
2021

2122

23+
if TYPE_CHECKING:
24+
# We need the pyright: ignore because pprof_pb2 does not exist as a real module, only as a pyi.
25+
from tests.profiling.collector import pprof_pb2 # pyright: ignore[reportMissingModuleSource]
26+
27+
2228
# Evaluated in the PARENT interpreter (subprocess bodies cannot express a skip:
2329
# an in-body ``pytest.skip`` would surface as a non-zero exit and FAIL the outer
2430
# test). ``test_hook_hits()`` is a read-only counter query with no side effects —
@@ -170,11 +176,11 @@ def test_profiler_keeps_managed_heap_when_native_heap_armed() -> None:
170176
with mock.patch.object(memalloc, "set_native_heap_partition") as set_partition:
171177
from ddtrace.profiling.profiler import Profiler
172178

173-
prof = Profiler()
179+
prof: Profiler = Profiler()
174180
prof.start()
175181
try:
176182
assert install.called, "the gotter must still be armed when native heap is enabled"
177-
has_mem = any(isinstance(c, memalloc.MemoryCollector) for c in prof._profiler._collectors)
183+
has_mem: bool = any(isinstance(c, memalloc.MemoryCollector) for c in prof._profiler._collectors)
178184
assert has_mem, (
179185
"in-process managed-heap (OBJ/MEM) collector must stay active when the gotter is armed; "
180186
"the gotter owns only the native/raw glibc malloc domain"
@@ -204,10 +210,10 @@ def test_profiler_keeps_managed_heap_when_gotter_not_installed() -> None:
204210
with mock.patch.object(memalloc, "set_native_heap_partition") as set_partition:
205211
from ddtrace.profiling.profiler import Profiler
206212

207-
prof = Profiler()
213+
prof: Profiler = Profiler()
208214
prof.start()
209215
try:
210-
has_mem = any(isinstance(c, memalloc.MemoryCollector) for c in prof._profiler._collectors)
216+
has_mem: bool = any(isinstance(c, memalloc.MemoryCollector) for c in prof._profiler._collectors)
211217
assert has_mem, "in-process memory collector must stay active when the gotter fails to install"
212218
# Not armed -> partition off so ALL sizes keep being sampled.
213219
set_partition.assert_called_once_with(False)
@@ -234,11 +240,11 @@ def test_profiler_keeps_managed_heap_when_native_heap_disabled() -> None:
234240
with mock.patch.object(memalloc, "set_native_heap_partition") as set_partition:
235241
from ddtrace.profiling.profiler import Profiler
236242

237-
prof = Profiler()
243+
prof: Profiler = Profiler()
238244
prof.start()
239245
try:
240246
assert not install.called
241-
has_mem = any(isinstance(c, memalloc.MemoryCollector) for c in prof._profiler._collectors)
247+
has_mem: bool = any(isinstance(c, memalloc.MemoryCollector) for c in prof._profiler._collectors)
242248
assert has_mem, "in-process memory collector must run when native heap is disabled"
243249
# Feature off -> partition off (all sizes sampled).
244250
set_partition.assert_called_once_with(False)
@@ -313,8 +319,8 @@ def test_native_heap_ownership_handoff_end_to_end() -> None:
313319
assert heap_gotter.install() is True
314320
assert heap_gotter.is_installed() is True
315321

316-
prefix = os.path.join(tempfile.mkdtemp(), "handoff")
317-
output_filename = prefix + "." + str(os.getpid())
322+
prefix: str = os.path.join(tempfile.mkdtemp(), "handoff")
323+
output_filename: str = prefix + "." + str(os.getpid())
318324
ddup.config(
319325
service="test_native_heap_ownership_handoff",
320326
version="test",
@@ -324,7 +330,7 @@ def test_native_heap_ownership_handoff_end_to_end() -> None:
324330
ddup.start()
325331

326332
store: list[object] = []
327-
mc = memalloc.MemoryCollector(heap_sample_size=64 * 1024)
333+
mc: memalloc.MemoryCollector = memalloc.MemoryCollector(heap_sample_size=64 * 1024)
328334
memalloc.set_native_heap_partition(True)
329335
try:
330336
with mc:
@@ -333,19 +339,19 @@ def test_native_heap_ownership_handoff_end_to_end() -> None:
333339
# raw malloc (it is NOT sampling-gated), so background allocations
334340
# can only inflate the delta — never shrink it below the number of
335341
# large buffers we deliberately allocate.
336-
hits_before = heap_gotter.test_hook_hits()
342+
hits_before: "int | None" = heap_gotter.test_hook_hits()
337343
_allocate_large_buffers(store)
338-
hits_after = heap_gotter.test_hook_hits()
344+
hits_after: "int | None" = heap_gotter.test_hook_hits()
339345

340346
_allocate_small_objects(store)
341347
mc.snapshot()
342348
ddup.upload()
343349

344-
profile = pprof_utils.parse_newest_profile(output_filename)
345-
heap_samples = pprof_utils.get_samples_with_value_type(profile, "heap-space")
350+
profile: "pprof_pb2.Profile" = pprof_utils.parse_newest_profile(output_filename)
351+
heap_samples: "list[pprof_pb2.Sample]" = pprof_utils.get_samples_with_value_type(profile, "heap-space")
346352

347353
# (a) In-process producer dropped the > 512B tail ...
348-
large_count = _count_heap_samples_with_function(profile, heap_samples, "_allocate_large_buffers")
354+
large_count: int = _count_heap_samples_with_function(profile, heap_samples, "_allocate_large_buffers")
349355
assert large_count == 0, (
350356
f"partition ON: > 512B managed allocations must NOT be sampled in-process (got {large_count})"
351357
)
@@ -354,15 +360,15 @@ def test_native_heap_ownership_handoff_end_to_end() -> None:
354360
# is a single raw malloc routed through the patched GOT, so the hook-hit
355361
# counter must advance by at least the number of large buffers.
356362
assert hits_before is not None and hits_after is not None
357-
delta = hits_after - hits_before
363+
delta: int = hits_after - hits_before
358364
assert delta >= _PARTITION_LARGE_ALLOC_COUNT, (
359365
"native gotter must capture the > 512B raw-malloc tail the in-process sampler dropped "
360366
f"(hook-hit delta {delta} < {_PARTITION_LARGE_ALLOC_COUNT} large allocations)"
361367
)
362368

363369
# Control: <= 512B pool-served allocations are invisible to the gotter
364370
# and must still be sampled in-process — the partition splits by size.
365-
small_count = _count_heap_samples_with_function(profile, heap_samples, "_allocate_small_objects")
371+
small_count: int = _count_heap_samples_with_function(profile, heap_samples, "_allocate_small_objects")
366372
assert small_count > 0, "partition ON: <= 512B managed allocations must still be sampled in-process"
367373
finally:
368374
# Reset the process-global flag so it cannot bleed into other tests

0 commit comments

Comments
 (0)