@@ -39,7 +39,8 @@ struct cbm_graph_mem {
3939 bool ready ; /* built; false while its builder runs */
4040 bool detached ; /* unlinked from the registry; freed at the last release */
4141 uint64_t last_use ;
42- size_t bytes ; /* reserved estimate while building, actual once ready */
42+ uint64_t idle_since_ms ; /* when the last reference went away */
43+ size_t bytes ; /* reserved estimate while building, actual once ready */
4344
4445 /* The graph, immutable once ready. Offsets cover node ids 0..n_ids-1:
4546 * the out-edges of id u are out_nbr/out_type[out_off[u] .. out_off[u+1]). */
@@ -67,12 +68,30 @@ static sqlite3_mutex *gm_registry_mutex(void) {
6768 return sqlite3_mutex_alloc (SQLITE_MUTEX_STATIC_APP2 );
6869}
6970
71+ #ifdef CBM_ENABLE_TEST_SEAMS
72+ static uint64_t (* g_clock_seam )(void );
73+ static size_t g_cached_bytes_seam ;
74+ void cbm_graph_mem_set_clock_for_testing (uint64_t (* now_ms )(void )) {
75+ g_clock_seam = now_ms ;
76+ }
77+ void cbm_graph_mem_set_cached_bytes_for_testing (size_t bytes ) {
78+ g_cached_bytes_seam = bytes ;
79+ }
80+ static uint64_t gm_now_ms (void ) {
81+ return g_clock_seam ? g_clock_seam () : cbm_now_ms ();
82+ }
83+ #else
84+ static uint64_t gm_now_ms (void ) {
85+ return cbm_now_ms ();
86+ }
87+ #endif
88+
7089static void * gm_alloc (size_t bytes ) {
71- return cbm_alloc (CBM_MEM_CLASS_STORE , bytes ? bytes : 1 );
90+ return cbm_alloc (CBM_MEM_CLASS_GRAPH_CACHE , bytes ? bytes : 1 );
7291}
7392
7493static void gm_free_ptr (void * p ) {
75- cbm_free (CBM_MEM_CLASS_STORE , p );
94+ cbm_free (CBM_MEM_CLASS_GRAPH_CACHE , p );
7695}
7796
7897static void gm_free_graph (cbm_graph_mem_t * g ) {
@@ -166,6 +185,31 @@ static bool gm_make_room(size_t bytes) {
166185 return cbm_mem_charged () + bytes <= budget ;
167186}
168187
188+ /* Retire every layer no request has held for CBM_GRAPH_MEM_IDLE_EVICT_MS.
189+ * Returns the bytes given back. */
190+ static size_t gm_evict_expired_locked (uint64_t now ) {
191+ size_t freed = 0 ;
192+ cbm_graph_mem_t * g = g_layers ;
193+ while (g ) {
194+ cbm_graph_mem_t * next = g -> next ;
195+ if (g -> ready && g -> refs == 0 && now >= g -> idle_since_ms &&
196+ now - g -> idle_since_ms >= CBM_GRAPH_MEM_IDLE_EVICT_MS ) {
197+ freed += g -> bytes ;
198+ gm_retire (g );
199+ }
200+ g = next ;
201+ }
202+ return freed ;
203+ }
204+
205+ static void gm_log_evicted (size_t freed ) {
206+ if (freed > 0 ) {
207+ char bytes [CBM_SZ_32 ];
208+ (void )snprintf (bytes , sizeof (bytes ), "%zu" , freed );
209+ cbm_log_info ("graph_mem.evict" , "reason" , "idle" , "bytes" , bytes );
210+ }
211+ }
212+
169213/* ── Build ──────────────────────────────────────────────────────── */
170214
171215static bool gm_bounds (sqlite3 * db , int64_t * max_node , int64_t * max_edge ) {
@@ -226,7 +270,7 @@ static bool gm_grow_u32(uint32_t **arr, size_t *cap, size_t need) {
226270 while (next < need ) {
227271 next *= GM_GROWTH ;
228272 }
229- uint32_t * grown = cbm_realloc (CBM_MEM_CLASS_STORE , * arr , next * sizeof (uint32_t ));
273+ uint32_t * grown = cbm_realloc (CBM_MEM_CLASS_GRAPH_CACHE , * arr , next * sizeof (uint32_t ));
230274 if (!grown ) {
231275 return false;
232276 }
@@ -264,12 +308,12 @@ static bool gm_scan_row(cbm_graph_mem_t *g, gm_scan_t *sc, sqlite3_stmt *st) {
264308 }
265309 if (g -> n_edges >= sc -> edge_cap ) {
266310 size_t next = sc -> edge_cap ? sc -> edge_cap * GM_GROWTH : 1 ;
267- uint32_t * nbr = cbm_realloc (CBM_MEM_CLASS_STORE , g -> out_nbr , next * sizeof (uint32_t ));
311+ uint32_t * nbr = cbm_realloc (CBM_MEM_CLASS_GRAPH_CACHE , g -> out_nbr , next * sizeof (uint32_t ));
268312 if (!nbr ) {
269313 return false;
270314 }
271315 g -> out_nbr = nbr ;
272- uint8_t * types = cbm_realloc (CBM_MEM_CLASS_STORE , g -> out_type , next );
316+ uint8_t * types = cbm_realloc (CBM_MEM_CLASS_GRAPH_CACHE , g -> out_type , next );
273317 if (!types ) {
274318 return false;
275319 }
@@ -294,7 +338,7 @@ static bool gm_scan_row(cbm_graph_mem_t *g, gm_scan_t *sc, sqlite3_stmt *st) {
294338 * back. Iterating sources in id order keeps every in-list sorted by source. */
295339static bool gm_build_inbound (cbm_graph_mem_t * g ) {
296340 size_t n_ids = g -> n_ids ;
297- g -> in_off = cbm_calloc (CBM_MEM_CLASS_STORE , (n_ids + 1 ) * sizeof (uint32_t ));
341+ g -> in_off = cbm_calloc (CBM_MEM_CLASS_GRAPH_CACHE , (n_ids + 1 ) * sizeof (uint32_t ));
298342 g -> in_nbr = gm_alloc ((size_t )g -> n_edges * sizeof (uint32_t ));
299343 g -> in_type = gm_alloc (g -> n_edges );
300344 if (!g -> in_off || !g -> in_nbr || !g -> in_type ) {
@@ -361,8 +405,8 @@ static bool gm_build(cbm_graph_mem_t *g, sqlite3 *db, int64_t max_node, int64_t
361405 /* max(edges.id) over-counts after deletions; give the slack back. */
362406 if (g -> n_edges < sc .edge_cap ) {
363407 size_t keep = g -> n_edges ? g -> n_edges : 1 ;
364- uint32_t * nbr = cbm_realloc (CBM_MEM_CLASS_STORE , g -> out_nbr , keep * sizeof (uint32_t ));
365- uint8_t * types = nbr ? cbm_realloc (CBM_MEM_CLASS_STORE , g -> out_type , keep ) : NULL ;
408+ uint32_t * nbr = cbm_realloc (CBM_MEM_CLASS_GRAPH_CACHE , g -> out_nbr , keep * sizeof (uint32_t ));
409+ uint8_t * types = nbr ? cbm_realloc (CBM_MEM_CLASS_GRAPH_CACHE , g -> out_type , keep ) : NULL ;
366410 if (nbr ) {
367411 g -> out_nbr = nbr ;
368412 }
@@ -397,11 +441,11 @@ static cbm_graph_mem_t *gm_take(cbm_graph_mem_t *hit) {
397441}
398442
399443static cbm_graph_mem_t * gm_new_entry (const char * db_path , const char * version , size_t est ) {
400- cbm_graph_mem_t * g = cbm_calloc (CBM_MEM_CLASS_STORE , sizeof (* g ));
444+ cbm_graph_mem_t * g = cbm_calloc (CBM_MEM_CLASS_GRAPH_CACHE , sizeof (* g ));
401445 if (!g ) {
402446 return NULL ;
403447 }
404- g -> path = cbm_mem_strdup (CBM_MEM_CLASS_STORE , db_path );
448+ g -> path = cbm_mem_strdup (CBM_MEM_CLASS_GRAPH_CACHE , db_path );
405449 if (!g -> path ) {
406450 gm_free_ptr (g );
407451 return NULL ;
@@ -416,11 +460,14 @@ cbm_graph_mem_t *cbm_graph_mem_acquire(sqlite3 *db, const char *db_path, const c
416460 if (!db || !db_path || !version || !version [0 ] || strlen (version ) >= GM_VERSION_MAX ) {
417461 return NULL ;
418462 }
463+ uint64_t now = gm_now_ms ();
419464 sqlite3_mutex * mu = gm_registry_mutex ();
420465 sqlite3_mutex_enter (mu );
466+ size_t freed = gm_evict_expired_locked (now );
421467 cbm_graph_mem_t * hit = gm_find (db_path , version );
422468 cbm_graph_mem_t * taken = hit ? gm_take (hit ) : NULL ;
423469 sqlite3_mutex_leave (mu );
470+ gm_log_evicted (freed );
424471 if (hit ) {
425472 return taken ;
426473 }
@@ -496,16 +543,29 @@ void cbm_graph_mem_release(cbm_graph_mem_t *g) {
496543 if (!g ) {
497544 return ;
498545 }
546+ uint64_t now = gm_now_ms ();
499547 sqlite3_mutex * mu = gm_registry_mutex ();
500548 sqlite3_mutex_enter (mu );
501549 g -> refs -- ;
550+ if (g -> refs == 0 ) {
551+ g -> idle_since_ms = now ;
552+ }
502553 if (g -> refs == 0 && g -> detached ) {
503554 g_total_bytes -= g -> bytes ;
504555 gm_destroy (g );
505556 }
506557 sqlite3_mutex_leave (mu );
507558}
508559
560+ void cbm_graph_mem_evict_expired (void ) {
561+ uint64_t now = gm_now_ms ();
562+ sqlite3_mutex * mu = gm_registry_mutex ();
563+ sqlite3_mutex_enter (mu );
564+ size_t freed = gm_evict_expired_locked (now );
565+ sqlite3_mutex_leave (mu );
566+ gm_log_evicted (freed );
567+ }
568+
509569/* ── Queries ────────────────────────────────────────────────────── */
510570
511571cbm_graph_mem_types_t cbm_graph_mem_type_mask (const cbm_graph_mem_t * g , const char * const * types ,
@@ -557,7 +617,7 @@ static size_t gm_set_home(uint32_t id, size_t mask) {
557617
558618static bool gm_set_grow (gm_set_t * s ) {
559619 size_t cap = s -> cap ? s -> cap * GM_GROWTH : GM_SET_MIN_CAP ;
560- uint32_t * slots = cbm_calloc (CBM_MEM_CLASS_STORE , cap * sizeof (uint32_t ));
620+ uint32_t * slots = cbm_calloc (CBM_MEM_CLASS_GRAPH_CACHE , cap * sizeof (uint32_t ));
561621 if (!slots ) {
562622 return false;
563623 }
@@ -619,7 +679,7 @@ static bool gm_hops_push(gm_hops_t *h, uint32_t id, int hop) {
619679 if (h -> count == h -> cap ) {
620680 size_t next = h -> cap ? h -> cap * GM_GROWTH : GM_SET_MIN_CAP ;
621681 cbm_graph_mem_hop_t * rows =
622- cbm_realloc (CBM_MEM_CLASS_STORE , h -> rows , next * sizeof (cbm_graph_mem_hop_t ));
682+ cbm_realloc (CBM_MEM_CLASS_GRAPH_CACHE , h -> rows , next * sizeof (cbm_graph_mem_hop_t ));
623683 if (!rows ) {
624684 return false;
625685 }
@@ -742,6 +802,11 @@ int cbm_graph_mem_cached_count(void) {
742802}
743803
744804size_t cbm_graph_mem_cached_bytes (void ) {
805+ #ifdef CBM_ENABLE_TEST_SEAMS
806+ if (g_cached_bytes_seam ) {
807+ return g_cached_bytes_seam ;
808+ }
809+ #endif
745810 sqlite3_mutex * mu = gm_registry_mutex ();
746811 sqlite3_mutex_enter (mu );
747812 size_t bytes = g_total_bytes ;
0 commit comments