From a793941357f358a441411a02fed962e9bbbcd4cf Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Thu, 26 Mar 2026 16:58:40 -0500 Subject: [PATCH] Individual app memory tracking added. --- apps/clock/clock.c | 1 + apps/cpanel/cpanel.c | 1 + apps/dvxdemo/dvxdemo.c | 1 + apps/imgview/imgview.c | 1 + apps/notepad/notepad.c | 1 + apps/progman/progman.c | 1 + core/dvxMem.h | 48 ++++++++++ core/dvxWidgetPlugin.h | 2 + core/platform/dvxPlatform.h | 32 +++++++ core/platform/dvxPlatformDos.c | 169 +++++++++++++++++++++++++++++++++ core/widgetEvent.c | 22 ++++- shell/shellApp.c | 6 ++ shell/shellInfo.c | 1 + shell/shellMain.c | 5 + taskmgr/shellTaskMgr.c | 32 +++++-- 15 files changed, 313 insertions(+), 10 deletions(-) create mode 100644 core/dvxMem.h diff --git a/apps/clock/clock.c b/apps/clock/clock.c index 0327cba..b694e5a 100644 --- a/apps/clock/clock.c +++ b/apps/clock/clock.c @@ -30,6 +30,7 @@ #include #include #include +#include "dvxMem.h" // ============================================================ // Module state diff --git a/apps/cpanel/cpanel.c b/apps/cpanel/cpanel.c index 1ee0ee7..4d34f73 100644 --- a/apps/cpanel/cpanel.c +++ b/apps/cpanel/cpanel.c @@ -33,6 +33,7 @@ #include #include #include +#include "dvxMem.h" // ============================================================ // Constants diff --git a/apps/dvxdemo/dvxdemo.c b/apps/dvxdemo/dvxdemo.c index fd8ad31..d16fe38 100644 --- a/apps/dvxdemo/dvxdemo.c +++ b/apps/dvxdemo/dvxdemo.c @@ -50,6 +50,7 @@ #include #include #include +#include "dvxMem.h" // ============================================================ // Menu command IDs diff --git a/apps/imgview/imgview.c b/apps/imgview/imgview.c index c79fb7e..9052c78 100644 --- a/apps/imgview/imgview.c +++ b/apps/imgview/imgview.c @@ -14,6 +14,7 @@ #include #include #include +#include "dvxMem.h" // ============================================================ // App descriptor diff --git a/apps/notepad/notepad.c b/apps/notepad/notepad.c index b325c67..bb9304e 100644 --- a/apps/notepad/notepad.c +++ b/apps/notepad/notepad.c @@ -23,6 +23,7 @@ #include #include #include +#include "dvxMem.h" // ============================================================ // Constants diff --git a/apps/progman/progman.c b/apps/progman/progman.c index 28304c7..b725897 100644 --- a/apps/progman/progman.c +++ b/apps/progman/progman.c @@ -39,6 +39,7 @@ #include #include #include +#include "dvxMem.h" // ============================================================ // Constants diff --git a/core/dvxMem.h b/core/dvxMem.h new file mode 100644 index 0000000..53b033b --- /dev/null +++ b/core/dvxMem.h @@ -0,0 +1,48 @@ +// dvxMem.h -- Per-app memory tracking for DVX +// +// REQUIRED: Every .c file compiled into a DXE (library, widget, shell, +// or app) MUST include this header AFTER all system includes. This +// ensures every malloc/free/calloc/realloc call goes through the +// tracking wrappers, maintaining pointer consistency. +// +// The implementation (dvxPlatformDos.c) does NOT include this header, +// so its dvxMalloc/dvxFree/dvxCalloc/dvxRealloc call the real libc +// functions with no recursion. +// +// How it works: +// 1. dvxMalloc prepends a 16-byte header (magic, appId, size) to +// each allocation and returns a pointer past the header. +// 2. dvxFree checks the magic value at ptr-16. If it matches, the +// header is valid and the allocation is tracked. If not, the +// pointer came from code outside DVX (libc internals, loader) +// and is passed through to the real free() unchanged. +// 3. The appId in the header comes from *dvxMemAppIdPtr, which the +// shell points at ctx->currentAppId. +// +// The magic check makes cross-boundary frees safe: if DXE code frees +// a pointer allocated by libc (e.g. from strdup, stb_ds internals), +// dvxFree detects the missing header and falls through to real free. + +#ifndef DVX_MEM_H +#define DVX_MEM_H + +#include + +extern int32_t *dvxMemAppIdPtr; + +void *dvxMalloc(size_t size); +void *dvxCalloc(size_t nmemb, size_t size); +void *dvxRealloc(void *ptr, size_t size); +void dvxFree(void *ptr); +void dvxMemSnapshotLoad(int32_t appId); +uint32_t dvxMemGetAppUsage(int32_t appId); +void dvxMemResetApp(int32_t appId); + +// Redirect standard allocator calls to tracking wrappers. +// This MUST appear after so the real prototypes exist. +#define malloc(s) dvxMalloc(s) +#define calloc(n, s) dvxCalloc((n), (s)) +#define realloc(p, s) dvxRealloc((p), (s)) +#define free(p) dvxFree(p) + +#endif // DVX_MEM_H diff --git a/core/dvxWidgetPlugin.h b/core/dvxWidgetPlugin.h index c4e24bf..c77dd20 100644 --- a/core/dvxWidgetPlugin.h +++ b/core/dvxWidgetPlugin.h @@ -22,6 +22,8 @@ #include #include +#include "dvxMem.h" + // ============================================================ // Widget class table // ============================================================ diff --git a/core/platform/dvxPlatform.h b/core/platform/dvxPlatform.h index 7b64793..2a38299 100644 --- a/core/platform/dvxPlatform.h +++ b/core/platform/dvxPlatform.h @@ -213,6 +213,38 @@ const char *platformValidateFilename(const char *name); // and free physical memory in kilobytes. Returns false if unavailable. bool platformGetMemoryInfo(uint32_t *totalKb, uint32_t *freeKb); +// ============================================================ +// Per-app memory tracking +// ============================================================ +// +// Wraps malloc/free/calloc/realloc with a small header per allocation +// that records the owning app ID and size. This lets DVX report per-app +// memory usage in the Task Manager and detect leaks at app termination. +// +// The allocator reads *dvxMemAppIdPtr to determine which app to charge. +// The shell sets this pointer to &ctx->currentAppId during init. +// Tracked allocations carry a 16-byte header (magic + appId + size + pad). +// Calls to dvxFree on non-tracked pointers (magic mismatch) fall through +// to the real free() safely. + +// Per-app memory tracking (see dvxMem.h for the full API). +// These are declared here for the platform implementation; DXE +// code should include dvxMem.h instead. +extern int32_t *dvxMemAppIdPtr; + +// Take a DPMI free-memory snapshot when an app is loaded. +// Call BEFORE the app's DXE is opened so the snapshot captures +// the free memory before the app allocates anything. +void dvxMemSnapshotLoad(int32_t appId); + +// Return estimated memory usage for an app (bytes). +// Computed as the difference between the snapshot at load time +// and the current DPMI free memory. Coarse but safe. +uint32_t dvxMemGetAppUsage(int32_t appId); + +// Clear the snapshot for an app (call when reaping/killing). +void dvxMemResetApp(int32_t appId); + // Create a directory and all parent directories (like mkdir -p). // Returns 0 on success, -1 on failure. Existing directories are not // an error. diff --git a/core/platform/dvxPlatformDos.c b/core/platform/dvxPlatformDos.c index ec358f6..e6de29e 100644 --- a/core/platform/dvxPlatformDos.c +++ b/core/platform/dvxPlatformDos.c @@ -1112,6 +1112,167 @@ bool platformGetMemoryInfo(uint32_t *totalKb, uint32_t *freeKb) { } +// ============================================================ +// Per-app memory tracking (header-based) +// ============================================================ +// +// Every DXE .c file includes dvxMem.h which #defines malloc/free/ +// calloc/realloc to dvxMalloc/dvxFree/dvxCalloc/dvxRealloc. This +// file does NOT include dvxMem.h, so calls to malloc/free here go +// directly to libc with no recursion. +// +// Each tracked allocation has a 16-byte header prepended. dvxFree +// checks the magic before adjusting the pointer -- if it doesn't +// match (pointer came from libc internals or loader code), it +// falls through to the real free() unchanged. + +#define DVX_ALLOC_MAGIC 0xDEADBEEFUL + +typedef struct { + uint32_t magic; + int32_t appId; + uint32_t size; + uint32_t pad; +} DvxAllocHeaderT; + +int32_t *dvxMemAppIdPtr = NULL; +static uint32_t *sAppMemUsed = NULL; +static int32_t sAppMemCap = 0; + + +static void dvxMemGrow(int32_t appId) { + if (appId < sAppMemCap) { + return; + } + + int32_t newCap = appId + 16; + uint32_t *newArr = (uint32_t *)realloc(sAppMemUsed, newCap * sizeof(uint32_t)); + + if (!newArr) { + return; + } + + memset(newArr + sAppMemCap, 0, (newCap - sAppMemCap) * sizeof(uint32_t)); + sAppMemUsed = newArr; + sAppMemCap = newCap; +} + + +void *dvxMalloc(size_t size) { + int32_t appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0; + DvxAllocHeaderT *hdr = (DvxAllocHeaderT *)malloc(sizeof(DvxAllocHeaderT) + size); + + if (!hdr) { + return NULL; + } + + hdr->magic = DVX_ALLOC_MAGIC; + hdr->appId = appId; + hdr->size = (uint32_t)size; + hdr->pad = 0; + + if (appId >= 0) { + dvxMemGrow(appId); + if (appId < sAppMemCap) { sAppMemUsed[appId] += (uint32_t)size; } + } + + return hdr + 1; +} + + +void *dvxCalloc(size_t nmemb, size_t size) { + size_t total = nmemb * size; + void *ptr = dvxMalloc(total); + + if (ptr) { + memset(ptr, 0, total); + } + + return ptr; +} + + +void dvxFree(void *ptr) { + if (!ptr) { + return; + } + + DvxAllocHeaderT *hdr = (DvxAllocHeaderT *)ptr - 1; + + if (hdr->magic != DVX_ALLOC_MAGIC) { + // Not a tracked allocation (libc, stb_ds, loader) -- pass through + free(ptr); + return; + } + + int32_t appId = hdr->appId; + + if (appId >= 0 && appId < sAppMemCap) { + sAppMemUsed[appId] -= hdr->size; + } + + hdr->magic = 0; + free(hdr); +} + + +void *dvxRealloc(void *ptr, size_t size) { + if (!ptr) { + return dvxMalloc(size); + } + + if (size == 0) { + dvxFree(ptr); + return NULL; + } + + DvxAllocHeaderT *hdr = (DvxAllocHeaderT *)ptr - 1; + + if (hdr->magic != DVX_ALLOC_MAGIC) { + // Not tracked -- pass through + return realloc(ptr, size); + } + + int32_t appId = hdr->appId; + uint32_t oldSize = hdr->size; + + DvxAllocHeaderT *newHdr = (DvxAllocHeaderT *)realloc(hdr, sizeof(DvxAllocHeaderT) + size); + + if (!newHdr) { + return NULL; + } + + if (appId >= 0 && appId < sAppMemCap) { + sAppMemUsed[appId] -= oldSize; + sAppMemUsed[appId] += (uint32_t)size; + } + + newHdr->size = (uint32_t)size; + return newHdr + 1; +} + + +void dvxMemSnapshotLoad(int32_t appId) { + (void)appId; +} + + +uint32_t dvxMemGetAppUsage(int32_t appId) { + if (appId < 0 || appId >= sAppMemCap) { + return 0; + } + + return sAppMemUsed[appId]; +} + + +void dvxMemResetApp(int32_t appId) { + if (appId >= 0 && appId < sAppMemCap) { + sAppMemUsed[appId] = 0; + } +} + + // ============================================================ // platformMkdirRecursive // ============================================================ @@ -2161,6 +2322,14 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(platformChdir) DXE_EXPORT(platformFlushRect) DXE_EXPORT(platformGetMemoryInfo) + DXE_EXPORT(dvxMemAppIdPtr) + DXE_EXPORT(dvxMalloc) + DXE_EXPORT(dvxCalloc) + DXE_EXPORT(dvxRealloc) + DXE_EXPORT(dvxFree) + DXE_EXPORT(dvxMemSnapshotLoad) + DXE_EXPORT(dvxMemGetAppUsage) + DXE_EXPORT(dvxMemResetApp) DXE_EXPORT(platformGetSystemInfo) DXE_EXPORT(platformInit) DXE_EXPORT(platformInstallCrashHandler) diff --git a/core/widgetEvent.c b/core/widgetEvent.c index 50da465..f4a78d2 100644 --- a/core/widgetEvent.c +++ b/core/widgetEvent.c @@ -172,8 +172,14 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { return; } - // Dispatch to per-widget onKey handler via vtable + // Attribute allocations during event handling to the owning app + AppContextT *ctx = (AppContextT *)root->userData; + int32_t prevAppId = ctx->currentAppId; + ctx->currentAppId = win->appId; + wclsOnKey(focus, key, mod); + + ctx->currentAppId = prevAppId; } @@ -195,6 +201,8 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { // are also in content-buffer space (set during layout), so no // coordinate transform is needed for hit testing. +static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons); + void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { WidgetT *root = win->widgetRoot; sClosedPopup = NULL; @@ -203,6 +211,18 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { return; } + // Attribute allocations during event handling to the owning app + AppContextT *ctx = (AppContextT *)root->userData; + int32_t prevAppId = ctx->currentAppId; + ctx->currentAppId = win->appId; + + widgetOnMouseInner(win, root, x, y, buttons); + + ctx->currentAppId = prevAppId; +} + + +static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons) { // Close popups from other windows if (sOpenPopup && sOpenPopup->window != win) { wclsClosePopup(sOpenPopup); diff --git a/shell/shellApp.c b/shell/shellApp.c index 4693048..86426d2 100644 --- a/shell/shellApp.c +++ b/shell/shellApp.c @@ -14,6 +14,7 @@ #include #include #include +#include "dvxMem.h" // ============================================================ // Module state @@ -260,6 +261,7 @@ void shellForceKillApp(AppContextT *ctx, ShellAppT *app) { } cleanupTempFile(app); + dvxMemResetApp(app->appId); app->state = AppStateFreeE; dvxLog("Shell: force-killed app '%s'", app->name); } @@ -332,6 +334,9 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) { loadPath = tempPath; } + // Snapshot free memory before loading so we can estimate app usage + dvxMemSnapshotLoad(id); + // Load the DXE void *handle = dlopen(loadPath, RTLD_GLOBAL); @@ -506,6 +511,7 @@ void shellReapApp(AppContextT *ctx, ShellAppT *app) { } cleanupTempFile(app); + dvxMemResetApp(app->appId); dvxLog("Shell: reaped app '%s'", app->name); app->state = AppStateFreeE; } diff --git a/shell/shellInfo.c b/shell/shellInfo.c index aadca24..74911e0 100644 --- a/shell/shellInfo.c +++ b/shell/shellInfo.c @@ -10,6 +10,7 @@ #include "dvxPlatform.h" #include +#include "dvxMem.h" // ============================================================ // Module state diff --git a/shell/shellMain.c b/shell/shellMain.c index 453b4b1..f9f240c 100644 --- a/shell/shellMain.c +++ b/shell/shellMain.c @@ -37,6 +37,7 @@ #include #include #include +#include "dvxMem.h" // ============================================================ // Module state @@ -260,6 +261,10 @@ int shellMain(int argc, char *argv[]) { // Initialize app slot table shellAppInit(); + // Point the memory tracker at currentAppId so allocations are + // attributed to whichever app is currently executing. + dvxMemAppIdPtr = &sCtx.currentAppId; + // Set up idle callback for cooperative yielding. When dvxUpdate has // no work to do (no input events, no dirty rects), it calls this // instead of busy-looping. This is the main mechanism for giving diff --git a/taskmgr/shellTaskMgr.c b/taskmgr/shellTaskMgr.c index 2f320ce..49324a5 100644 --- a/taskmgr/shellTaskMgr.c +++ b/taskmgr/shellTaskMgr.c @@ -18,14 +18,15 @@ #include #include +#include "dvxMem.h" // ============================================================ // Constants // ============================================================ -#define TM_COL_COUNT 5 +#define TM_COL_COUNT 6 #define TM_MAX_PATH 260 -#define TM_WIN_W 520 +#define TM_WIN_W 580 #define TM_WIN_H 280 #define TM_LIST_PREF_H 160 #define TM_BTN_SPACING 8 @@ -52,6 +53,7 @@ typedef struct { char title[MAX_TITLE_LEN]; char file[64]; char type[12]; + char mem[16]; } TmRowStringsT; static AppContextT *sCtx = NULL; @@ -226,6 +228,14 @@ static void refreshTaskList(void) { snprintf(row.file, sizeof(row.file), "%.63s", fname); snprintf(row.type, sizeof(row.type), "%s", app->hasMainLoop ? "Task" : "Callback"); + uint32_t memKb = dvxMemGetAppUsage(app->appId) / 1024; + + if (memKb >= 1024) { + snprintf(row.mem, sizeof(row.mem), "%lu MB", (unsigned long)(memKb / 1024)); + } else { + snprintf(row.mem, sizeof(row.mem), "%lu KB", (unsigned long)memKb); + } + arrput(sRowStrs, row); arrput(appIds, i); } @@ -241,6 +251,7 @@ static void refreshTaskList(void) { arrput(sCells, sRowStrs[r].title); arrput(sCells, sRowStrs[r].file); arrput(sCells, sRowStrs[r].type); + arrput(sCells, sRowStrs[r].mem); arrput(sCells, "Running"); } @@ -313,20 +324,23 @@ void shellTaskMgrOpen(AppContextT *ctx) { static ListViewColT tmCols[TM_COL_COUNT]; tmCols[0].title = "Name"; - tmCols[0].width = wgtPercent(20); + tmCols[0].width = wgtPercent(18); tmCols[0].align = ListViewAlignLeftE; tmCols[1].title = "Title"; - tmCols[1].width = wgtPercent(30); + tmCols[1].width = wgtPercent(26); tmCols[1].align = ListViewAlignLeftE; tmCols[2].title = "File"; - tmCols[2].width = wgtPercent(22); + tmCols[2].width = wgtPercent(18); tmCols[2].align = ListViewAlignLeftE; tmCols[3].title = "Type"; - tmCols[3].width = wgtPercent(14); + tmCols[3].width = wgtPercent(12); tmCols[3].align = ListViewAlignLeftE; - tmCols[4].title = "Status"; - tmCols[4].width = wgtPercent(14); - tmCols[4].align = ListViewAlignLeftE; + tmCols[4].title = "Memory"; + tmCols[4].width = wgtPercent(12); + tmCols[4].align = ListViewAlignRightE; + tmCols[5].title = "Status"; + tmCols[5].width = wgtPercent(14); + tmCols[5].align = ListViewAlignLeftE; sTmListView = wgtListView(root); sTmListView->weight = 100;