diff --git a/src/daemon/runtime.c b/src/daemon/runtime.c index fc3cc0b1d..4049017be 100644 --- a/src/daemon/runtime.c +++ b/src/daemon/runtime.c @@ -1853,6 +1853,10 @@ static void *runtime_connection_worker(void *opaque) { * of live blocks). Collecting here hands the pages back while this thread * still owns them, which is the only point at which that is cheap (#581). */ cbm_mem_collect(); + /* Census AFTER the collect: this is the per-connection steady state, so a + * pool that climbs across these lines is holding memory the connection was + * supposed to have given back. */ + cbm_mem_census_log("connection.exit"); return NULL; } diff --git a/src/foundation/mem.c b/src/foundation/mem.c index 014e3b41f..64d7584c8 100644 --- a/src/foundation/mem.c +++ b/src/foundation/mem.c @@ -744,6 +744,12 @@ bool cbm_mem_win_census(cbm_mem_win_census_t *out) { switch (mbi.Type) { case MEM_PRIVATE: out->committed_private += mbi.RegionSize; + if (mbi.RegionSize > out->largest_private_region) { + out->largest_private_region = mbi.RegionSize; + } + if (mbi.RegionSize >= (size_t)CBM_SZ_1K * CBM_SZ_1K) { + out->private_regions_1mb++; + } break; case MEM_MAPPED: out->committed_mapped += mbi.RegionSize; @@ -791,3 +797,38 @@ bool cbm_mem_win_census(cbm_mem_win_census_t *out) { return true; #endif } + +void cbm_mem_census_log(const char *tag) { + if (!cbm_mem_census_enabled()) { + return; + } + cbm_mem_win_census_t census; + if (!cbm_mem_win_census(&census)) { + return; + } + /* Kilobytes throughout: the CRT heap measured 0 at megabyte resolution, + * which reads as "empty" when it may simply be small — a diagnostic that + * rounds its own evidence away is worse than none. */ + char priv_kb[CBM_SZ_32]; + char crt_kb[CBM_SZ_32]; + char busy_kb[CBM_SZ_32]; + char mapped_kb[CBM_SZ_32]; + char rss_kb[CBM_SZ_32]; + char biggest_kb[CBM_SZ_32]; + char regions[CBM_SZ_32]; + char big_regions[CBM_SZ_32]; + char heaps[CBM_SZ_32]; + (void)snprintf(priv_kb, sizeof(priv_kb), "%zu", census.committed_private / CBM_SZ_1K); + (void)snprintf(crt_kb, sizeof(crt_kb), "%zu", census.crt_heap_committed / CBM_SZ_1K); + (void)snprintf(busy_kb, sizeof(busy_kb), "%zu", census.crt_heap_busy / CBM_SZ_1K); + (void)snprintf(mapped_kb, sizeof(mapped_kb), "%zu", census.committed_mapped / CBM_SZ_1K); + (void)snprintf(rss_kb, sizeof(rss_kb), "%zu", cbm_mem_rss() / CBM_SZ_1K); + (void)snprintf(biggest_kb, sizeof(biggest_kb), "%zu", + census.largest_private_region / CBM_SZ_1K); + (void)snprintf(regions, sizeof(regions), "%u", census.regions); + (void)snprintf(big_regions, sizeof(big_regions), "%u", census.private_regions_1mb); + (void)snprintf(heaps, sizeof(heaps), "%u", census.heap_walk_ok ? census.crt_heap_count : 0); + cbm_log_info("mem.census", "at", tag ? tag : "?", "priv_kb", priv_kb, "crt_kb", crt_kb, + "crt_busy_kb", busy_kb, "mapped_kb", mapped_kb, "rss_kb", rss_kb, "biggest_kb", + biggest_kb, "regions", regions, "big_regions", big_regions, "heaps", heaps); +} diff --git a/src/foundation/mem.h b/src/foundation/mem.h index a6d671d8f..581dbc368 100644 --- a/src/foundation/mem.h +++ b/src/foundation/mem.h @@ -233,6 +233,11 @@ typedef struct { size_t crt_heap_busy; /* bytes in live allocations inside them */ unsigned crt_heap_count; unsigned regions; + /* The single largest committed private region, and how many are >=1MiB. + * One enormous region means one reservation (allocator metadata, a pagemap) + * rather than many leaked blocks — a different bug with a different fix. */ + size_t largest_private_region; + unsigned private_regions_1mb; bool heap_walk_ok; /* false → crt_* are unknown, not zero */ } cbm_mem_win_census_t; @@ -240,6 +245,10 @@ typedef struct { * (non-Windows, or out is NULL). */ bool cbm_mem_win_census(cbm_mem_win_census_t *out); +/* Emit one census line tagged with the call site, so a growth series can be + * read straight out of the daemon log. No-op unless CBM_MEM_CENSUS=1. */ +void cbm_mem_census_log(const char *tag); + /* Census logging gate: CBM_MEM_CENSUS=1. Off by default — HeapWalk locks each * CRT heap, so this is a diagnostic, never a hot-path metric. */ bool cbm_mem_census_enabled(void); diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index c9d5f1c20..d891e7dd9 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -10453,30 +10453,7 @@ char *cbm_mcp_handle_tool(cbm_mcp_server_t *srv, const char *tool_name, const ch * rather than inferred from a process total (#581). Emitted after the * request store is released, which is the point where a well-behaved * request has given everything back. */ - if (cbm_mem_census_enabled()) { - cbm_mem_win_census_t census; - if (cbm_mem_win_census(&census)) { - const size_t mb = (size_t)CBM_SZ_1K * CBM_SZ_1K; - char private_mb[CBM_SZ_32]; - char crt_mb[CBM_SZ_32]; - char busy_mb[CBM_SZ_32]; - char mapped_mb[CBM_SZ_32]; - char rss_mb[CBM_SZ_32]; - char regions[CBM_SZ_32]; - (void)snprintf(private_mb, sizeof(private_mb), "%zu", - census.committed_private / mb); - (void)snprintf(crt_mb, sizeof(crt_mb), "%zu", - census.crt_heap_committed / mb); - (void)snprintf(busy_mb, sizeof(busy_mb), "%zu", census.crt_heap_busy / mb); - (void)snprintf(mapped_mb, sizeof(mapped_mb), "%zu", - census.committed_mapped / mb); - (void)snprintf(rss_mb, sizeof(rss_mb), "%zu", cbm_mem_rss() / mb); - (void)snprintf(regions, sizeof(regions), "%u", census.regions); - cbm_log_info("mem.census", "private_mb", private_mb, "crt_heap_mb", crt_mb, - "crt_busy_mb", busy_mb, "mapped_mb", mapped_mb, "rss_mb", rss_mb, - "regions", regions); - } - } + cbm_mem_census_log("mcp.request"); return result; }