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 : }
|