mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
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:
1 parent
4563748955
commit
c4b635b25f
7 files changed
+141
-21
No files matched your search
+30
-9
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user