Add DUSK_TRACK_MEMORY allocation tracker and memoryCheckUnfreed report
memoryAllocate() becomes a macro capturing __FILE__/__LINE__ into a fixed-size tracking table when DUSK_TRACK_MEMORY is enabled (Linux only for now), so a leak surviving to shutdown can be traced back to its call site. Replaces engineDispose's hard assertTrue with memoryCheckUnfreed(), which reports unfreed allocations (with call sites when tracking is on, just a count otherwise) instead of aborting. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -28,6 +28,17 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
|
||||
set(DUSK_BACKTRACE ON CACHE BOOL "Enable backtrace support for assert failures.")
|
||||
|
||||
# Records the file/line of every memoryAllocate() call into a fixed-size
|
||||
# tracking table (removed again on memoryFree()) so a leak still present at
|
||||
# engineDispose()'s memoryGetAllocatedCount() check can be traced back to
|
||||
# where it was allocated. Linux only for now.
|
||||
set(DUSK_TRACK_MEMORY OFF CACHE BOOL "Track memoryAllocate() call sites to help find memory leaks.")
|
||||
if(DUSK_TRACK_MEMORY)
|
||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_TRACK_MEMORY
|
||||
)
|
||||
endif()
|
||||
|
||||
# Define platform-specific macros.
|
||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_SDL2
|
||||
@@ -45,4 +56,5 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_TIME_DYNAMIC
|
||||
DUSK_NETWORK_IPV6
|
||||
DUSK_THREAD_PTHREAD
|
||||
DUSK_TRACK_MEMORY
|
||||
)
|
||||
@@ -108,5 +108,7 @@ errorret_t engineDispose(void) {
|
||||
errorChain(assetDispose());
|
||||
perfDispose();
|
||||
|
||||
memoryCheckUnfreed();
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,27 @@
|
||||
#include "memory.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/math.h"
|
||||
#include "log/log.h"
|
||||
|
||||
#ifdef DUSK_TRACK_MEMORY
|
||||
|
||||
// Fixed-size, pre-allocated table of live memoryAllocate() calls, keyed by
|
||||
// pointer - deliberately never grows (backed by static storage, not
|
||||
// malloc) so the tracker itself can never allocate, recurse or leak. Raise
|
||||
// this if it's ever exhausted (see the assert in memoryAllocateTracked).
|
||||
#define MEMORY_TRACK_MAX 16384
|
||||
|
||||
typedef struct {
|
||||
void *ptr;
|
||||
const char_t *file;
|
||||
int_t line;
|
||||
size_t size;
|
||||
} memorytrackentry_t;
|
||||
|
||||
static memorytrackentry_t MEMORY_TRACK_ENTRIES[MEMORY_TRACK_MAX];
|
||||
static size_t MEMORY_TRACK_COUNT = 0;
|
||||
|
||||
#endif
|
||||
|
||||
size_t memoryGetAllocatedCount(void) {
|
||||
return MEMORY_POINTERS_IN_USE;
|
||||
@@ -18,6 +39,32 @@ void memoryTrack(void *ptr) {
|
||||
MEMORY_POINTERS_IN_USE++;
|
||||
}
|
||||
|
||||
#ifdef DUSK_TRACK_MEMORY
|
||||
|
||||
void * memoryAllocateTracked(
|
||||
const size_t size, const char_t *file, const int_t line
|
||||
) {
|
||||
assertTrue(size > 0, "Cannot allocate 0 bytes of memory.");
|
||||
void *ptr = malloc(size);
|
||||
assertNotNull(ptr, "Memory allocation failed.");
|
||||
memoryTrack(ptr);
|
||||
|
||||
assertTrue(
|
||||
MEMORY_TRACK_COUNT < MEMORY_TRACK_MAX,
|
||||
"Memory tracking table is full - raise MEMORY_TRACK_MAX."
|
||||
);
|
||||
MEMORY_TRACK_ENTRIES[MEMORY_TRACK_COUNT++] = (memorytrackentry_t){
|
||||
.ptr = ptr,
|
||||
.file = file,
|
||||
.line = line,
|
||||
.size = size
|
||||
};
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void * memoryAllocate(const size_t size) {
|
||||
assertTrue(size > 0, "Cannot allocate 0 bytes of memory.");
|
||||
void *ptr = malloc(size);
|
||||
@@ -26,6 +73,8 @@ void * memoryAllocate(const size_t size) {
|
||||
return ptr;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void * memoryAlign(size_t alignment, size_t size) {
|
||||
assertTrue(alignment > 0, "Alignment must be greater than 0.");
|
||||
assertTrue(size > 0, "Cannot allocate 0 bytes of memory.");
|
||||
@@ -35,8 +84,65 @@ void * memoryAlign(size_t alignment, size_t size) {
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void memoryCheckUnfreed(void) {
|
||||
size_t count = memoryGetAllocatedCount();
|
||||
if(count == 0) {
|
||||
logDebug("memoryCheckUnfreed: no unfreed allocations.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef DUSK_TRACK_MEMORY
|
||||
logDebug("memoryCheckUnfreed: %zu unfreed allocation(s):\n", count);
|
||||
|
||||
size_t trackedBytes = 0;
|
||||
for(size_t i = 0; i < MEMORY_TRACK_COUNT; i++) {
|
||||
memorytrackentry_t *entry = &MEMORY_TRACK_ENTRIES[i];
|
||||
logDebug(
|
||||
" - %zu bytes allocated at %s:%d\n",
|
||||
entry->size, entry->file, entry->line
|
||||
);
|
||||
trackedBytes += entry->size;
|
||||
}
|
||||
logDebug(" (%zu bytes across %zu tracked allocations)\n",
|
||||
trackedBytes, MEMORY_TRACK_COUNT
|
||||
);
|
||||
|
||||
// memoryAlign()/memoryTrack() callers aren't recorded into
|
||||
// MEMORY_TRACK_ENTRIES yet, so the tracked count can fall short of the
|
||||
// real total - call that out rather than silently under-reporting.
|
||||
size_t untracked = count - MEMORY_TRACK_COUNT;
|
||||
if(untracked > 0) {
|
||||
logDebug(
|
||||
" + %zu more unfreed allocation(s) with no call site recorded "
|
||||
"(from memoryAlign()/memoryTrack())\n",
|
||||
untracked
|
||||
);
|
||||
}
|
||||
#else
|
||||
logDebug(
|
||||
"memoryCheckUnfreed: %zu unfreed allocation(s) - rebuild with "
|
||||
"DUSK_TRACK_MEMORY=ON for call site details.\n",
|
||||
count
|
||||
);
|
||||
#endif
|
||||
}
|
||||
|
||||
void memoryFree(void *ptr) {
|
||||
assertNotNull(ptr, "Cannot free NULL memory.");
|
||||
|
||||
#ifdef DUSK_TRACK_MEMORY
|
||||
// Only pointers from memoryAllocate() are in here (memoryAlign/
|
||||
// memoryTrack callers aren't tracked yet), so a miss here is expected,
|
||||
// not an error - just leave the table alone.
|
||||
for(size_t i = 0; i < MEMORY_TRACK_COUNT; i++) {
|
||||
if(MEMORY_TRACK_ENTRIES[i].ptr == ptr) {
|
||||
// Swap-remove - order doesn't matter for leak tracking.
|
||||
MEMORY_TRACK_ENTRIES[i] = MEMORY_TRACK_ENTRIES[--MEMORY_TRACK_COUNT];
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
free(ptr);
|
||||
MEMORY_POINTERS_IN_USE--;
|
||||
}
|
||||
|
||||
+40
-2
@@ -19,19 +19,57 @@ size_t memoryGetAllocatedCount(void);
|
||||
|
||||
/**
|
||||
* Track a pointer that was malloc'd outside of the dusk engine.
|
||||
*
|
||||
*
|
||||
* @param ptr The pointer to track.
|
||||
*/
|
||||
void memoryTrack(void *ptr);
|
||||
|
||||
/**
|
||||
* Reports (via logDebug) whether any memoryAllocate()-family allocation is
|
||||
* still outstanding - intended to be called once, near shutdown, after
|
||||
* every subsystem has had a chance to dispose (see engineDispose). With
|
||||
* DUSK_TRACK_MEMORY defined, prints exactly where (file:line) and how many
|
||||
* bytes each still-unfreed allocation came from; without it, there's
|
||||
* nowhere to report a call site from, so it only prints how many blocks
|
||||
* remain outstanding.
|
||||
*/
|
||||
void memoryCheckUnfreed(void);
|
||||
|
||||
#ifdef DUSK_TRACK_MEMORY
|
||||
|
||||
/**
|
||||
* Allocates memory, recording the call site (file/line) into a small
|
||||
* fixed-size tracking table so a block still allocated at shutdown (see
|
||||
* engineDispose's memoryGetAllocatedCount() check) can be traced back to
|
||||
* where it came from. memoryFree removes the matching entry again once the
|
||||
* pointer is freed. Only called via the memoryAllocate(size) macro below
|
||||
* when DUSK_TRACK_MEMORY is defined - callers keep writing
|
||||
* memoryAllocate(size) exactly as without it.
|
||||
*
|
||||
* @param size The size of the memory to allocate.
|
||||
* @param file Call site file name (__FILE__).
|
||||
* @param line Call site line number (__LINE__).
|
||||
* @return The allocated memory.
|
||||
*/
|
||||
void * memoryAllocateTracked(
|
||||
const size_t size, const char_t *file, const int_t line
|
||||
);
|
||||
|
||||
#define memoryAllocate(size) \
|
||||
memoryAllocateTracked((size), __FILE__, __LINE__)
|
||||
|
||||
#else
|
||||
|
||||
/**
|
||||
* Allocates memory.
|
||||
*
|
||||
*
|
||||
* @param size The size of the memory to allocate.
|
||||
* @return The allocated memory.
|
||||
*/
|
||||
void * memoryAllocate(const size_t size);
|
||||
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Allocates aligned memory.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user