LCOV - code coverage report
Current view: top level - disco/gui - fd_gui_hist.c (source / functions) Hit Total Coverage
Test: cov.lcov Lines: 404 515 78.4 %
Date: 2026-08-14 04:54:57 Functions: 41 51 80.4 %

          Line data    Source code
       1             : #include "fd_gui_hist.h"
       2             : #include "fd_gui_store.h"
       3             : #include "fd_gui.h" /* fd_gui_t, record types */
       4             : 
       5             : #include <stddef.h> /* offsetof */
       6             : 
       7             : /* Every record type must fit in one store region (header + record). */
       8             : FD_STATIC_ASSERT( sizeof(fd_gui_slot_history_shred_event_t)<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
       9             : FD_STATIC_ASSERT( sizeof(fd_gui_tile_timers_hist_t        )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
      10             : FD_STATIC_ASSERT( sizeof(fd_gui_scheduler_counts_t        )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
      11             : FD_STATIC_ASSERT( sizeof(fd_gui_tile_stats_t              )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
      12             : FD_STATIC_ASSERT( sizeof(fd_gui_txn_waterfall_t           )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
      13             : FD_STATIC_ASSERT( sizeof(fd_gui_store_txn_start_t         )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
      14             : FD_STATIC_ASSERT( sizeof(fd_gui_store_txn_end_t           )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
      15             : FD_STATIC_ASSERT( sizeof(fd_gui_slot_t                    )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
      16             : FD_STATIC_ASSERT( sizeof(fd_gui_leader_slot_t             )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
      17             : FD_STATIC_ASSERT( sizeof(fd_gui_epoch_t                   )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
      18             : 
      19             : /* fd_gui_hist_ts_append copies each TS record through a fixed stack buffer
      20             :    so it can rewrite the clamped timestamp; every TS record type must fit. */
      21             : #define FD_GUI_HIST_TS_SZ_MAX (512UL)
      22             : FD_STATIC_ASSERT( sizeof(fd_gui_slot_history_shred_event_t)<=FD_GUI_HIST_TS_SZ_MAX, ts_rec_fits );
      23             : FD_STATIC_ASSERT( sizeof(fd_gui_tile_timers_hist_t        )<=FD_GUI_HIST_TS_SZ_MAX, ts_rec_fits );
      24             : FD_STATIC_ASSERT( sizeof(fd_gui_scheduler_counts_t        )<=FD_GUI_HIST_TS_SZ_MAX, ts_rec_fits );
      25             : FD_STATIC_ASSERT( sizeof(fd_gui_tile_stats_t              )<=FD_GUI_HIST_TS_SZ_MAX, ts_rec_fits );
      26             : FD_STATIC_ASSERT( sizeof(fd_gui_txn_waterfall_t           )<=FD_GUI_HIST_TS_SZ_MAX, ts_rec_fits );
      27             : FD_STATIC_ASSERT( sizeof(fd_gui_store_txn_start_t         )<=FD_GUI_HIST_TS_SZ_MAX, ts_rec_fits );
      28             : FD_STATIC_ASSERT( sizeof(fd_gui_store_txn_end_t           )<=FD_GUI_HIST_TS_SZ_MAX, ts_rec_fits );
      29             : 
      30             : /* Each key type must alias the matching record field exactly. */
      31             : FD_STATIC_ASSERT( offsetof( fd_gui_hist_slot_key_t,        slot     )==offsetof( fd_gui_slot_t,        slot     ), key_layout );
      32             : FD_STATIC_ASSERT( offsetof( fd_gui_hist_slot_key_t,        bank_seq )==offsetof( fd_gui_slot_t,        bank_seq ), key_layout );
      33             : FD_STATIC_ASSERT( offsetof( fd_gui_hist_leader_slot_key_t, slot     )==offsetof( fd_gui_leader_slot_t, slot     ), key_layout );
      34             : FD_STATIC_ASSERT( offsetof( fd_gui_hist_leader_slot_key_t, bank_seq )==offsetof( fd_gui_leader_slot_t, bank_seq ), key_layout );
      35             : FD_STATIC_ASSERT( offsetof( fd_gui_hist_epoch_key_t,       epoch    )==offsetof( fd_gui_epoch_t,       epoch    ), key_layout );
      36             : 
      37       11811 : #define FD_GUI_HIST_KEYSHAPE_TIMESERIES (0) /* (ts, ...)        */
      38        6720 : #define FD_GUI_HIST_KEYSHAPE_SLOT_BANK  (1) /* (slot, bank_seq) */
      39         291 : #define FD_GUI_HIST_KEYSHAPE_EPOCH      (2) /* (epoch)          */
      40             : 
      41             : static inline long
      42          75 : fd_gui_hist_dbi_res_ns( int dbi FD_PARAM_UNUSED ) {
      43          75 :   return FD_GUI_HIST_RES_1S_NS; /* all TS DBs use the 1s resolution */
      44          75 : }
      45             : 
      46             : static inline ulong
      47        3237 : fd_gui_hist_dbi_ts_off( int dbi ) {
      48        3237 :   switch( dbi ) {
      49        3111 :     case FD_GUI_HIST_SHRED_EVENTS:     return offsetof( fd_gui_slot_history_shred_event_t, timestamp           );
      50           3 :     case FD_GUI_HIST_TILE_TIMERS:      return offsetof( fd_gui_tile_timers_hist_t,         sample_time_nanos   );
      51         108 :     case FD_GUI_HIST_SCHEDULER_COUNTS: return offsetof( fd_gui_scheduler_counts_t,         sample_time_ns      );
      52           3 :     case FD_GUI_HIST_TILE_STATS:       return offsetof( fd_gui_tile_stats_t,               sample_time_nanos   );
      53           3 :     case FD_GUI_HIST_TXN_WATERFALL:    return offsetof( fd_gui_txn_waterfall_t,            sample_time_nanos   );
      54           3 :     case FD_GUI_HIST_TXN_START:        return offsetof( fd_gui_store_txn_start_t,          microblock_start_ns );
      55           3 :     case FD_GUI_HIST_TXN_END:          return offsetof( fd_gui_store_txn_end_t,            microblock_end_ns   );
      56           3 :     default:                           return 0UL;
      57        3237 :   }
      58        3237 : }
      59             : 
      60             : static inline ulong
      61         111 : fd_gui_hist_window( long ts_ns, long res_ns ) {
      62         111 :   return (ulong)( ts_ns / res_ns );
      63         111 : }
      64             : 
      65             : static int
      66       11040 : fd_gui_hist_keyshape( int dbi ) {
      67       11040 :   switch( dbi ) {
      68         222 :     case FD_GUI_HIST_SCHEDULER_COUNTS:
      69         318 :     case FD_GUI_HIST_TILE_TIMERS:
      70         414 :     case FD_GUI_HIST_TILE_STATS:
      71         510 :     case FD_GUI_HIST_TXN_WATERFALL:
      72         606 :     case FD_GUI_HIST_TOWER:
      73        3846 :     case FD_GUI_HIST_SHRED_EVENTS:
      74        3942 :     case FD_GUI_HIST_TXN_START:
      75        4038 :     case FD_GUI_HIST_TXN_END:          return FD_GUI_HIST_KEYSHAPE_TIMESERIES;
      76        6435 :     case FD_GUI_HIST_SLOT:
      77        6720 :     case FD_GUI_HIST_LEADER_SLOT:      return FD_GUI_HIST_KEYSHAPE_SLOT_BANK;
      78         282 :     case FD_GUI_HIST_EPOCH:            return FD_GUI_HIST_KEYSHAPE_EPOCH;
      79           0 :     default: FD_LOG_ERR(( "invalid dbi %d", dbi )); return -1;
      80       11040 :   }
      81       11040 : }
      82             : 
      83             : static inline int
      84        7773 : fd_gui_hist_is_timeseries( int dbi ) {
      85        7773 :   return fd_gui_hist_keyshape( dbi )==FD_GUI_HIST_KEYSHAPE_TIMESERIES;
      86        7773 : }
      87             : 
      88             : static ulong
      89       12597 : fd_gui_hist_slot_key_hash( void const * key ) {
      90       12597 :   fd_gui_hist_slot_key_t const * k = key;
      91       12597 :   return fd_ulong_hash( k->slot );
      92       12597 : }
      93             : 
      94             : static int
      95        6369 : fd_gui_hist_slot_key_cmp( void const * a, void const * b ) {
      96        6369 :   fd_gui_hist_slot_key_t const * ka = a;
      97        6369 :   fd_gui_hist_slot_key_t const * kb = b;
      98        6369 :   if( ka->slot<kb->slot ) return -1;
      99        3279 :   if( ka->slot>kb->slot ) return  1;
     100        3276 :   if( ka->bank_seq==ULONG_MAX || kb->bank_seq==ULONG_MAX ) return 0;
     101        3147 :   if( ka->bank_seq<kb->bank_seq ) return -1;
     102        3147 :   if( ka->bank_seq>kb->bank_seq ) return  1;
     103        3147 :   return 0;
     104        3147 : }
     105             : 
     106             : static ulong
     107         306 : fd_gui_hist_leader_slot_key_hash( void const * key ) {
     108         306 :   fd_gui_hist_leader_slot_key_t const * k = key;
     109         306 :   return fd_ulong_hash( k->slot );
     110         306 : }
     111             : 
     112             : static int
     113         129 : fd_gui_hist_leader_slot_key_cmp( void const * a, void const * b ) {
     114         129 :   fd_gui_hist_leader_slot_key_t const * ka = a;
     115         129 :   fd_gui_hist_leader_slot_key_t const * kb = b;
     116         129 :   if( ka->slot<kb->slot ) return -1;
     117          99 :   if( ka->slot>kb->slot ) return  1;
     118          99 :   if( ka->bank_seq==ULONG_MAX || kb->bank_seq==ULONG_MAX ) return 0;
     119          33 :   if( ka->bank_seq<kb->bank_seq ) return -1;
     120          33 :   if( ka->bank_seq>kb->bank_seq ) return  1;
     121          33 :   return 0;
     122          33 : }
     123             : 
     124             : static ulong
     125         342 : fd_gui_hist_epoch_key_hash( void const * key ) {
     126         342 :   fd_gui_hist_epoch_key_t const * k = key;
     127         342 :   return fd_ulong_hash( k->epoch );
     128         342 : }
     129             : 
     130             : static int
     131         390 : fd_gui_hist_epoch_key_cmp( void const * a, void const * b ) {
     132         390 :   fd_gui_hist_epoch_key_t const * ka = a;
     133         390 :   fd_gui_hist_epoch_key_t const * kb = b;
     134         390 :   if( ka->epoch<kb->epoch ) return -1;
     135         327 :   if( ka->epoch>kb->epoch ) return  1;
     136         201 :   return 0;
     137         327 : }
     138             : 
     139             : static inline ulong
     140        3360 : fd_gui_hist_rec_sz( int dbi ) {
     141        3360 :   switch( dbi ) {
     142        3144 :     case FD_GUI_HIST_SHRED_EVENTS:     return sizeof(fd_gui_slot_history_shred_event_t);
     143           3 :     case FD_GUI_HIST_TILE_TIMERS:      return sizeof(fd_gui_tile_timers_hist_t);
     144         126 :     case FD_GUI_HIST_SCHEDULER_COUNTS: return sizeof(fd_gui_scheduler_counts_t);
     145           3 :     case FD_GUI_HIST_TILE_STATS:       return sizeof(fd_gui_tile_stats_t);
     146           3 :     case FD_GUI_HIST_TXN_WATERFALL:    return sizeof(fd_gui_txn_waterfall_t);
     147           3 :     case FD_GUI_HIST_TXN_START:        return sizeof(fd_gui_store_txn_start_t);
     148           3 :     case FD_GUI_HIST_TXN_END:          return sizeof(fd_gui_store_txn_end_t);
     149          24 :     case FD_GUI_HIST_SLOT:             return sizeof(fd_gui_slot_t);
     150          24 :     case FD_GUI_HIST_LEADER_SLOT:      return sizeof(fd_gui_leader_slot_t);
     151          24 :     case FD_GUI_HIST_EPOCH:            return sizeof(fd_gui_epoch_t);
     152             :     /* FD_GUI_HIST_TOWER is declared but not yet written (no record type). */
     153           3 :     default:                           return 0UL;
     154        3360 :   }
     155        3360 : }
     156             : 
     157             : /* fd_gui_hist_key_sz returns the KV key width for `dbi`: 16 bytes
     158             :    (slot, bank_seq) for slot-keyed DBs, 8 bytes (epoch) for EPOCH, 0 for TS. */
     159             : static inline ulong
     160           9 : fd_gui_hist_key_sz( int dbi ) {
     161           9 :   switch( dbi ) {
     162           3 :     case FD_GUI_HIST_SLOT:        return sizeof(fd_gui_hist_slot_key_t);
     163           3 :     case FD_GUI_HIST_LEADER_SLOT: return sizeof(fd_gui_hist_leader_slot_key_t);
     164           3 :     case FD_GUI_HIST_EPOCH:       return sizeof(fd_gui_hist_epoch_key_t);
     165           0 :     default:                      return 0UL; /* TS DBs have no key */
     166           9 :   }
     167           9 : }
     168             : 
     169             : /* fd_gui_hist_key_hash / fd_gui_hist_key_cmp return the per-DB KV key
     170             :    callbacks for `dbi` (NULL for TS DBs). */
     171             : 
     172             : static inline ulong
     173           9 : ( * fd_gui_hist_key_hash( int dbi ) )( void const * key ) {
     174           9 :   switch( dbi ) {
     175           3 :     case FD_GUI_HIST_SLOT:        return fd_gui_hist_slot_key_hash;
     176           3 :     case FD_GUI_HIST_LEADER_SLOT: return fd_gui_hist_leader_slot_key_hash;
     177           3 :     case FD_GUI_HIST_EPOCH:       return fd_gui_hist_epoch_key_hash;
     178           0 :     default:                      return NULL;
     179           9 :   }
     180           9 : }
     181             : 
     182             : static inline int
     183           9 : ( * fd_gui_hist_key_cmp( int dbi ) )( void const * a, void const * b ) {
     184           9 :   switch( dbi ) {
     185           3 :     case FD_GUI_HIST_SLOT:        return fd_gui_hist_slot_key_cmp;
     186           3 :     case FD_GUI_HIST_LEADER_SLOT: return fd_gui_hist_leader_slot_key_cmp;
     187           3 :     case FD_GUI_HIST_EPOCH:       return fd_gui_hist_epoch_key_cmp;
     188           0 :     default:                      return NULL;
     189           9 :   }
     190           9 : }
     191             : 
     192             : /* fd_gui_hist_kv_stride approximates fd_gui_store's per-record ring stride
     193             :    for KV DB `dbi`.  The store adds no header (the key and link live inside
     194             :    the value), so the stride is just the align-padded record. */
     195             : static inline ulong
     196          63 : fd_gui_hist_kv_stride( int dbi ) {
     197          63 :   return fd_ulong_align_up( fd_gui_hist_rec_sz( dbi ), 8UL );
     198          63 : }
     199             : 
     200             : static inline ulong
     201          21 : fd_gui_hist_bytes_per_epoch( void ) {
     202          21 :   return fd_gui_hist_kv_stride( FD_GUI_HIST_EPOCH )
     203          21 :        + MAX_SLOTS_PER_EPOCH * ( fd_gui_hist_kv_stride( FD_GUI_HIST_SLOT ) + fd_gui_hist_kv_stride( FD_GUI_HIST_LEADER_SLOT ) );
     204          21 : }
     205             : 
     206             : fd_gui_store_desc_t const *
     207          36 : fd_gui_hist_db_descs( ulong store_bytes ) {
     208          36 :   static char const * const names[ FD_GUI_HIST_CNT ] = {
     209          36 :     "scheduler_counts", "tile_timers", "shred_events", "txn_start",
     210          36 :     "txn_end", "tower", "slot", "leader_slot",
     211          36 :     "epoch", "tile_stats", "txn_waterfall"
     212          36 :   };
     213          36 :   static fd_gui_store_desc_t descs[ FD_GUI_HIST_CNT ];
     214          36 :   static ulong built_for = 0UL; /* store_bytes the table was built for (0 = unbuilt) */
     215             : 
     216          36 :   if( FD_UNLIKELY( built_for && built_for!=store_bytes ) )
     217           0 :     FD_LOG_ERR(( "fd_gui_hist_db_descs: called with %lu after %lu", store_bytes, built_for ));
     218             : 
     219          36 :   if( FD_UNLIKELY( !built_for ) ) {
     220           3 :     ulong per_epoch_bytes = fd_gui_hist_bytes_per_epoch();
     221           3 :     ulong epoch_n = fd_ulong_min( fd_ulong_max( FD_GUI_HIST_MIN_EPOCHS, store_bytes / fd_ulong_max( per_epoch_bytes, 1UL ) ), FD_GUI_HIST_MAX_EPOCHS );
     222           3 :     FD_TEST( FD_GUI_HIST_MAX_EPOCHS>=FD_GUI_HIST_MIN_EPOCHS );
     223             : 
     224          36 :     for( int i=0; i<FD_GUI_HIST_CNT; i++ ) {
     225          33 :       int   ts     = fd_gui_hist_is_timeseries( i );
     226          33 :       ulong rec_sz = fd_gui_hist_rec_sz( i );
     227          33 :       ulong key_sz = ts ? 0UL : fd_gui_hist_key_sz( i ); /* TS DBs have no key (0) */
     228          33 :       ulong val_sz = ts ? fd_ulong_max( rec_sz, 1UL ) : rec_sz;
     229          33 :       ulong max_records = 0UL;
     230          33 :       if( !ts ) {
     231           9 :         max_records = ( fd_gui_hist_keyshape( i )==FD_GUI_HIST_KEYSHAPE_EPOCH ) ? epoch_n : epoch_n * MAX_SLOTS_PER_EPOCH;
     232           9 :       }
     233             : 
     234          33 :       descs[ i ].name        = names[ i ];
     235          33 :       descs[ i ].kind        = ts ? FD_GUI_STORE_KIND_TS : FD_GUI_STORE_KIND_KV;
     236          33 :       descs[ i ].key_off     = 0UL; /* the key is the leading field(s) of the value */
     237          33 :       descs[ i ].key_sz      = key_sz;
     238          33 :       descs[ i ].key_hash    = ts ? NULL : fd_gui_hist_key_hash( i );
     239          33 :       descs[ i ].key_cmp     = ts ? NULL : fd_gui_hist_key_cmp( i );
     240          33 :       descs[ i ].val_sz      = val_sz;
     241          33 :       descs[ i ].val_align   = 8UL;
     242          33 :       descs[ i ].ts_off      = ts ? fd_gui_hist_dbi_ts_off( i ) : 0UL;
     243          33 :       descs[ i ].granularity = ts ? (ulong)fd_gui_hist_dbi_res_ns( i ) : 0UL;
     244          33 :       descs[ i ].max_records = max_records;
     245          33 :     }
     246           3 :     FD_COMPILER_MFENCE();
     247           3 :     built_for = store_bytes;
     248           3 :   }
     249          36 :   return descs;
     250          36 : }
     251             : 
     252             : /* ---- space-pressure eviction -----------------------------------------
     253             : 
     254             :    Eviction is a small resumable state machine, advanced in small batches
     255             :    at an infrequent cadence.  It evicts the oldest epoch as a whole, in
     256             :    phases:
     257             : 
     258             :      IDLE       -> nothing in progress (the common case)
     259             :      SLOT       -> deleting the epoch's (slot,bank_seq) KV rows
     260             :      TIMESERIES -> deleting the epoch's TS rows (by wallclock window)
     261             :      EPOCH      -> deleting the EPOCH record itself, then -> IDLE
     262             : 
     263             :    The trigger is space, not age: eviction happens eagely as utilization
     264             :    crosses hysteresis thresholds. */
     265             : 
     266        1026 : #define FD_GUI_HIST_EVICT_IDLE       (0)
     267         192 : #define FD_GUI_HIST_EVICT_SLOT       (1)
     268         609 : #define FD_GUI_HIST_EVICT_TIMESERIES (2)
     269         126 : #define FD_GUI_HIST_EVICT_EPOCH      (3)
     270             : 
     271             : /* High/low water marks as a fraction (in 1/100ths) of the configured map
     272             :    size. */
     273           0 : #define FD_GUI_HIST_EVICT_HIGH_PCT (99UL)
     274           0 : #define FD_GUI_HIST_EVICT_LOW_PCT  (95UL)
     275             : 
     276             : /* Maximum number of records deleted per evition iteration. */
     277         738 : #define FD_GUI_HIST_EVICT_BATCH (512UL)
     278             : 
     279             : struct fd_gui_hist_private {
     280             :   ulong magic;          /* ==FD_GUI_HIST_MAGIC after fd_gui_hist_new */
     281             : 
     282             :   struct {
     283             :     int   armed;        /* 1 once over the high-water mark, until under low  */
     284             :     int   phase;        /* FD_GUI_HIST_EVICT_*                               */
     285             :     ulong epoch;        /* epoch being evicted                              */
     286             :     ulong start_slot;   /* first slot of the epoch (inclusive)              */
     287             :     ulong end_slot;     /* last slot of the epoch (inclusive)               */
     288             :     ulong window_hi;    /* last TS window of the epoch (inclusive)  */
     289             :     int   have_ts;      /* 1 if the epoch has any TS to evict       */
     290             :     int   cur_dbi;      /* DB the current phase is mid-scan on              */
     291             :   } evict;
     292             : 
     293             :   fd_gui_hist_metrics_t metrics;
     294             : };
     295             : 
     296             : FD_FN_CONST ulong
     297         396 : fd_gui_hist_align( void ) {
     298         396 :   return 128UL;
     299         396 : }
     300             : 
     301             : FD_FN_CONST ulong
     302          54 : fd_gui_hist_footprint( void ) {
     303          54 :   ulong l = FD_LAYOUT_INIT;
     304          54 :   l = FD_LAYOUT_APPEND( l, fd_gui_hist_align(), sizeof(fd_gui_hist_t) );
     305          54 :   return FD_LAYOUT_FINI( l, fd_gui_hist_align() );
     306          54 : }
     307             : 
     308             : void *
     309             : fd_gui_hist_new( void *                 mem,
     310          18 :                  fd_gui_store_t const * db ) {
     311          18 :   if( FD_UNLIKELY( !mem ) ) { FD_LOG_WARNING(( "fd_gui_hist_new: null mem" )); return NULL; }
     312          18 :   if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)mem, fd_gui_hist_align() ) ) ) { FD_LOG_WARNING(( "fd_gui_hist_new: misaligned mem" )); return NULL; }
     313          18 :   if( FD_UNLIKELY( !db ) ) { FD_LOG_WARNING(( "fd_gui_hist_new: null db" )); return NULL; }
     314             : 
     315          18 :   ulong store_bytes = fd_gui_store_size( db );
     316          18 :   ulong min_bytes   = FD_GUI_HIST_MIN_EPOCHS * fd_gui_hist_bytes_per_epoch() + fd_gui_store_min_overhead_bytes();
     317          18 :   if( FD_UNLIKELY( store_bytes<min_bytes ) ) {
     318           0 :     FD_LOG_WARNING(( "fd_gui_hist_new: store size %lu bytes too small; must be >= %lu bytes", store_bytes, min_bytes ));
     319           0 :     return NULL;
     320           0 :   }
     321             : 
     322          18 :   fd_memset( mem, 0, sizeof(fd_gui_hist_t) );
     323          18 :   FD_SCRATCH_ALLOC_INIT( l, mem );
     324          18 :   fd_gui_hist_t * hist = FD_SCRATCH_ALLOC_APPEND( l, fd_gui_hist_align(), sizeof(fd_gui_hist_t) );
     325          18 :   hist->evict.phase = FD_GUI_HIST_EVICT_IDLE;
     326          18 :   FD_SCRATCH_ALLOC_FINI( l, fd_gui_hist_align() );
     327             : 
     328          18 :   FD_COMPILER_MFENCE();
     329          18 :   hist->magic = FD_GUI_HIST_MAGIC;
     330          18 :   FD_COMPILER_MFENCE();
     331          18 :   return mem;
     332          18 : }
     333             : 
     334             : fd_gui_hist_t *
     335          18 : fd_gui_hist_join( void * mem ) {
     336          18 :   if( FD_UNLIKELY( !mem ) ) return NULL;
     337          18 :   fd_gui_hist_t * hist = (fd_gui_hist_t *)mem;
     338          18 :   if( FD_UNLIKELY( hist->magic!=FD_GUI_HIST_MAGIC ) ) { FD_LOG_WARNING(( "fd_gui_hist_join: bad magic" )); return NULL; }
     339          18 :   return hist;
     340          18 : }
     341             : 
     342             : void *
     343           0 : fd_gui_hist_leave( fd_gui_hist_t * hist ) {
     344           0 :   return (void *)hist;
     345           0 : }
     346             : 
     347             : void *
     348           0 : fd_gui_hist_delete( void * mem ) {
     349           0 :   if( FD_UNLIKELY( !mem ) ) return NULL;
     350           0 :   ((fd_gui_hist_t *)mem)->magic = 0UL;
     351           0 :   return mem;
     352           0 : }
     353             : 
     354             : static inline fd_gui_hist_t *
     355         882 : fd_gui_hist( fd_gui_t * gui ) {
     356         882 :   return (fd_gui_hist_t *)gui->hist;
     357         882 : }
     358             : 
     359             : static inline fd_gui_store_t *
     360       17418 : fd_gui_hist_db( fd_gui_t * gui ) {
     361       17418 :   return (fd_gui_store_t *)gui->db;
     362       17418 : }
     363             : 
     364             : static int
     365           0 : fd_gui_hist_reserve_evict_step( fd_gui_t * gui ) {
     366           0 :   if( FD_LIKELY( fd_gui_hist_evict_oldest( gui ) ) ) return 1;
     367           0 :   return fd_gui_hist_evict_ts_oldest( gui );
     368           0 : }
     369             : 
     370             : static int
     371        6573 : fd_gui_hist_reserve( fd_gui_t * gui, int dbi ) {
     372        6573 :   fd_gui_store_t * db = fd_gui_hist_db( gui );
     373        6573 :   (void)dbi;
     374        6573 :   if( FD_LIKELY( fd_gui_store_free_region_cnt( db )>0UL ) ) return 0; /* fast path: room already */
     375             : 
     376           0 :   int evicted = 0;
     377           0 :   while( fd_gui_store_free_region_cnt( db )==0UL ) {
     378           0 :     if( FD_UNLIKELY( !fd_gui_hist_reserve_evict_step( gui ) ) ) break; /* genuinely full */
     379           0 :     evicted = 1;
     380           0 :   }
     381           0 :   return evicted;
     382        6573 : }
     383             : 
     384             : static int
     385           0 : fd_gui_hist_map_full_evict_step( fd_gui_t * gui ) {
     386           0 :   return fd_gui_hist_reserve_evict_step( gui );
     387           0 : }
     388             : 
     389             : fd_gui_hist_metrics_t const *
     390           0 : fd_gui_hist_metrics( fd_gui_t const * gui ) {
     391           0 :   if( FD_UNLIKELY( !gui->hist ) ) return NULL;
     392           0 :   return &((fd_gui_hist_t const *)gui->hist)->metrics;
     393           0 : }
     394             : 
     395             : void *
     396             : fd_gui_hist_kv_get_or_create( fd_gui_t *   gui,
     397             :                               int          dbi,
     398        3360 :                               void const * key ) {
     399        3360 :   if( FD_UNLIKELY( fd_gui_hist_is_timeseries( dbi ) ) ) { FD_LOG_WARNING(( "fd_gui_hist_kv_get_or_create: dbi %d is time-series", dbi )); return NULL; }
     400             : 
     401        3360 :   int forced_eviction = fd_gui_hist_reserve( gui, dbi );
     402        3360 :   void * val = NULL;
     403        3360 :   int rc;
     404        3360 :   for(;;) {
     405        3360 :     rc = fd_gui_store_kv_get_or_create( fd_gui_hist_db( gui ), (ulong)dbi, key, &val );
     406        3360 :     if( FD_LIKELY( rc==FD_GUI_STORE_SUCCESS ) ) {
     407        3360 :       if( FD_UNLIKELY( forced_eviction ) ) fd_gui_hist( gui )->metrics.reserves[ dbi ]++;
     408        3360 :       return val;
     409        3360 :     }
     410           0 :     if( FD_LIKELY( rc!=FD_GUI_STORE_MAP_FULL ) ) break;
     411           0 :     if( FD_UNLIKELY( !fd_gui_hist_map_full_evict_step( gui ) ) ) break;
     412           0 :     forced_eviction = 1;
     413           0 :   }
     414           0 :   if( FD_UNLIKELY( rc==FD_GUI_STORE_MAP_FULL ) ) {
     415           0 :     fd_gui_hist( gui )->metrics.map_full[ dbi ]++;
     416           0 :     FD_LOG_WARNING(( "fd_gui_hist_kv_get_or_create: dropping a record for dbi %d; store full and nothing left to evict", dbi ));
     417           0 :   }
     418           0 :   return NULL;
     419        3360 : }
     420             : 
     421             : int
     422             : fd_gui_hist_ts_append( fd_gui_t *   gui,
     423             :                        int          dbi,
     424             :                        long         now,
     425             :                        long         ts_ns,
     426        3213 :                        void const * val ) {
     427        3213 :   if( FD_UNLIKELY( dbi<0 || dbi>=FD_GUI_HIST_CNT ) ) { FD_LOG_WARNING(( "fd_gui_hist_ts_append: bad dbi %d", dbi )); return -1; }
     428        3213 :   if( FD_UNLIKELY( !fd_gui_hist_is_timeseries( dbi ) ) ) { FD_LOG_WARNING(( "fd_gui_hist_ts_append: dbi %d is not time-series", dbi )); return -1; }
     429        3213 :   fd_gui_store_t * db = fd_gui_hist_db( gui );
     430             : 
     431        3213 :   ulong rec_sz = fd_gui_hist_rec_sz( dbi );
     432        3213 :   if( FD_UNLIKELY( !rec_sz ) ) { FD_LOG_WARNING(( "fd_gui_hist_ts_append: dbi %d has no record type", dbi )); return -1; }
     433             : 
     434             :   /* Clamp the record's timestamp to a bounded skew around `now` and
     435             :      write the clamped value back into the record. */
     436        3213 :   long clamped_ts = fd_long_max( now-FD_GUI_HIST_TS_SKEW_NS, fd_long_min( ts_ns, now+FD_GUI_HIST_TS_SKEW_NS ) );
     437             : 
     438        3213 :   uchar buf[ FD_GUI_HIST_TS_SZ_MAX ];
     439        3213 :   if( FD_UNLIKELY( rec_sz>sizeof(buf) ) ) { FD_LOG_WARNING(( "fd_gui_hist_ts_append: dbi %d record too large (%lu)", dbi, rec_sz )); return -1; }
     440        3213 :   fd_memcpy( buf, val, rec_sz );
     441        3213 :   *(long *)( buf + fd_gui_hist_dbi_ts_off( dbi ) ) = clamped_ts;
     442             : 
     443             :   /* Reserve space ahead of the append. */
     444        3213 :   int forced_eviction = fd_gui_hist_reserve( gui, dbi );
     445        3213 :   int rc;
     446        3213 :   for(;;) {
     447        3213 :     rc = fd_gui_store_ts_append( db, (ulong)dbi, buf );
     448        3213 :     if( FD_LIKELY( rc==FD_GUI_STORE_SUCCESS ) ) {
     449        3213 :       if( FD_UNLIKELY( forced_eviction ) ) fd_gui_hist( gui )->metrics.reserves[ dbi ]++;
     450        3213 :       return 0;
     451        3213 :     }
     452           0 :     if( FD_LIKELY( rc!=FD_GUI_STORE_MAP_FULL ) ) break;
     453           0 :     if( FD_UNLIKELY( !fd_gui_hist_map_full_evict_step( gui ) ) ) break;
     454           0 :     forced_eviction = 1;
     455           0 :   }
     456           0 :   if( FD_UNLIKELY( rc==FD_GUI_STORE_MAP_FULL ) ) {
     457           0 :     fd_gui_hist( gui )->metrics.map_full[ dbi ]++;
     458           0 :     FD_LOG_WARNING(( "fd_gui_hist_ts_append: dropping a record for dbi %d; store full and nothing left to evict", dbi ));
     459           0 :   }
     460           0 :   return -1;
     461        3213 : }
     462             : 
     463             : 
     464             : static void
     465        3753 : fd_gui_hist_iter_load( fd_gui_hist_iter_t * iter ) {
     466        3753 :   for(;;) {
     467        3753 :     if( fd_gui_store_ts_scan_done( &iter->_it ) ) { iter->rec = NULL; return; }
     468        3702 :     void const * rec = iter->_it.rec; /* the gui record verbatim; self-describing */
     469        3702 :     if( !iter->_filter || iter->_filter( rec, iter->_filter_ctx ) ) {
     470        3702 :       iter->rec = rec;
     471        3702 :       return;
     472        3702 :     }
     473           0 :     fd_gui_store_ts_scan_next( &iter->_it );
     474           0 :   }
     475        3753 : }
     476             : 
     477             : int
     478             : fd_gui_hist_range_begin( fd_gui_t *                   gui,
     479             :                          fd_gui_hist_iter_t *         iter,
     480             :                          int                          dbi,
     481             :                          long                         lo_ns,
     482             :                          long                         hi_ns,
     483             :                          fd_gui_hist_range_filter_fn  filter,
     484          51 :                          void *                       filter_ctx ) {
     485          51 :   fd_gui_store_t * db = fd_gui_hist_db( gui );
     486          51 :   memset( iter, 0, sizeof(fd_gui_hist_iter_t) );
     487             : 
     488          51 :   if( FD_UNLIKELY( !fd_gui_hist_is_timeseries( dbi ) ) ) { FD_LOG_WARNING(( "fd_gui_hist_range_begin: dbi %d is not time-series", dbi )); return -1; }
     489             : 
     490          51 :   FD_TEST( lo_ns!=LONG_MIN && hi_ns!=LONG_MAX ); /* open-ended queries not supported */
     491             : 
     492          51 :   long res_ns = fd_gui_hist_dbi_res_ns( dbi );
     493          51 :   ulong window_lo = (lo_ns<=0L) ? 0UL : fd_gui_hist_window( lo_ns, res_ns );
     494          51 :   ulong window_hi = fd_gui_hist_window( hi_ns, res_ns );
     495             : 
     496          51 :   iter->_dbi        = dbi;
     497          51 :   iter->rec_sz      = fd_gui_hist_rec_sz( dbi );
     498          51 :   iter->_filter     = filter;
     499          51 :   iter->_filter_ctx = filter_ctx;
     500             : 
     501          51 :   fd_gui_store_ts_scan_begin( db, &iter->_it, (ulong)dbi, window_lo, window_hi, NULL, NULL );
     502          51 :   fd_gui_hist_iter_load( iter );
     503          51 :   return 0;
     504          51 : }
     505             : 
     506             : int
     507        3753 : fd_gui_hist_range_next( fd_gui_hist_iter_t * iter ) {
     508        3753 :   if( FD_UNLIKELY( !iter->rec ) ) return 0;
     509        3744 :   if( iter->_emitted ) {
     510        3702 :     fd_gui_store_ts_scan_next( &iter->_it );
     511        3702 :     fd_gui_hist_iter_load( iter );
     512        3702 :     if( !iter->rec ) return 0;
     513        3702 :   }
     514        3702 :   iter->_emitted = 1;
     515        3702 :   return 1;
     516        3744 : }
     517             : 
     518             : void
     519          51 : fd_gui_hist_range_end( fd_gui_hist_iter_t * iter ) {
     520          51 :   fd_gui_store_ts_scan_end( &iter->_it );
     521          51 : }
     522             : 
     523             : void *
     524             : fd_gui_hist_kv_get( fd_gui_t *   gui,
     525             :                     int          dbi,
     526          87 :                     void const * key ) {
     527          87 :   if( FD_UNLIKELY( fd_gui_hist_is_timeseries( dbi ) ) ) { FD_LOG_WARNING(( "fd_gui_hist_meta_get: dbi %d is time-series", dbi )); return NULL; }
     528          87 :   return fd_gui_store_kv_get( fd_gui_hist_db( gui ), (ulong)dbi, key );
     529          87 : }
     530             : 
     531             : void *
     532             : fd_gui_hist_kv_get_slot_any( fd_gui_t * gui,
     533             :                              int        dbi,
     534        3258 :                              ulong      slot ) {
     535        3258 :   if( FD_UNLIKELY( fd_gui_hist_keyshape( dbi )!=FD_GUI_HIST_KEYSHAPE_SLOT_BANK ) ) {
     536           0 :     FD_LOG_WARNING(( "fd_gui_hist_kv_get_slot_any: dbi %d is not slot/bank-keyed", dbi ));
     537           0 :     return NULL;
     538           0 :   }
     539             : 
     540        3258 :   fd_gui_hist_slot_key_t key = { .slot=slot, .bank_seq=ULONG_MAX };
     541        3258 :   return fd_gui_store_kv_get_any( fd_gui_hist_db( gui ), (ulong)dbi, &key );
     542        3258 : }
     543             : 
     544             : /* fd_gui_hist_ts_iter_load reads the iterator's current backend record into
     545             :    the typed iterator fields (or marks it done). */
     546             : static void
     547           0 : fd_gui_hist_ts_iter_load( fd_gui_hist_kv_slot_iter_t * iter ) {
     548           0 :   if( FD_UNLIKELY( fd_gui_store_kv_iter_done( &iter->_it ) ) ) {
     549           0 :     iter->rec      = NULL;
     550           0 :     iter->bank_seq = ULONG_MAX;
     551           0 :     return;
     552           0 :   }
     553           0 :   iter->rec      = iter->_it.rec;
     554           0 :   iter->bank_seq = ( iter->_it.key_sz>=2UL*sizeof(ulong) )
     555           0 :                  ? ((ulong const *)iter->_it.key)[ 1 ]
     556           0 :                  : ULONG_MAX;
     557           0 : }
     558             : 
     559             : fd_gui_hist_kv_slot_iter_t *
     560             : fd_gui_hist_kv_iter_begin( fd_gui_t *                   gui,
     561             :                            fd_gui_hist_kv_slot_iter_t * iter,
     562             :                            int                          dbi,
     563           0 :                            ulong                        slot ) {
     564           0 :   iter->rec      = NULL;
     565           0 :   iter->bank_seq = ULONG_MAX;
     566           0 :   if( FD_UNLIKELY( fd_gui_hist_keyshape( dbi )!=FD_GUI_HIST_KEYSHAPE_SLOT_BANK ) ) {
     567           0 :     FD_LOG_WARNING(( "fd_gui_hist_kv_iter_begin: dbi %d is not slot/bank-keyed", dbi ));
     568           0 :     fd_gui_hist_slot_key_t none = { .slot=ULONG_MAX, .bank_seq=ULONG_MAX };
     569           0 :     fd_gui_store_kv_iter_begin( fd_gui_hist_db( gui ), &iter->_it, (ulong)dbi, &none );
     570           0 :     return iter;
     571           0 :   }
     572           0 :   fd_gui_hist_slot_key_t key = { .slot=slot, .bank_seq=ULONG_MAX };
     573           0 :   fd_gui_store_kv_iter_begin( fd_gui_hist_db( gui ), &iter->_it, (ulong)dbi, &key );
     574           0 :   fd_gui_hist_ts_iter_load( iter );
     575           0 :   return iter;
     576           0 : }
     577             : 
     578             : int
     579           0 : fd_gui_hist_kv_iter_next( fd_gui_hist_kv_slot_iter_t * iter ) {
     580           0 :   if( FD_UNLIKELY( !iter->rec ) ) return 0;
     581           0 :   fd_gui_store_kv_iter_next( &iter->_it );
     582           0 :   fd_gui_hist_ts_iter_load( iter );
     583           0 :   return iter->rec!=NULL;
     584           0 : }
     585             : 
     586             : /* fd_gui_hist_evict_used_pct returns the store's high-water-mark fill level
     587             :    as a percentage (0..100) of its configured map size. */
     588             : 
     589             : static ulong
     590           0 : fd_gui_hist_evict_used_pct( fd_gui_t * gui ) {
     591           0 :   fd_gui_store_t * db = fd_gui_hist_db( gui );
     592           0 :   ulong size = fd_gui_store_size( db );
     593           0 :   if( FD_UNLIKELY( !size ) ) return 0UL;
     594           0 :   return ( fd_gui_store_used_bytes( db ) * 100UL ) / size;
     595           0 : }
     596             : 
     597             : /* fd_gui_hist_evict_slot_completed_window returns, in *out_window, the
     598             :    wallclock window (floored completion time) of the lowest-bank_seq
     599             :    SLOT record for `slot`, or 0 if there is no such record (or it
     600             :    has no completion time).  Used to bound the TS eviction window.
     601             :    Returns 1 on success. */
     602             : 
     603             : static int
     604             : fd_gui_hist_evict_slot_completed_window( fd_gui_t * gui,
     605             :                                          ulong      slot,
     606          63 :                                          ulong *    out_window ) {
     607          63 :   fd_gui_slot_t const * rmeta = fd_gui_hist_kv_get_slot_any( gui, FD_GUI_HIST_SLOT, slot );
     608          63 :   if( FD_UNLIKELY( !rmeta ) ) return 0;
     609          60 :   if( FD_UNLIKELY( rmeta->completed_time==LONG_MAX ) ) return 0;
     610          60 :   *out_window = fd_gui_hist_window( rmeta->completed_time, FD_GUI_HIST_RES_1S_NS );
     611          60 :   return 1;
     612          60 : }
     613             : 
     614             : /* fd_gui_hist_evict_begin sets up an eviction cascade for the oldest epoch.
     615             :    Returns 1 if a cascade was armed (state populated, phase advanced past
     616             :    IDLE), 0 if there is nothing to evict. */
     617             : 
     618             : static int
     619          72 : fd_gui_hist_evict_begin( fd_gui_t * gui ) {
     620          72 :   fd_gui_hist_t * hist = fd_gui_hist( gui );
     621             : 
     622             :   /* Make sure we always keep the current/next epoch. */
     623          72 :   if( FD_UNLIKELY( gui->epoch.stored_epoch_cnt<FD_GUI_HIST_MIN_EPOCHS ) ) return 0;
     624             : 
     625          63 :   fd_gui_epoch_t const * meta = fd_gui_store_kv_get_any( fd_gui_hist_db( gui ), (ulong)FD_GUI_HIST_EPOCH, NULL );
     626          63 :   if( FD_UNLIKELY( !meta ) ) return 0;
     627          63 :   ulong epoch      = meta->epoch;
     628          63 :   ulong meta_start = meta->start_slot;
     629          63 :   ulong meta_cnt   = meta->slot_cnt;
     630          63 :   ulong start_slot = meta_start;
     631          63 :   ulong end_slot   = meta_start + meta_cnt - 1UL;
     632          63 :   ulong next_start = meta_start + meta_cnt; /* next epoch's first slot; always valid (>= FD_GUI_HIST_MIN_EPOCHS resident) */
     633             : 
     634          63 :   ulong window_hi = 0UL;
     635          63 :   int   have_ts   = 0;
     636          63 :   ulong next_window;
     637          63 :   if( FD_LIKELY( fd_gui_hist_evict_slot_completed_window( gui, next_start, &next_window ) ) ) {
     638          60 :     window_hi = ( next_window>0UL ) ? ( next_window-1UL ) : 0UL;
     639          60 :     have_ts   = next_window>0UL;
     640          60 :   }
     641             : 
     642          63 :   hist->evict.epoch      = epoch;
     643          63 :   hist->evict.start_slot = start_slot;
     644          63 :   hist->evict.end_slot   = end_slot;
     645          63 :   hist->evict.window_hi  = window_hi;
     646          63 :   hist->evict.have_ts    = have_ts;
     647          63 :   hist->evict.phase      = FD_GUI_HIST_EVICT_SLOT;
     648          63 :   hist->evict.cur_dbi    = FD_GUI_HIST_SLOT;
     649          63 :   return 1;
     650          63 : }
     651             : 
     652             : /* fd_gui_hist_evict_slot_batch advances KV DB `dbi`'s watermark to
     653             :    evict the (slot,bank_seq) rows with slot <= end_slot, decrementing
     654             :    *budget per reclaimed row.  Returns 1 if the DB's range is fully
     655             :    drained, 0 if it stopped because the budget ran out. */
     656             : 
     657             : static int
     658             : fd_gui_hist_evict_slot_batch( fd_gui_t * gui,
     659             :                               int        dbi,
     660             :                               ulong      end_slot,
     661         129 :                               ulong *    budget ) {
     662         129 :   fd_gui_hist_slot_key_t hi = { .slot=end_slot+1UL, .bank_seq=0UL };
     663         129 :   int drained = 1;
     664         129 :   fd_gui_store_kv_evict( fd_gui_hist_db( gui ), (ulong)dbi, &hi, budget, &drained );
     665         129 :   return drained;
     666         129 : }
     667             : 
     668             : /* fd_gui_hist_evict_ts_batch advances TS DB `dbi`'s watermark to evict
     669             :    rows with window <= window_hi, decrementing *budget per reclaimed row.
     670             :    Returns 1 if drained, 0 if it stopped on the budget. */
     671             : 
     672             : static int
     673             : fd_gui_hist_evict_ts_batch( fd_gui_t * gui,
     674             :                             int        dbi,
     675             :                             ulong      window_hi,
     676         483 :                             ulong *    budget ) {
     677         483 :   int drained = 1;
     678         483 :   fd_gui_store_ts_evict( fd_gui_hist_db( gui ), (ulong)dbi, window_hi+1UL, budget, &drained );
     679         483 :   return drained;
     680         483 : }
     681             : 
     682             : /* fd_gui_hist_evict_one advances an in-progress cascade by one bounded batch.
     683             :    Returns 1 (it always does work, or
     684             :    resolves the cascade, when not IDLE). */
     685             : 
     686             : static int
     687         738 : fd_gui_hist_evict_one( fd_gui_t * gui ) {
     688         738 :   fd_gui_hist_t * hist   = fd_gui_hist( gui );
     689         738 :   ulong           budget = FD_GUI_HIST_EVICT_BATCH;
     690             : 
     691         738 :   switch( hist->evict.phase ) {
     692             : 
     693         129 :   case FD_GUI_HIST_EVICT_SLOT: {
     694         129 :     int drained = fd_gui_hist_evict_slot_batch( gui, hist->evict.cur_dbi, hist->evict.end_slot, &budget );
     695         129 :     if( !drained ) return 1; /* budget spent on this DB; resume next step */
     696             :     /* advance to the next slot-keyed KV DB, or to the TS phase */
     697         126 :     if( hist->evict.cur_dbi==FD_GUI_HIST_SLOT ) {
     698          63 :       hist->evict.cur_dbi = FD_GUI_HIST_LEADER_SLOT;
     699          63 :     } else {
     700          63 :       hist->evict.phase   = FD_GUI_HIST_EVICT_TIMESERIES;
     701          63 :       hist->evict.cur_dbi = 0; /* fd_gui_hist_evict_one finds the first TS DB below */
     702          63 :     }
     703         126 :     return 1;
     704         129 :   }
     705             : 
     706         546 :   case FD_GUI_HIST_EVICT_TIMESERIES: {
     707             :     /* skip non-TS DBs (the KV DBs interleave by index) */
     708         726 :     while( hist->evict.cur_dbi<FD_GUI_HIST_CNT && !fd_gui_hist_is_timeseries( hist->evict.cur_dbi ) ) hist->evict.cur_dbi++;
     709         546 :     if( hist->evict.cur_dbi>=FD_GUI_HIST_CNT || !hist->evict.have_ts ) {
     710          63 :       hist->evict.phase = FD_GUI_HIST_EVICT_EPOCH;
     711          63 :       return 1;
     712          63 :     }
     713         483 :     int drained = fd_gui_hist_evict_ts_batch( gui, hist->evict.cur_dbi, hist->evict.window_hi, &budget );
     714         483 :     if( !drained ) return 1;
     715         480 :     hist->evict.cur_dbi++; /* next step picks up the next TS DB (or the EPOCH phase) */
     716         480 :     return 1;
     717         483 :   }
     718             : 
     719          63 :   case FD_GUI_HIST_EVICT_EPOCH: {
     720          63 :     fd_gui_hist_epoch_key_t hi = { .epoch=hist->evict.epoch+1UL };
     721             : 
     722          63 :     fd_gui_store_t * db      = fd_gui_hist_db( gui );
     723          63 :     int           drained = 1;
     724          63 :     fd_gui_store_kv_evict( db, (ulong)FD_GUI_HIST_EPOCH, &hi, &budget, &drained );
     725          63 :     if( !drained ) return 1; /* budget spent; resume this phase next step */
     726          63 :     if( FD_LIKELY( gui->epoch.stored_epoch_cnt ) ) gui->epoch.stored_epoch_cnt--;
     727             : 
     728          63 :     hist->evict.phase = FD_GUI_HIST_EVICT_IDLE; /* cascade complete */
     729          63 :     return 1;
     730          63 :   }
     731             : 
     732           0 :   default:
     733           0 :     hist->evict.phase = FD_GUI_HIST_EVICT_IDLE;
     734           0 :     return 0;
     735         738 :   }
     736         738 : }
     737             : 
     738             : int
     739           0 : fd_gui_hist_evict_step( fd_gui_t * gui ) {
     740           0 :   if( FD_UNLIKELY( !gui->db || !gui->hist ) ) return 0;
     741           0 :   fd_gui_hist_t * hist = fd_gui_hist( gui );
     742             : 
     743           0 :   if( hist->evict.phase==FD_GUI_HIST_EVICT_IDLE ) {
     744           0 :     ulong used_pct = fd_gui_hist_evict_used_pct( gui );
     745           0 :     if( used_pct>=FD_GUI_HIST_EVICT_HIGH_PCT ) hist->evict.armed = 1;
     746           0 :     else if( used_pct<FD_GUI_HIST_EVICT_LOW_PCT ) hist->evict.armed = 0;
     747           0 :     if( !hist->evict.armed ) return 0;
     748           0 :     if( FD_UNLIKELY( !fd_gui_hist_evict_begin( gui ) ) ) { hist->evict.armed = 0; return 0; } /* nothing to evict */
     749           0 :     return 1;
     750           0 :   }
     751             : 
     752           0 :   return fd_gui_hist_evict_one( gui );
     753           0 : }
     754             : 
     755             : int
     756          72 : fd_gui_hist_evict_oldest( fd_gui_t * gui ) {
     757          72 :   fd_gui_hist_t * hist = fd_gui_hist( gui );
     758          72 :   if( FD_UNLIKELY( !gui->db || !gui->hist ) ) return 0;
     759          72 :   if( hist->evict.phase==FD_GUI_HIST_EVICT_IDLE && FD_UNLIKELY( !fd_gui_hist_evict_begin( gui ) ) ) return 0;
     760         801 :   while( hist->evict.phase!=FD_GUI_HIST_EVICT_IDLE ) fd_gui_hist_evict_one( gui );
     761          63 :   return 1;
     762          72 : }
     763             : 
     764             : /* fd_gui_hist_ts_oldest_window finds the oldest live window across all
     765             :    TS DBs (the global minimum of each DB's oldest live record's window).
     766             :    Stores it in *out_window and returns 1 if any TS record exists, 0 if
     767             :    every TS DB is empty.  O(1) per DB: no scan. */
     768             : 
     769             : static int
     770             : fd_gui_hist_ts_oldest_window( fd_gui_t * gui,
     771          18 :                               ulong *    out_window ) {
     772          18 :   fd_gui_store_t * db     = fd_gui_hist_db( gui );
     773          18 :   ulong            oldest = ULONG_MAX;
     774          18 :   int              found  = 0;
     775         216 :   for( int dbi=0; dbi<FD_GUI_HIST_CNT; dbi++ ) {
     776         198 :     if( !fd_gui_hist_is_timeseries( dbi ) ) continue;
     777         144 :     ulong window;
     778         144 :     if( fd_gui_store_ts_oldest_window( db, (ulong)dbi, &window ) ) {
     779          30 :       found  = 1;
     780          30 :       oldest = fd_ulong_min( oldest, window );
     781          30 :     }
     782         144 :   }
     783          18 :   if( FD_UNLIKELY( !found ) ) return 0;
     784          15 :   *out_window = oldest;
     785          15 :   return 1;
     786          18 : }
     787             : 
     788             : int
     789          18 : fd_gui_hist_evict_ts_oldest( fd_gui_t * gui ) {
     790          18 :   if( FD_UNLIKELY( !gui->db || !gui->hist ) ) return 0;
     791             : 
     792          18 :   ulong oldest;
     793          18 :   if( FD_UNLIKELY( !fd_gui_hist_ts_oldest_window( gui, &oldest ) ) ) return 0; /* no TS data left */
     794             : 
     795         180 :   for( int dbi=0; dbi<FD_GUI_HIST_CNT; dbi++ ) {
     796         165 :     if( !fd_gui_hist_is_timeseries( dbi ) ) continue;
     797         120 :     ulong budget  = ULONG_MAX;
     798         120 :     int   drained = 1;
     799         120 :     fd_gui_store_ts_evict( fd_gui_hist_db( gui ), (ulong)dbi, oldest+1UL, &budget, &drained );
     800         120 :   }
     801          15 :   return 1;
     802          18 : }

Generated by: LCOV version 1.14