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

Generated by: LCOV version 1.14