diff --git a/cmake/targets/linux.cmake b/cmake/targets/linux.cmake index 1d67e95b..ae00ad3a 100644 --- a/cmake/targets/linux.cmake +++ b/cmake/targets/linux.cmake @@ -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 ) \ No newline at end of file diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index fa640248..2c17ccb5 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -108,5 +108,7 @@ errorret_t engineDispose(void) { errorChain(assetDispose()); perfDispose(); + memoryCheckUnfreed(); + errorOk(); } diff --git a/src/dusk/util/memory.c b/src/dusk/util/memory.c index 21bcfb64..0f5edb3d 100644 --- a/src/dusk/util/memory.c +++ b/src/dusk/util/memory.c @@ -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--; } diff --git a/src/dusk/util/memory.h b/src/dusk/util/memory.h index f03287e4..94171a32 100644 --- a/src/dusk/util/memory.h +++ b/src/dusk/util/memory.h @@ -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. *