Skip to content

Commit 7aa77ea

Browse files
alyshawangavidal
authored andcommitted
refactor(profiling): unifying the allocation and live memory profilers
This PR unifies the Python memory profiler's allocation and live heap profilers into a single, consistent, sampling-based system. Previously, both profilers operated independently with separate implementations, overlapping functionality, and distinct data paths. This change replaces the older allocation profiler logic with an enhanced version of the heap profiler that: - Tracks both live allocations and freed-but-unreported allocations in one place. - Produces enriched samples that include: in_use flag (is this allocation still alive?), reported flag (has this sample been reported before?), count (scaling factor for probabilistic allocation count) - Fully removes the legacy iter_events() and its global_alloc_tracker-based sampling.
1 parent 1a0ab61 commit 7aa77ea

File tree

11 files changed

+958
-509
lines changed

11 files changed

+958
-509
lines changed

ddtrace/profiling/collector/_memalloc.c

Lines changed: 8 additions & 304 deletions
Large diffs are not rendered by default.

ddtrace/profiling/collector/_memalloc.pyi

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@ from .. import event
66
FrameType = event.DDFrame
77
StackType = event.StackTraceType
88

9-
# (stack, nframe, thread_id)
10-
TracebackType = typing.Tuple[StackType, int, int]
9+
# (stack, thread_id)
10+
TracebackType = typing.Tuple[StackType, int]
1111

1212
def start(max_nframe: int, max_events: int, heap_sample_size: int) -> None: ...
1313
def stop() -> None: ...
14-
def heap() -> typing.List[typing.Tuple[TracebackType, int]]: ...
15-
def iter_events() -> typing.Iterator[typing.Tuple[TracebackType, int]]: ...
14+
def heap() -> typing.List[typing.Tuple[TracebackType, int, int, int]]: ...

ddtrace/profiling/collector/_memalloc_heap.c

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ typedef struct
7979
memalloc_heap_map_t* allocs_m;
8080
ptr_array_t frees;
8181
} freezer;
82+
/* List of freed samples that haven't been reported yet */
83+
traceback_array_t unreported_samples;
8284

8385
/* Debug guard to assert that GIL-protected critical sections are maintained
8486
* while accessing the profiler's state */
@@ -109,6 +111,7 @@ heap_tracker_init(heap_tracker_t* heap_tracker)
109111
heap_tracker->allocs_m = memalloc_heap_map_new();
110112
heap_tracker->freezer.allocs_m = memalloc_heap_map_new();
111113
ptr_array_init(&heap_tracker->freezer.frees);
114+
traceback_array_init(&heap_tracker->unreported_samples);
112115
heap_tracker->allocated_memory = 0;
113116
heap_tracker->frozen = false;
114117
heap_tracker->sample_size = 0;
@@ -122,6 +125,7 @@ heap_tracker_wipe(heap_tracker_t* heap_tracker)
122125
memalloc_heap_map_delete(heap_tracker->allocs_m);
123126
memalloc_heap_map_delete(heap_tracker->freezer.allocs_m);
124127
ptr_array_wipe(&heap_tracker->freezer.frees);
128+
traceback_array_wipe(&heap_tracker->unreported_samples);
125129
}
126130

127131
static void
@@ -214,6 +218,12 @@ memalloc_heap_untrack_no_cpython(heap_tracker_t* heap_tracker, void* ptr)
214218
}
215219
if (!heap_tracker->frozen) {
216220
traceback_t* tb = memalloc_heap_map_remove(heap_tracker->allocs_m, ptr);
221+
if (tb && !tb->reported) {
222+
/* If the sample hasn't been reported yet, add it to the allocation list */
223+
traceback_array_append(&heap_tracker->unreported_samples, tb);
224+
MEMALLOC_GIL_DEBUG_CHECK_RELEASE(&heap_tracker->gil_guard);
225+
return NULL;
226+
}
217227
MEMALLOC_GIL_DEBUG_CHECK_RELEASE(&heap_tracker->gil_guard);
218228
return tb;
219229
}
@@ -328,7 +338,7 @@ memalloc_heap_track(uint16_t max_nframe, void* ptr, size_t size, PyMemAllocatorD
328338
will tend to be larger for large allocations and smaller for small
329339
allocations, and close to the average sampling interval so that the sum
330340
of sample live allocations stays close to the actual heap size */
331-
traceback_t* tb = memalloc_get_traceback(max_nframe, ptr, global_heap_tracker.allocated_memory, domain);
341+
traceback_t* tb = memalloc_get_traceback(max_nframe, ptr, size, domain, global_heap_tracker.allocated_memory);
332342
if (!tb) {
333343
memalloc_yield_guard();
334344
return;
@@ -342,6 +352,36 @@ memalloc_heap_track(uint16_t max_nframe, void* ptr, size_t size, PyMemAllocatorD
342352
memalloc_yield_guard();
343353
}
344354

355+
PyObject*
356+
memalloc_sample_to_tuple(traceback_t* tb, bool is_live)
357+
{
358+
PyObject* tb_and_info = PyTuple_New(4);
359+
if (tb_and_info == NULL) {
360+
return NULL;
361+
}
362+
363+
size_t in_use_size;
364+
size_t alloc_size;
365+
366+
if (is_live) {
367+
/* alloc_size tracks new allocations since the last heap snapshot. Once
368+
* we report it (tb->reported == true), we set the value to 0 to avoid
369+
* double-counting allocations across multiple snapshots. */
370+
in_use_size = tb->size;
371+
alloc_size = tb->reported ? 0 : tb->size;
372+
} else {
373+
in_use_size = 0;
374+
alloc_size = tb->size;
375+
}
376+
377+
PyTuple_SET_ITEM(tb_and_info, 0, traceback_to_tuple(tb));
378+
PyTuple_SET_ITEM(tb_and_info, 1, PyLong_FromSize_t(in_use_size));
379+
PyTuple_SET_ITEM(tb_and_info, 2, PyLong_FromSize_t(alloc_size));
380+
PyTuple_SET_ITEM(tb_and_info, 3, PyLong_FromSize_t(tb->count));
381+
382+
return tb_and_info;
383+
}
384+
345385
PyObject*
346386
memalloc_heap(void)
347387
{
@@ -351,7 +391,57 @@ memalloc_heap(void)
351391
* New allocations will go into the secondary freezer.allocs_m map and allocations
352392
* tracked in allocs_m which are freed will be added to a list to be removed when
353393
* the profiler is thawed. */
354-
PyObject* heap_list = memalloc_heap_map_export(global_heap_tracker.allocs_m);
394+
395+
/* Calculate total number of samples: live + freed */
396+
size_t live_count = memalloc_heap_map_size(global_heap_tracker.allocs_m);
397+
size_t freed_count = global_heap_tracker.unreported_samples.count;
398+
size_t total_count = live_count + freed_count;
399+
400+
PyObject* heap_list = PyList_New(total_count);
401+
if (heap_list == NULL) {
402+
heap_tracker_thaw(&global_heap_tracker);
403+
return NULL;
404+
}
405+
406+
int list_index = 0;
407+
408+
/* First, iterate over live samples using the new iterator API */
409+
memalloc_heap_map_iter_t* it = memalloc_heap_map_iter_new(global_heap_tracker.allocs_m);
410+
// TODO: handle NULL return
411+
412+
void* key;
413+
traceback_t* tb;
414+
415+
while (memalloc_heap_map_iter_next(it, &key, &tb)) {
416+
PyObject* tb_and_info = memalloc_sample_to_tuple(tb, true);
417+
418+
PyList_SET_ITEM(heap_list, list_index, tb_and_info);
419+
list_index++;
420+
421+
/* Mark as reported */
422+
tb->reported = true;
423+
}
424+
425+
memalloc_heap_map_iter_delete(it);
426+
427+
/* Second, iterate over freed samples from unreported_samples */
428+
for (size_t i = 0; i < global_heap_tracker.unreported_samples.count; i++) {
429+
traceback_t* tb = global_heap_tracker.unreported_samples.tab[i];
430+
431+
PyObject* tb_and_info = memalloc_sample_to_tuple(tb, false);
432+
433+
PyList_SET_ITEM(heap_list, list_index, tb_and_info);
434+
list_index++;
435+
}
436+
437+
/* Free all tracebacks in unreported_samples after reporting them */
438+
for (size_t i = 0; i < global_heap_tracker.unreported_samples.count; i++) {
439+
if (global_heap_tracker.unreported_samples.tab[i] != NULL) {
440+
traceback_free(global_heap_tracker.unreported_samples.tab[i]);
441+
}
442+
}
443+
/* Reset the count to 0 so we can reuse the memory */
444+
global_heap_tracker.unreported_samples.count = 0;
355445

356446
heap_tracker_thaw(&global_heap_tracker);
357447

ddtrace/profiling/collector/_memalloc_heap_map.c

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ typedef struct memalloc_heap_map_t
8282
HeapSamples map;
8383
} memalloc_heap_map_t;
8484

85+
typedef struct memalloc_heap_map_iter_t
86+
{
87+
HeapSamples_CIter iter;
88+
bool started;
89+
} memalloc_heap_map_iter_t;
90+
8591
memalloc_heap_map_t*
8692
memalloc_heap_map_new()
8793
{
@@ -178,3 +184,43 @@ memalloc_heap_map_delete(memalloc_heap_map_t* m)
178184
HeapSamples_destroy(&m->map);
179185
free(m);
180186
}
187+
188+
memalloc_heap_map_iter_t*
189+
memalloc_heap_map_iter_new(memalloc_heap_map_t* m)
190+
{
191+
memalloc_heap_map_iter_t* it = malloc(sizeof(memalloc_heap_map_iter_t));
192+
if (it) {
193+
it->iter = HeapSamples_citer(&m->map);
194+
it->started = false;
195+
}
196+
return it;
197+
}
198+
199+
bool
200+
memalloc_heap_map_iter_next(memalloc_heap_map_iter_t* it, void** key, traceback_t** tb)
201+
{
202+
const HeapSamples_Entry* e;
203+
204+
if (!it->started) {
205+
e = HeapSamples_CIter_get(&it->iter);
206+
it->started = true;
207+
} else {
208+
e = HeapSamples_CIter_next(&it->iter);
209+
}
210+
211+
if (e != NULL) {
212+
*key = e->key;
213+
*tb = e->val;
214+
return true;
215+
}
216+
217+
return false;
218+
}
219+
220+
void
221+
memalloc_heap_map_iter_delete(memalloc_heap_map_iter_t* it)
222+
{
223+
if (it) {
224+
free(it);
225+
}
226+
}

ddtrace/profiling/collector/_memalloc_heap_map.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
*/
1212
typedef struct memalloc_heap_map_t memalloc_heap_map_t;
1313

14+
typedef struct memalloc_heap_map_iter_t memalloc_heap_map_iter_t;
15+
1416
/* Construct an empty map */
1517
memalloc_heap_map_t*
1618
memalloc_heap_map_new();
@@ -35,6 +37,19 @@ memalloc_heap_map_remove(memalloc_heap_map_t* m, void* key);
3537
PyObject*
3638
memalloc_heap_map_export(memalloc_heap_map_t* m);
3739

40+
/* Create a new iterator for the heap map */
41+
memalloc_heap_map_iter_t*
42+
memalloc_heap_map_iter_new(memalloc_heap_map_t* m);
43+
44+
/* Get the next key-value pair from the iterator. Returns true if a pair was found,
45+
* false if the iterator is exhausted */
46+
bool
47+
memalloc_heap_map_iter_next(memalloc_heap_map_iter_t* it, void** key, traceback_t** tb);
48+
49+
/* Delete the iterator */
50+
void
51+
memalloc_heap_map_iter_delete(memalloc_heap_map_iter_t* it);
52+
3853
/* Copy the contents of src into dst, removing the items from src */
3954
void
4055
memalloc_heap_map_destructive_copy(memalloc_heap_map_t* dst, memalloc_heap_map_t* src);

ddtrace/profiling/collector/_memalloc_tb.c

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,6 @@ traceback_free(traceback_t* tb)
160160
Py_DECREF(tb->frames[nframe].filename);
161161
Py_DECREF(tb->frames[nframe].name);
162162
}
163-
memalloc_debug_gil_release();
164163
PyMem_RawFree(tb);
165164
}
166165

@@ -250,7 +249,7 @@ memalloc_frame_to_traceback(PyFrameObject* pyframe, uint16_t max_nframe)
250249
}
251250

252251
traceback_t*
253-
memalloc_get_traceback(uint16_t max_nframe, void* ptr, size_t size, PyMemAllocatorDomain domain)
252+
memalloc_get_traceback(uint16_t max_nframe, void* ptr, size_t size, PyMemAllocatorDomain domain, size_t weighted_size)
254253
{
255254
PyThreadState* tstate = PyThreadState_Get();
256255

@@ -271,13 +270,18 @@ memalloc_get_traceback(uint16_t max_nframe, void* ptr, size_t size, PyMemAllocat
271270
if (traceback == NULL)
272271
return NULL;
273272

274-
traceback->size = size;
273+
traceback->size = weighted_size;
275274
traceback->ptr = ptr;
276275

277276
traceback->thread_id = PyThread_get_thread_ident();
278277

279278
traceback->domain = domain;
280279

280+
traceback->reported = false;
281+
282+
double scaled_count = ((double)weighted_size) / ((double)size);
283+
traceback->count = (size_t)scaled_count;
284+
281285
return traceback;
282286
}
283287

@@ -316,9 +320,8 @@ traceback_to_tuple(traceback_t* tb)
316320
PyTuple_SET_ITEM(stack, nframe, frame_tuple);
317321
}
318322

319-
PyObject* tuple = PyTuple_New(3);
323+
PyObject* tuple = PyTuple_New(2);
320324
PyTuple_SET_ITEM(tuple, 0, stack);
321-
PyTuple_SET_ITEM(tuple, 1, PyLong_FromUnsignedLong(tb->total_nframe));
322-
PyTuple_SET_ITEM(tuple, 2, PyLong_FromUnsignedLong(tb->thread_id));
325+
PyTuple_SET_ITEM(tuple, 1, PyLong_FromUnsignedLong(tb->thread_id));
323326
return tuple;
324327
}

ddtrace/profiling/collector/_memalloc_tb.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ typedef struct
3737
PyMemAllocatorDomain domain;
3838
/* Thread ID */
3939
unsigned long thread_id;
40+
/* True if this sample has been reported previously */
41+
bool reported;
42+
/* Count of allocations this sample represents (for scaling) */
43+
size_t count;
4044
/* List of frames, top frame first */
4145
frame_t frames[1];
4246
} traceback_t;
@@ -56,7 +60,11 @@ void
5660
traceback_free(traceback_t* tb);
5761

5862
traceback_t*
59-
memalloc_get_traceback(uint16_t max_nframe, void* ptr, size_t size, PyMemAllocatorDomain domain);
63+
memalloc_get_traceback(uint16_t max_nframe,
64+
void* ptr,
65+
size_t size,
66+
PyMemAllocatorDomain domain,
67+
size_t allocated_memory);
6068

6169
PyObject*
6270
traceback_to_tuple(traceback_t* tb);

0 commit comments

Comments
 (0)