fix(store,ui): route the paired-free allocations through the memory core

The three exported release functions added for #1762 freed raw heap
blocks. Since the memory-core ratchet landed, the allocating side has to
go through the core as well, and on current main cbm_leiden already
allocates its result in CBM_MEM_CLASS_OTHER, so a raw free() in
cbm_leiden_free left the class counters drifting.

- cbm_store_fetch_call_edges: allocate and grow the source/target
  arrays with cbm_alloc/cbm_realloc in CBM_MEM_CLASS_STORE. An
  allocation failure now finalizes the statement, releases both arrays
  and returns CBM_STORE_ERR instead of writing through a NULL array.
- cbm_store_free_call_edges, cbm_leiden_free and cbm_layout_free_json
  release with cbm_free in the class the matching allocation used.
- cbm_layout_to_json writes its output through cbm_json_writer_allocator,
  so the string is an OTHER-class core block; the UI server's five
  release sites now call cbm_layout_free_json.
- The header comments name the paired free instead of "caller frees".
- Tests: ownership checks for all three pairs measure the class counters
  across the allocating call and the paired free, so a raw allocation on
  either side fails; the call-edge check grows past the initial capacity.

Refs #1762

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-20 19:24:56 +02:00
1 parent 4563748955
commit c4b635b25f
7 files changed
+141 -21

No files matched your search

+30 -9
View File
@@ -4123,10 +4123,18 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge
bind_text(stmt, SKIP_ONE, project);
sqlite3_bind_int(stmt, 2, max_edges > 0 ? max_edges + SKIP_ONE : CBM_NOT_FOUND);
/* Both arrays are STORE-class memory-core blocks: the caller releases them
* with cbm_store_free_call_edges, the only free that matches. */
int cap = ST_INIT_CAP_16;
int n = 0;
int64_t *src = malloc((size_t)cap * sizeof(int64_t));
int64_t *tgt = malloc((size_t)cap * sizeof(int64_t));
int64_t *src = cbm_alloc(CBM_MEM_CLASS_STORE, (size_t)cap * sizeof(int64_t));
int64_t *tgt = cbm_alloc(CBM_MEM_CLASS_STORE, (size_t)cap * sizeof(int64_t));
if (!src || !tgt) {
sqlite3_finalize(stmt);
cbm_store_free_call_edges(src, tgt);
store_set_error(s, "fetch_call_edges: out of memory");
return CBM_STORE_ERR;
}
int scan_rc;
while ((scan_rc = sqlite3_step(stmt)) == SQLITE_ROW) {
if (max_edges > 0 && n >= max_edges) {
@@ -4136,9 +4144,23 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge
break;
}
if (n >= cap) {
size_t grown_bytes = (size_t)cap * ST_GROWTH * sizeof(int64_t);
int64_t *grown_src = cbm_realloc(CBM_MEM_CLASS_STORE, src, grown_bytes);
if (grown_src) {
src = grown_src;
}
int64_t *grown_tgt =
grown_src ? cbm_realloc(CBM_MEM_CLASS_STORE, tgt, grown_bytes) : NULL;
if (grown_tgt) {
tgt = grown_tgt;
}
if (!grown_src || !grown_tgt) {
sqlite3_finalize(stmt);
cbm_store_free_call_edges(src, tgt);
store_set_error(s, "fetch_call_edges: out of memory");
return CBM_STORE_ERR;
}
cap *= ST_GROWTH;
src = safe_realloc(src, (size_t)cap * sizeof(int64_t));
tgt = safe_realloc(tgt, (size_t)cap * sizeof(int64_t));
}
src[n] = sqlite3_column_int64(stmt, 0);
tgt[n] = sqlite3_column_int64(stmt, 1);
@@ -4146,8 +4168,7 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge
}
sqlite3_finalize(stmt);
if (scan_rc != SQLITE_DONE && scan_rc != SQLITE_ROW) {
free(src);
free(tgt);
cbm_store_free_call_edges(src, tgt);
store_set_error_sqlite(s, "fetch_call_edges scan");
return CBM_STORE_ERR;
}
@@ -4158,8 +4179,8 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge
}
void cbm_store_free_call_edges(int64_t *src, int64_t *tgt) {
free(src);
free(tgt);
cbm_free(CBM_MEM_CLASS_STORE, src);
cbm_free(CBM_MEM_CLASS_STORE, tgt);
}
int cbm_store_find_edges_by_source(cbm_store_t *s, int64_t source_id, cbm_edge_t **out,
@@ -10499,7 +10520,7 @@ int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *
}
void cbm_leiden_free(cbm_louvain_result_t *result) {
free(result);
cbm_free(CBM_MEM_CLASS_OTHER, result); /* cbm_leiden allocates OTHER */
}
/* ── Architecture: community clusters via Leiden ───────────────── */
+3 -2
View File
@@ -708,7 +708,8 @@ int64_t cbm_store_insert_edge(cbm_store_t *s, const cbm_edge_t *e);
int cbm_store_insert_edge_batch(cbm_store_t *s, const cbm_edge_t *edges, int count);
/* Fetch all CALLS edges among Function/Method nodes for a project as parallel
* (source_id, target_id) arrays (caller frees both). For SCC / cycle analysis.
* (source_id, target_id) arrays, released together with
* cbm_store_free_call_edges. For SCC / cycle analysis.
* Stops at max_edges and sets *truncated — never a silent cap. Returns
* CBM_STORE_OK (or _ERR); *count is the number returned. */
int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edges,
@@ -1291,7 +1292,7 @@ typedef struct {
* reported community is internally connected. The resolution parameter
* controls granularity (higher -> more, smaller communities); 1.0 is standard.
* Allocates *out (length *out_count == node_count) in CBM_MEM_CLASS_OTHER;
* the caller releases it with cbm_free(CBM_MEM_CLASS_OTHER, *out). */
* the caller releases it with cbm_leiden_free(*out). */
int cbm_leiden(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *edges,
int edge_count, double resolution, cbm_louvain_result_t **out, int *out_count);
+5 -5
View File
@@ -2042,7 +2042,7 @@ static bool attach_missed_graph(yyjson_mut_doc *mdoc, yyjson_mut_val *mroot, cbm
return false;
}
yyjson_doc *mldoc = yyjson_read(mjson, strlen(mjson), 0);
free(mjson);
cbm_layout_free_json(mjson);
if (!mldoc) {
return false;
}
@@ -2164,7 +2164,7 @@ static void handle_layout(cbm_http_conn_t *c, const cbm_http_req_t *req) {
return;
}
cbm_http_replyf(c, 200, g_cors_json, "%s", scoped_json);
free(scoped_json);
cbm_layout_free_json(scoped_json);
return;
}
@@ -2200,13 +2200,13 @@ static void handle_layout(cbm_http_conn_t *c, const cbm_http_req_t *req) {
if (linked_count == 0 && missed_graph) {
cbm_store_close(store);
cbm_http_replyf(c, 200, g_cors_json, "%s", primary_json);
free(primary_json);
cbm_layout_free_json(primary_json);
return;
}
/* Parse primary JSON and append missed_graph + linked_projects */
yyjson_doc *pdoc = yyjson_read(primary_json, strlen(primary_json), 0);
free(primary_json);
cbm_layout_free_json(primary_json);
if (!pdoc) {
cbm_store_close(store);
cbm_http_replyf(c, 500, g_cors_json, "{\"error\":\"JSON parse failed\"}");
@@ -2258,7 +2258,7 @@ static void handle_layout(cbm_http_conn_t *c, const cbm_http_req_t *req) {
/* Parse linked project layout */
yyjson_doc *lpdoc = yyjson_read(lp_json, strlen(lp_json), 0);
free(lp_json);
cbm_layout_free_json(lp_json);
if (!lpdoc) {
cbm_store_close(lp_store);
free(linked[li]);
+6 -3
View File
@@ -14,6 +14,7 @@
#include "ui/layout3d.h"
#include "ui/layout_internal.h"
#include "foundation/log.h"
#include "foundation/json_writer.h"
#include "foundation/mem_core.h"
#include "foundation/platform.h"
@@ -878,10 +879,12 @@ char *cbm_layout_to_json(const cbm_layout_result_t *r) {
yyjson_mut_obj_add_val(doc, root, "edges", ea);
yyjson_mut_obj_add_int(doc, root, "total_nodes", r->total_nodes);
/* The output string is an OTHER-class memory-core block, released by
* cbm_layout_free_json; the document keeps its own allocator. */
size_t len = 0;
yyjson_write_err write_err = {0};
char *json =
yyjson_mut_write_opts(doc, YYJSON_WRITE_ALLOW_INVALID_UNICODE, NULL, &len, &write_err);
char *json = yyjson_mut_write_opts(doc, YYJSON_WRITE_ALLOW_INVALID_UNICODE,
cbm_json_writer_allocator(), &len, &write_err);
yyjson_mut_doc_free(doc);
if (!json) {
char code[CBM_SZ_32];
@@ -893,5 +896,5 @@ char *cbm_layout_to_json(const cbm_layout_result_t *r) {
}
void cbm_layout_free_json(char *json) {
free(json);
cbm_free(CBM_MEM_CLASS_OTHER, json);
}
+1 -1
View File
@@ -69,7 +69,7 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
/* Release a computed layout result and its fields through their matching allocators. */
void cbm_layout_free(cbm_layout_result_t *result);
/* Serialize layout result to JSON string. Caller must free(). */
/* Serialize layout result to JSON string. Release with cbm_layout_free_json(). */
char *cbm_layout_to_json(const cbm_layout_result_t *result);
/* Free the JSON string returned by cbm_layout_to_json. */
+61 -1
View File
@@ -1431,6 +1431,64 @@ TEST(fetch_call_edges_empty_project) {
PASS();
}
/* The two arrays cbm_store_fetch_call_edges hands out are memory-core blocks of
* the STORE class, and cbm_store_free_call_edges is what takes them back. Both
* halves are measured: the fetch adds exactly the two arrays and the paired free
* removes exactly those, so a raw allocation on either side fails. Thirty-six
* edges outgrow the initial capacity of 16, so the growth path is covered too. */
TEST(fetch_call_edges_memory_core_ownership) {
enum { FN = 37 };
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
cbm_store_upsert_project(s, "grow", "/tmp/grow");
int64_t ids[FN];
for (int i = 0; i < FN; i++) {
char name[32];
char qn[64];
snprintf(name, sizeof(name), "f%d", i);
snprintf(qn, sizeof(qn), "grow.pkg.f%d", i);
cbm_node_t n = {.project = "grow",
.label = "Function",
.name = name,
.qualified_name = qn,
.file_path = "pkg/f.go"};
ids[i] = cbm_store_upsert_node(s, &n);
ASSERT_TRUE(ids[i] > 0);
}
for (int i = 0; i + 1 < FN; i++) {
cbm_edge_t e = {
.project = "grow", .source_id = ids[i], .target_id = ids[i + 1], .type = "CALLS"};
ASSERT_TRUE(cbm_store_insert_edge(s, &e) > 0);
}
size_t bytes = cbm_mem_class_live_bytes(CBM_MEM_CLASS_STORE);
size_t blocks = cbm_mem_class_live_blocks(CBM_MEM_CLASS_STORE);
int64_t *src = NULL;
int64_t *tgt = NULL;
int count = 0;
bool truncated = true;
ASSERT_EQ(cbm_store_fetch_call_edges(s, "grow", 100, &src, &tgt, &count, &truncated),
CBM_STORE_OK);
ASSERT_EQ(count, FN - 1);
ASSERT_FALSE(truncated);
ASSERT_NOT_NULL(src);
ASSERT_NOT_NULL(tgt);
/* Ordered by (source_id, target_id): growth kept every pair in place. */
ASSERT_EQ(src[0], ids[0]);
ASSERT_EQ(tgt[0], ids[1]);
ASSERT_EQ(src[FN - 2], ids[FN - 2]);
ASSERT_EQ(tgt[FN - 2], ids[FN - 1]);
ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_STORE), blocks + 2);
ASSERT_TRUE(cbm_mem_class_live_bytes(CBM_MEM_CLASS_STORE) >=
bytes + 2 * (size_t)count * sizeof(int64_t));
cbm_store_free_call_edges(src, tgt);
ASSERT_EQ(cbm_mem_class_live_bytes(CBM_MEM_CLASS_STORE), bytes);
ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_STORE), blocks);
cbm_store_close(s);
PASS();
}
/* ── Louvain tests ──────────────────────────────────────────────── */
TEST(louvain_basic) {
@@ -1564,7 +1622,8 @@ TEST(leiden_result_memory_core_ownership) {
ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks + 1);
ASSERT_TRUE(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER) >=
bytes + 3 * sizeof(*result));
cbm_free(CBM_MEM_CLASS_OTHER, result);
/* Released through the exported pair, the way an FFI caller must. */
cbm_leiden_free(result);
ASSERT_EQ(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER), bytes);
ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks);
}
@@ -2014,6 +2073,7 @@ SUITE(store_arch) {
RUN_TEST(fetch_call_edges_basic);
RUN_TEST(fetch_call_edges_truncated);
RUN_TEST(fetch_call_edges_empty_project);
RUN_TEST(fetch_call_edges_memory_core_ownership);
/* Louvain */
RUN_TEST(louvain_basic);
+35
View File
@@ -562,6 +562,40 @@ TEST(layout_to_json) {
PASS();
}
/* cbm_layout_to_json hands out one memory-core block of the OTHER class, and
* cbm_layout_free_json takes exactly that block back. A raw allocation on either
* side of the pair fails the count. */
TEST(layout_json_memory_core_ownership) {
cbm_store_t *store = cbm_store_open_memory();
ASSERT_NOT_NULL(store);
cbm_store_upsert_project(store, "test", "/tmp/test");
cbm_node_t n = {.project = "test",
.label = "Function",
.name = "hello",
.qualified_name = "test::hello",
.file_path = "a.c",
.start_line = 1,
.end_line = 5};
cbm_store_upsert_node(store, &n);
cbm_layout_result_t *r = cbm_layout_compute(store, "test", CBM_LAYOUT_OVERVIEW, NULL, 0, 100);
ASSERT_NOT_NULL(r);
size_t bytes = cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER);
size_t blocks = cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER);
char *json = cbm_layout_to_json(r);
ASSERT_NOT_NULL(json);
ASSERT(strstr(json, "\"hello\"") != NULL);
ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks + 1);
ASSERT_TRUE(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER) >= bytes + strlen(json) + 1);
cbm_layout_free_json(json);
ASSERT_EQ(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER), bytes);
ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks);
cbm_layout_free(r);
cbm_store_close(store);
PASS();
}
TEST(layout_null_inputs) {
/* NULL store → NULL result */
cbm_layout_result_t *r = cbm_layout_compute(NULL, "test", CBM_LAYOUT_OVERVIEW, NULL, 0, 100);
@@ -2698,6 +2732,7 @@ SUITE(ui) {
RUN_TEST(layout_honors_budget_above_default);
RUN_TEST(layout_deterministic);
RUN_TEST(layout_to_json);
RUN_TEST(layout_json_memory_core_ownership);
RUN_TEST(layout_null_inputs);
RUN_TEST(layout_dead_code_classification);
RUN_TEST(layout_coincident_nodes_bounded);