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.")
|
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.
|
# Define platform-specific macros.
|
||||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||||
DUSK_SDL2
|
DUSK_SDL2
|
||||||
@@ -45,4 +56,5 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|||||||
DUSK_TIME_DYNAMIC
|
DUSK_TIME_DYNAMIC
|
||||||
DUSK_NETWORK_IPV6
|
DUSK_NETWORK_IPV6
|
||||||
DUSK_THREAD_PTHREAD
|
DUSK_THREAD_PTHREAD
|
||||||
|
DUSK_TRACK_MEMORY
|
||||||
)
|
)
|
||||||
@@ -108,5 +108,7 @@ errorret_t engineDispose(void) {
|
|||||||
errorChain(assetDispose());
|
errorChain(assetDispose());
|
||||||
perfDispose();
|
perfDispose();
|
||||||
|
|
||||||
|
memoryCheckUnfreed();
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,27 @@
|
|||||||
#include "memory.h"
|
#include "memory.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/math.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) {
|
size_t memoryGetAllocatedCount(void) {
|
||||||
return MEMORY_POINTERS_IN_USE;
|
return MEMORY_POINTERS_IN_USE;
|
||||||
@@ -18,6 +39,32 @@ void memoryTrack(void *ptr) {
|
|||||||
MEMORY_POINTERS_IN_USE++;
|
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) {
|
void * memoryAllocate(const size_t size) {
|
||||||
assertTrue(size > 0, "Cannot allocate 0 bytes of memory.");
|
assertTrue(size > 0, "Cannot allocate 0 bytes of memory.");
|
||||||
void *ptr = malloc(size);
|
void *ptr = malloc(size);
|
||||||
@@ -26,6 +73,8 @@ void * memoryAllocate(const size_t size) {
|
|||||||
return ptr;
|
return ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
void * memoryAlign(size_t alignment, size_t size) {
|
void * memoryAlign(size_t alignment, size_t size) {
|
||||||
assertTrue(alignment > 0, "Alignment must be greater than 0.");
|
assertTrue(alignment > 0, "Alignment must be greater than 0.");
|
||||||
assertTrue(size > 0, "Cannot allocate 0 bytes of memory.");
|
assertTrue(size > 0, "Cannot allocate 0 bytes of memory.");
|
||||||
@@ -35,8 +84,65 @@ void * memoryAlign(size_t alignment, size_t size) {
|
|||||||
return ptr;
|
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) {
|
void memoryFree(void *ptr) {
|
||||||
assertNotNull(ptr, "Cannot free NULL memory.");
|
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);
|
free(ptr);
|
||||||
MEMORY_POINTERS_IN_USE--;
|
MEMORY_POINTERS_IN_USE--;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,42 @@ size_t memoryGetAllocatedCount(void);
|
|||||||
*/
|
*/
|
||||||
void memoryTrack(void *ptr);
|
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.
|
* Allocates memory.
|
||||||
*
|
*
|
||||||
@@ -32,6 +68,8 @@ void memoryTrack(void *ptr);
|
|||||||
*/
|
*/
|
||||||
void * memoryAllocate(const size_t size);
|
void * memoryAllocate(const size_t size);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allocates aligned memory.
|
* Allocates aligned memory.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user