Line data Source code
1 : #include "fd_gui_hist.h"
2 : #include "fd_gui_store.h"
3 : #include "fd_gui_shred.h"
4 : #include "fd_gui.h" /* fd_gui_t, record types */
5 :
6 : #include <stddef.h> /* offsetof */
7 :
8 : /* Every record type must fit in one store region (header + record). */
9 : FD_STATIC_ASSERT( sizeof(fd_gui_shred_batch_t )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
10 : FD_STATIC_ASSERT( sizeof(fd_gui_tile_timers_hist_t)<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
11 : FD_STATIC_ASSERT( sizeof(fd_gui_scheduler_counts_t)<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
12 : FD_STATIC_ASSERT( sizeof(fd_gui_tile_stats_t )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
13 : FD_STATIC_ASSERT( sizeof(fd_gui_txn_waterfall_t )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
14 : FD_STATIC_ASSERT( sizeof(fd_gui_store_txn_start_t )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
15 : FD_STATIC_ASSERT( sizeof(fd_gui_store_txn_end_t )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
16 : FD_STATIC_ASSERT( sizeof(fd_gui_store_replay_txn_t)<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
17 : FD_STATIC_ASSERT( sizeof(fd_gui_slot_t )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
18 : FD_STATIC_ASSERT( sizeof(fd_gui_leader_slot_t )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
19 : FD_STATIC_ASSERT( sizeof(fd_gui_epoch_t )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
20 : FD_STATIC_ASSERT( sizeof(fd_gui_timeline_day_t )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
21 : FD_STATIC_ASSERT( sizeof(fd_gui_timeline_tile_day_t)<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
22 : FD_STATIC_ASSERT( sizeof(fd_gui_fec_event_t )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
23 : FD_STATIC_ASSERT( sizeof(fd_gui_fec_event_batch_t )<=FD_GUI_STORE_MAX_REC_SZ, rec_fits );
24 :
25 : /* Each key type must alias the matching record field exactly. */
26 : FD_STATIC_ASSERT( offsetof( fd_gui_hist_slot_key_t, slot )==offsetof( fd_gui_slot_t, slot ), key_layout );
27 : FD_STATIC_ASSERT( offsetof( fd_gui_hist_slot_key_t, bank_seq )==offsetof( fd_gui_slot_t, bank_seq ), key_layout );
28 : FD_STATIC_ASSERT( offsetof( fd_gui_hist_leader_slot_key_t, slot )==offsetof( fd_gui_leader_slot_t, slot ), key_layout );
29 : FD_STATIC_ASSERT( offsetof( fd_gui_hist_leader_slot_key_t, bank_seq )==offsetof( fd_gui_leader_slot_t, bank_seq ), key_layout );
30 : FD_STATIC_ASSERT( offsetof( fd_gui_hist_epoch_key_t, epoch )==offsetof( fd_gui_epoch_t, epoch ), key_layout );
31 : FD_STATIC_ASSERT( offsetof( fd_gui_hist_tile_day_key_t, day_idx )==offsetof( fd_gui_timeline_tile_day_t, day_idx ), key_layout );
32 : FD_STATIC_ASSERT( offsetof( fd_gui_hist_tile_day_key_t, tile_idx )==offsetof( fd_gui_timeline_tile_day_t, tile_idx ), key_layout );
33 :
34 0 : #define FD_GUI_HIST_KEYSHAPE_TIMESERIES (0) /* (ts, ...) */
35 0 : #define FD_GUI_HIST_KEYSHAPE_SLOT_BANK (1) /* (slot, bank_seq) */
36 0 : #define FD_GUI_HIST_KEYSHAPE_EPOCH (2) /* (epoch) */
37 0 : #define FD_GUI_HIST_KEYSHAPE_DAY_TILE (3) /* (day_idx, tile_idx) */
38 0 : #define FD_GUI_HIST_KEYSHAPE_FEC (4) /* (16-byte FEC event key) */
39 :
40 : static inline long
41 0 : fd_gui_hist_dbi_res_ns( int dbi FD_PARAM_UNUSED ) {
42 0 : return FD_GUI_HIST_RES_1S_NS; /* all TS DBs use the 1s resolution */
43 0 : }
44 :
45 : static inline ulong
46 0 : fd_gui_hist_dbi_ts_off( int dbi ) {
47 0 : switch( dbi ) {
48 0 : case FD_GUI_HIST_SHRED_EVENTS: return offsetof( fd_gui_shred_batch_t, insert_time_ns );
49 0 : case FD_GUI_HIST_TILE_TIMERS: return offsetof( fd_gui_tile_timers_hist_t, sample_time_nanos );
50 0 : case FD_GUI_HIST_SCHEDULER_COUNTS: return offsetof( fd_gui_scheduler_counts_t, sample_time_ns );
51 0 : case FD_GUI_HIST_TILE_STATS: return offsetof( fd_gui_tile_stats_t, sample_time_nanos );
52 0 : case FD_GUI_HIST_TXN_WATERFALL: return offsetof( fd_gui_txn_waterfall_t, sample_time_nanos );
53 0 : case FD_GUI_HIST_TXN_START: return offsetof( fd_gui_store_txn_start_t, insert_time_ns );
54 0 : case FD_GUI_HIST_TXN_END: return offsetof( fd_gui_store_txn_end_t, insert_time_ns );
55 0 : case FD_GUI_HIST_REPLAY_TXN: return offsetof( fd_gui_store_replay_txn_t, insert_time_ns );
56 0 : case FD_GUI_HIST_TIMELINE_DAY: return offsetof( fd_gui_timeline_day_t, end_time_ns );
57 0 : case FD_GUI_HIST_FEC_EVENT_ARCHIVE: return offsetof( fd_gui_fec_event_batch_t, insert_time_ns );
58 0 : default: return 0UL;
59 0 : }
60 0 : }
61 :
62 : static inline ulong
63 0 : fd_gui_hist_window( long ts_ns, long res_ns ) {
64 0 : return (ulong)( ts_ns / res_ns );
65 0 : }
66 :
67 : static int
68 0 : fd_gui_hist_keyshape( int dbi ) {
69 0 : switch( dbi ) {
70 0 : case FD_GUI_HIST_SCHEDULER_COUNTS:
71 0 : case FD_GUI_HIST_TILE_TIMERS:
72 0 : case FD_GUI_HIST_TILE_STATS:
73 0 : case FD_GUI_HIST_TXN_WATERFALL:
74 0 : case FD_GUI_HIST_SHRED_EVENTS:
75 0 : case FD_GUI_HIST_TXN_START:
76 0 : case FD_GUI_HIST_TXN_END: return FD_GUI_HIST_KEYSHAPE_TIMESERIES;
77 0 : case FD_GUI_HIST_REPLAY_TXN:
78 0 : case FD_GUI_HIST_TIMELINE_DAY:
79 0 : case FD_GUI_HIST_FEC_EVENT_ARCHIVE: return FD_GUI_HIST_KEYSHAPE_TIMESERIES;
80 0 : case FD_GUI_HIST_SLOT:
81 0 : case FD_GUI_HIST_LEADER_SLOT: return FD_GUI_HIST_KEYSHAPE_SLOT_BANK;
82 0 : case FD_GUI_HIST_EPOCH: return FD_GUI_HIST_KEYSHAPE_EPOCH;
83 0 : case FD_GUI_HIST_TIMELINE_TILE_DAY: return FD_GUI_HIST_KEYSHAPE_DAY_TILE;
84 0 : case FD_GUI_HIST_FEC_EVENTS: return FD_GUI_HIST_KEYSHAPE_FEC;
85 0 : default: FD_LOG_ERR(( "invalid dbi %d", dbi )); return -1;
86 0 : }
87 0 : }
88 :
89 : static inline int
90 0 : fd_gui_hist_is_timeseries( int dbi ) {
91 0 : return fd_gui_hist_keyshape( dbi )==FD_GUI_HIST_KEYSHAPE_TIMESERIES;
92 0 : }
93 :
94 : static ulong
95 0 : fd_gui_hist_slot_key_hash( void const * key ) {
96 0 : fd_gui_hist_slot_key_t const * k = key;
97 0 : return fd_ulong_hash( k->slot );
98 0 : }
99 :
100 : static int
101 0 : fd_gui_hist_slot_key_cmp( void const * a, void const * b ) {
102 0 : fd_gui_hist_slot_key_t const * ka = a;
103 0 : fd_gui_hist_slot_key_t const * kb = b;
104 0 : if( ka->slot<kb->slot ) return -1;
105 0 : if( ka->slot>kb->slot ) return 1;
106 0 : if( ka->bank_seq==ULONG_MAX || kb->bank_seq==ULONG_MAX ) return 0;
107 0 : if( ka->bank_seq<kb->bank_seq ) return -1;
108 0 : if( ka->bank_seq>kb->bank_seq ) return 1;
109 0 : return 0;
110 0 : }
111 :
112 : static ulong
113 0 : fd_gui_hist_leader_slot_key_hash( void const * key ) {
114 0 : fd_gui_hist_leader_slot_key_t const * k = key;
115 0 : return fd_ulong_hash( k->slot );
116 0 : }
117 :
118 : static int
119 0 : fd_gui_hist_leader_slot_key_cmp( void const * a, void const * b ) {
120 0 : fd_gui_hist_leader_slot_key_t const * ka = a;
121 0 : fd_gui_hist_leader_slot_key_t const * kb = b;
122 0 : if( ka->slot<kb->slot ) return -1;
123 0 : if( ka->slot>kb->slot ) return 1;
124 0 : if( ka->bank_seq==ULONG_MAX || kb->bank_seq==ULONG_MAX ) return 0;
125 0 : if( ka->bank_seq<kb->bank_seq ) return -1;
126 0 : if( ka->bank_seq>kb->bank_seq ) return 1;
127 0 : return 0;
128 0 : }
129 :
130 : static ulong
131 0 : fd_gui_hist_epoch_key_hash( void const * key ) {
132 0 : fd_gui_hist_epoch_key_t const * k = key;
133 0 : return fd_ulong_hash( k->epoch );
134 0 : }
135 :
136 : static int
137 0 : fd_gui_hist_epoch_key_cmp( void const * a, void const * b ) {
138 0 : fd_gui_hist_epoch_key_t const * ka = a;
139 0 : fd_gui_hist_epoch_key_t const * kb = b;
140 0 : if( ka->epoch<kb->epoch ) return -1;
141 0 : if( ka->epoch>kb->epoch ) return 1;
142 0 : return 0;
143 0 : }
144 :
145 : static ulong
146 0 : fd_gui_hist_tile_day_key_hash( void const * key ) {
147 0 : fd_gui_hist_tile_day_key_t const * k = key;
148 0 : return fd_ulong_hash( k->day_idx ) ^ fd_ulong_hash( k->tile_idx+0x9e3779b97f4a7c15UL );
149 0 : }
150 :
151 : static int
152 0 : fd_gui_hist_tile_day_key_cmp( void const * a, void const * b ) {
153 0 : fd_gui_hist_tile_day_key_t const * ka = a;
154 0 : fd_gui_hist_tile_day_key_t const * kb = b;
155 0 : if( ka->day_idx<kb->day_idx ) return -1;
156 0 : if( ka->day_idx>kb->day_idx ) return 1;
157 0 : if( ka->tile_idx<kb->tile_idx ) return -1;
158 0 : if( ka->tile_idx>kb->tile_idx ) return 1;
159 0 : return 0;
160 0 : }
161 :
162 : static ulong
163 0 : fd_gui_hist_fec_key_hash( void const * key ) {
164 0 : ulong words[ 2 ];
165 0 : fd_memcpy( words, key, sizeof(words) );
166 0 : return fd_ulong_hash( words[ 0 ] ) ^ fd_ulong_hash( words[ 1 ] );
167 0 : }
168 :
169 : static int
170 : fd_gui_hist_fec_key_cmp( void const * a,
171 0 : void const * b ) {
172 0 : return memcmp( a, b, sizeof(fd_gui_fec_event_key_t) );
173 0 : }
174 :
175 : static inline ulong
176 0 : fd_gui_hist_rec_sz( int dbi ) {
177 0 : switch( dbi ) {
178 0 : case FD_GUI_HIST_SHRED_EVENTS: return sizeof(fd_gui_shred_batch_t);
179 0 : case FD_GUI_HIST_TILE_TIMERS: return sizeof(fd_gui_tile_timers_hist_t);
180 0 : case FD_GUI_HIST_SCHEDULER_COUNTS: return sizeof(fd_gui_scheduler_counts_t);
181 0 : case FD_GUI_HIST_TILE_STATS: return sizeof(fd_gui_tile_stats_t);
182 0 : case FD_GUI_HIST_TXN_WATERFALL: return sizeof(fd_gui_txn_waterfall_t);
183 0 : case FD_GUI_HIST_TXN_START: return sizeof(fd_gui_store_txn_start_t);
184 0 : case FD_GUI_HIST_TXN_END: return sizeof(fd_gui_store_txn_end_t);
185 0 : case FD_GUI_HIST_REPLAY_TXN: return sizeof(fd_gui_store_replay_txn_t);
186 0 : case FD_GUI_HIST_SLOT: return sizeof(fd_gui_slot_t);
187 0 : case FD_GUI_HIST_LEADER_SLOT: return sizeof(fd_gui_leader_slot_t);
188 0 : case FD_GUI_HIST_EPOCH: return sizeof(fd_gui_epoch_t);
189 0 : case FD_GUI_HIST_TIMELINE_DAY: return sizeof(fd_gui_timeline_day_t);
190 0 : case FD_GUI_HIST_TIMELINE_TILE_DAY: return sizeof(fd_gui_timeline_tile_day_t);
191 0 : case FD_GUI_HIST_FEC_EVENTS: return sizeof(fd_gui_fec_event_t);
192 0 : case FD_GUI_HIST_FEC_EVENT_ARCHIVE: return sizeof(fd_gui_fec_event_batch_t);
193 0 : default: return 0UL;
194 0 : }
195 0 : }
196 :
197 : /* fd_gui_hist_key_sz returns the KV key width for `dbi`: 16 bytes
198 : (slot, bank_seq) for slot-keyed DBs, 8 bytes for EPOCH, 16 bytes
199 : (day_idx, tile_idx) for TIMELINE_TILE_DAY, 16 bytes for FEC_EVENTS,
200 : 0 for TS. */
201 : static inline ulong
202 0 : fd_gui_hist_key_sz( int dbi ) {
203 0 : switch( dbi ) {
204 0 : case FD_GUI_HIST_SLOT: return sizeof(fd_gui_hist_slot_key_t);
205 0 : case FD_GUI_HIST_LEADER_SLOT: return sizeof(fd_gui_hist_leader_slot_key_t);
206 0 : case FD_GUI_HIST_EPOCH: return sizeof(fd_gui_hist_epoch_key_t);
207 0 : case FD_GUI_HIST_TIMELINE_TILE_DAY: return sizeof(fd_gui_hist_tile_day_key_t);
208 0 : case FD_GUI_HIST_FEC_EVENTS: return sizeof(fd_gui_fec_event_key_t);
209 0 : default: return 0UL; /* TS DBs have no key */
210 0 : }
211 0 : }
212 :
213 : /* fd_gui_hist_key_hash / fd_gui_hist_key_cmp return the per-DB KV key
214 : callbacks for `dbi` (NULL for TS DBs). */
215 :
216 : static inline ulong
217 0 : ( * fd_gui_hist_key_hash( int dbi ) )( void const * key ) {
218 0 : switch( dbi ) {
219 0 : case FD_GUI_HIST_SLOT: return fd_gui_hist_slot_key_hash;
220 0 : case FD_GUI_HIST_LEADER_SLOT: return fd_gui_hist_leader_slot_key_hash;
221 0 : case FD_GUI_HIST_EPOCH: return fd_gui_hist_epoch_key_hash;
222 0 : case FD_GUI_HIST_TIMELINE_TILE_DAY: return fd_gui_hist_tile_day_key_hash;
223 0 : case FD_GUI_HIST_FEC_EVENTS: return fd_gui_hist_fec_key_hash;
224 0 : default: return NULL;
225 0 : }
226 0 : }
227 :
228 : static inline int
229 0 : ( * fd_gui_hist_key_cmp( int dbi ) )( void const * a, void const * b ) {
230 0 : switch( dbi ) {
231 0 : case FD_GUI_HIST_SLOT: return fd_gui_hist_slot_key_cmp;
232 0 : case FD_GUI_HIST_LEADER_SLOT: return fd_gui_hist_leader_slot_key_cmp;
233 0 : case FD_GUI_HIST_EPOCH: return fd_gui_hist_epoch_key_cmp;
234 0 : case FD_GUI_HIST_TIMELINE_TILE_DAY: return fd_gui_hist_tile_day_key_cmp;
235 0 : case FD_GUI_HIST_FEC_EVENTS: return fd_gui_hist_fec_key_cmp;
236 0 : default: return NULL;
237 0 : }
238 0 : }
239 :
240 : /* fd_gui_hist_kv_stride approximates fd_gui_store's per-record ring stride
241 : for KV DB `dbi`. The store adds no header (the key and link live inside
242 : the value), so the stride is just the align-padded record. */
243 : static inline ulong
244 0 : fd_gui_hist_kv_stride( int dbi ) {
245 0 : return fd_ulong_align_up( fd_gui_hist_rec_sz( dbi ), 8UL );
246 0 : }
247 :
248 : /* fd_gui_hist_kv_cap returns the number of records of KV DB `dbi` that
249 : fit in one store region. */
250 : static inline ulong
251 0 : fd_gui_hist_kv_cap( int dbi ) {
252 0 : return FD_GUI_STORE_REGION_SZ / fd_gui_hist_kv_stride( dbi );
253 0 : }
254 :
255 : /* fd_gui_hist_base_regions returns the number of regions DB `dbi` is
256 : guaranteed to keep (see fd_gui_store_desc_t::base_regions). The
257 : retention floors are:
258 : - TS DBs: the newest full region plus the head being filled.
259 : - slot/leader_slot/epoch: at least FD_GUI_HIST_MIN_EPOCHS epochs of
260 : records plus one partially filled head region.
261 : - timeline_day: today and yesterday (one record per region).
262 : - timeline_tile_day: today's and yesterday's record for every tile
263 : plus one partially filled head region.
264 : Everything above these floors comes from the shared pool, which is
265 : reclaimed oldest region first across DBs. */
266 : static inline ulong
267 0 : fd_gui_hist_ceil_div( ulong a, ulong b ) {
268 0 : return (a+b-1UL)/b;
269 0 : }
270 :
271 : static ulong
272 0 : fd_gui_hist_base_regions( int dbi ) {
273 0 : ulong cap = fd_gui_hist_kv_cap( dbi );
274 0 : switch( dbi ) {
275 0 : case FD_GUI_HIST_SLOT: return fd_gui_hist_ceil_div( FD_GUI_HIST_MIN_EPOCHS*MAX_SLOTS_PER_EPOCH, cap ) + 1UL;
276 0 : case FD_GUI_HIST_LEADER_SLOT: return fd_gui_hist_ceil_div( FD_GUI_HIST_MIN_EPOCHS*FD_GUI_HIST_MAX_LEADER_SLOTS_PER_EPOCH, cap ) + 1UL;
277 0 : case FD_GUI_HIST_EPOCH: return fd_gui_hist_ceil_div( FD_GUI_HIST_MIN_EPOCHS, cap ) + 1UL;
278 0 : case FD_GUI_HIST_TIMELINE_DAY: return 2UL;
279 0 : case FD_GUI_HIST_TIMELINE_TILE_DAY: return fd_gui_hist_ceil_div( 2UL*FD_TOPO_MAX_TILES, cap ) + 1UL;
280 0 : default: return 2UL;
281 0 : }
282 0 : }
283 :
284 : /* fd_gui_hist_max_keys returns the most distinct keys KV DB `dbi` can
285 : ever hold. Shared regions beyond this are never needed. */
286 : static ulong
287 0 : fd_gui_hist_max_keys( int dbi ) {
288 0 : switch( dbi ) {
289 0 : case FD_GUI_HIST_SLOT: return FD_GUI_HIST_MAX_EPOCHS*MAX_SLOTS_PER_EPOCH;
290 0 : case FD_GUI_HIST_LEADER_SLOT: return FD_GUI_HIST_MAX_EPOCHS*FD_GUI_HIST_MAX_LEADER_SLOTS_PER_EPOCH;
291 0 : case FD_GUI_HIST_EPOCH: return FD_GUI_HIST_MAX_EPOCHS;
292 0 : case FD_GUI_HIST_TIMELINE_TILE_DAY: return FD_GUI_HIST_MAX_TIMELINE_DAYS*FD_TOPO_MAX_TILES;
293 0 : default: FD_LOG_ERR(( "invalid kv dbi %d", dbi )); return 0UL;
294 0 : }
295 0 : }
296 :
297 : /* fd_gui_hist_kv_max_records returns max_records for KV DB `dbi`, given
298 : its base_regions and the number of shared regions in the store. */
299 : static ulong
300 : fd_gui_hist_kv_max_records( int dbi,
301 : ulong base_regions,
302 0 : ulong shared ) {
303 0 : if( dbi==FD_GUI_HIST_FEC_EVENTS ) return FD_GUI_FEC_RECORD_MAX;
304 0 : ulong cap = fd_gui_hist_kv_cap( dbi );
305 0 : ulong lo = base_regions*cap;
306 0 : ulong hi = (base_regions+shared)*cap;
307 0 : return fd_ulong_max( lo, fd_ulong_min( hi, fd_gui_hist_max_keys( dbi ) ) );
308 0 : }
309 :
310 : fd_gui_store_desc_t *
311 : fd_gui_hist_db_descs( fd_gui_store_desc_t descs[ static FD_GUI_HIST_CNT ],
312 0 : ulong store_bytes ) {
313 0 : static char const * const names[ FD_GUI_HIST_CNT ] = {
314 0 : "scheduler_counts", "tile_timers", "shred_events", "txn_start",
315 0 : "txn_end", "slot", "leader_slot", "epoch", "tile_stats",
316 0 : "txn_waterfall", "timeline_day", "replay_txn", "timeline_tile_day",
317 0 : "fec_events", "fec_event_archive"
318 0 : };
319 :
320 0 : for( int i=0; i<FD_GUI_HIST_CNT; i++ ) descs[ i ].base_regions = fd_gui_hist_base_regions( i );
321 0 : ulong shared = fd_ulong_sat_sub( store_bytes, fd_gui_store_min_size( FD_GUI_HIST_CNT, descs ) ) / FD_GUI_STORE_REGION_SZ;
322 :
323 0 : for( int i=0; i<FD_GUI_HIST_CNT; i++ ) {
324 0 : int ts = fd_gui_hist_is_timeseries( i );
325 0 : ulong rec_sz = fd_gui_hist_rec_sz( i );
326 0 : ulong key_sz = ts ? 0UL : fd_gui_hist_key_sz( i ); /* TS DBs have no key (0) */
327 0 : ulong val_sz = ts ? fd_ulong_max( rec_sz, 1UL ) : rec_sz;
328 0 : ulong max_records = ts ? 0UL : fd_gui_hist_kv_max_records( i, descs[ i ].base_regions, shared );
329 :
330 0 : descs[ i ].name = names[ i ];
331 0 : descs[ i ].kind = ts ? FD_GUI_STORE_KIND_TS : FD_GUI_STORE_KIND_KV;
332 0 : descs[ i ].key_off = 0UL; /* the key is the leading field(s) of the value */
333 0 : descs[ i ].key_sz = key_sz;
334 0 : descs[ i ].key_hash = ts ? NULL : fd_gui_hist_key_hash( i );
335 0 : descs[ i ].key_cmp = ts ? NULL : fd_gui_hist_key_cmp( i );
336 0 : descs[ i ].val_sz = val_sz;
337 0 : descs[ i ].val_align = 8UL;
338 0 : descs[ i ].ts_off = ts ? fd_gui_hist_dbi_ts_off( i ) : 0UL;
339 0 : descs[ i ].granularity = ts ? (ulong)fd_gui_hist_dbi_res_ns( i ) : 0UL;
340 0 : descs[ i ].max_records = max_records;
341 0 : }
342 0 : return descs;
343 0 : }
344 :
345 : ulong
346 0 : fd_gui_hist_store_footprint( ulong store_bytes ) {
347 0 : fd_gui_store_desc_t descs[ FD_GUI_HIST_CNT ];
348 0 : return fd_gui_store_footprint( store_bytes, FD_GUI_HIST_CNT, fd_gui_hist_db_descs( descs, store_bytes ) );
349 0 : }
350 :
351 : void *
352 : fd_gui_hist_store_new( void * mem,
353 : char const * path,
354 : ulong store_bytes,
355 0 : ulong seed ) {
356 0 : fd_gui_store_desc_t descs[ FD_GUI_HIST_CNT ];
357 0 : return fd_gui_store_new( mem, path, store_bytes, FD_GUI_HIST_CNT, seed, fd_gui_hist_db_descs( descs, store_bytes ) );
358 0 : }
359 :
360 : /* Maximum number of records deleted per eviction iteration. */
361 0 : #define FD_GUI_HIST_EVICT_BATCH (512UL)
362 :
363 : struct fd_gui_hist_private {
364 : ulong magic; /* ==FD_GUI_HIST_MAGIC after fd_gui_hist_new */
365 : long last_ts[ FD_GUI_HIST_CNT ];
366 : int has_last_ts[ FD_GUI_HIST_CNT ];
367 :
368 : fd_gui_hist_metrics_t metrics;
369 : };
370 :
371 : FD_FN_CONST ulong
372 0 : fd_gui_hist_align( void ) {
373 0 : return 128UL;
374 0 : }
375 :
376 : FD_FN_CONST ulong
377 0 : fd_gui_hist_footprint( void ) {
378 0 : ulong l = FD_LAYOUT_INIT;
379 0 : l = FD_LAYOUT_APPEND( l, fd_gui_hist_align(), sizeof(fd_gui_hist_t) );
380 0 : return FD_LAYOUT_FINI( l, fd_gui_hist_align() );
381 0 : }
382 :
383 : void *
384 : fd_gui_hist_new( void * mem,
385 0 : fd_gui_store_t const * db ) {
386 0 : if( FD_UNLIKELY( !mem ) ) { FD_LOG_WARNING(( "fd_gui_hist_new: null mem" )); return NULL; }
387 0 : if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)mem, fd_gui_hist_align() ) ) ) { FD_LOG_WARNING(( "fd_gui_hist_new: misaligned mem" )); return NULL; }
388 0 : if( FD_UNLIKELY( !db ) ) { FD_LOG_WARNING(( "fd_gui_hist_new: null db" )); return NULL; }
389 :
390 0 : if( FD_UNLIKELY( fd_gui_store_cnt( db )!=FD_GUI_HIST_CNT ) ) {
391 0 : FD_LOG_WARNING(( "fd_gui_hist_new: store has %lu DBs, expected %d", fd_gui_store_cnt( db ), FD_GUI_HIST_CNT ));
392 0 : return NULL;
393 0 : }
394 :
395 0 : fd_memset( mem, 0, sizeof(fd_gui_hist_t) );
396 0 : FD_SCRATCH_ALLOC_INIT( l, mem );
397 0 : fd_gui_hist_t * hist = FD_SCRATCH_ALLOC_APPEND( l, fd_gui_hist_align(), sizeof(fd_gui_hist_t) );
398 0 : FD_SCRATCH_ALLOC_FINI( l, fd_gui_hist_align() );
399 :
400 0 : FD_COMPILER_MFENCE();
401 0 : hist->magic = FD_GUI_HIST_MAGIC;
402 0 : FD_COMPILER_MFENCE();
403 0 : return mem;
404 0 : }
405 :
406 : fd_gui_hist_t *
407 0 : fd_gui_hist_join( void * mem ) {
408 0 : if( FD_UNLIKELY( !mem ) ) return NULL;
409 0 : fd_gui_hist_t * hist = (fd_gui_hist_t *)mem;
410 0 : if( FD_UNLIKELY( hist->magic!=FD_GUI_HIST_MAGIC ) ) { FD_LOG_WARNING(( "fd_gui_hist_join: bad magic" )); return NULL; }
411 0 : return hist;
412 0 : }
413 :
414 : void *
415 0 : fd_gui_hist_leave( fd_gui_hist_t * hist ) {
416 0 : return (void *)hist;
417 0 : }
418 :
419 : void *
420 0 : fd_gui_hist_delete( void * mem ) {
421 0 : if( FD_UNLIKELY( !mem ) ) return NULL;
422 0 : ((fd_gui_hist_t *)mem)->magic = 0UL;
423 0 : return mem;
424 0 : }
425 :
426 : static inline fd_gui_hist_t *
427 0 : fd_gui_hist( fd_gui_t * gui ) {
428 0 : return (fd_gui_hist_t *)gui->hist;
429 0 : }
430 :
431 : static inline fd_gui_store_t *
432 0 : fd_gui_hist_db( fd_gui_t * gui ) {
433 0 : return (fd_gui_store_t *)gui->db;
434 0 : }
435 :
436 : fd_gui_hist_metrics_t const *
437 0 : fd_gui_hist_metrics( fd_gui_t const * gui ) {
438 0 : if( FD_UNLIKELY( !gui->hist ) ) return NULL;
439 0 : return &((fd_gui_hist_t const *)gui->hist)->metrics;
440 0 : }
441 :
442 : static int
443 : fd_gui_hist_reclaim_eligible_cb( ulong dbi,
444 : void const * rec FD_PARAM_UNUSED,
445 0 : void * ctx FD_PARAM_UNUSED ) {
446 : /* FEC staging is not disposable history, including closed records
447 : awaiting archival. Only fd_gui_fec_archive drains it. */
448 0 : return dbi!=FD_GUI_HIST_FEC_EVENTS;
449 0 : }
450 :
451 : static int
452 : fd_gui_hist_make_room( fd_gui_t * gui,
453 : int dbi,
454 0 : int rc ) {
455 0 : if( FD_UNLIKELY( rc!=FD_GUI_STORE_MAP_FULL && rc!=FD_GUI_STORE_RING_FULL ) ) return 0;
456 0 : if( dbi==FD_GUI_HIST_FEC_EVENTS ) return !!fd_gui_fec_archive( gui, FD_GUI_HIST_EVICT_BATCH );
457 0 : if( rc==FD_GUI_STORE_MAP_FULL && fd_gui_store_reclaim( gui->db, ULONG_MAX, FD_GUI_HIST_EVICT_BATCH, fd_gui_hist_reclaim_eligible_cb, NULL ) ) return 1;
458 0 : return !!fd_gui_store_reclaim( gui->db, (ulong)dbi, FD_GUI_HIST_EVICT_BATCH, fd_gui_hist_reclaim_eligible_cb, NULL );
459 0 : }
460 :
461 : void *
462 : fd_gui_hist_kv_get_or_create( fd_gui_t * gui,
463 : int dbi,
464 0 : void const * key ) {
465 0 : 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; }
466 :
467 0 : int forced_eviction = 0;
468 0 : void * val = NULL;
469 0 : int rc;
470 0 : for(;;) {
471 0 : rc = fd_gui_store_kv_get_or_create( fd_gui_hist_db( gui ), (ulong)dbi, key, &val );
472 0 : if( FD_LIKELY( rc==FD_GUI_STORE_SUCCESS ) ) {
473 0 : if( FD_UNLIKELY( forced_eviction ) ) fd_gui_hist( gui )->metrics.reserves[ dbi ]++;
474 0 : return val;
475 0 : }
476 : /* FEC staging pressure gets one bounded archive attempt. */
477 0 : if( forced_eviction && dbi==FD_GUI_HIST_FEC_EVENTS ) break;
478 0 : if( FD_UNLIKELY( !fd_gui_hist_make_room( gui, dbi, rc ) ) ) break;
479 0 : forced_eviction = 1;
480 0 : }
481 0 : if( FD_UNLIKELY( rc==FD_GUI_STORE_MAP_FULL || rc==FD_GUI_STORE_RING_FULL ) ) {
482 0 : fd_gui_hist( gui )->metrics.map_full[ dbi ]++;
483 0 : }
484 0 : return NULL;
485 0 : }
486 :
487 : void *
488 : fd_gui_hist_ts_emplace( fd_gui_t * gui,
489 : int dbi,
490 0 : long stored_ts ) {
491 0 : if( FD_UNLIKELY( dbi<0 || dbi>=FD_GUI_HIST_CNT ) ) { FD_LOG_WARNING(( "fd_gui_hist_ts_emplace: bad dbi %d", dbi )); return NULL; }
492 0 : if( FD_UNLIKELY( !fd_gui_hist_is_timeseries( dbi ) ) ) { FD_LOG_WARNING(( "fd_gui_hist_ts_emplace: dbi %d is not time-series", dbi )); return NULL; }
493 0 : fd_gui_store_t * db = fd_gui_hist_db( gui );
494 :
495 0 : ulong rec_sz = fd_gui_hist_rec_sz( dbi );
496 0 : if( FD_UNLIKELY( !rec_sz ) ) { FD_LOG_WARNING(( "fd_gui_hist_ts_emplace: dbi %d has no record type", dbi )); return NULL; }
497 :
498 0 : fd_gui_hist_t * hist = fd_gui_hist( gui );
499 0 : if( FD_UNLIKELY( hist->has_last_ts[ dbi ] && stored_ts<hist->last_ts[ dbi ] ) ) {
500 0 : FD_LOG_WARNING(( "fd_gui_hist_ts_emplace: dbi %d insertion timestamp decreased from %ld to %ld", dbi, hist->last_ts[ dbi ], stored_ts ));
501 0 : return NULL;
502 0 : }
503 :
504 0 : int forced_eviction = 0;
505 0 : void * val = NULL;
506 0 : int rc;
507 0 : for(;;) {
508 0 : rc = fd_gui_store_ts_emplace( db, (ulong)dbi, stored_ts, &val );
509 0 : if( FD_LIKELY( rc==FD_GUI_STORE_SUCCESS ) ) {
510 0 : hist->last_ts[ dbi ] = stored_ts;
511 0 : hist->has_last_ts[ dbi ] = 1;
512 0 : if( FD_UNLIKELY( forced_eviction ) ) hist->metrics.reserves[ dbi ]++;
513 0 : return val;
514 0 : }
515 0 : if( FD_UNLIKELY( !fd_gui_hist_make_room( gui, dbi, rc ) ) ) break;
516 0 : forced_eviction = 1;
517 0 : }
518 0 : if( FD_UNLIKELY( rc==FD_GUI_STORE_MAP_FULL ) ) {
519 0 : fd_gui_hist( gui )->metrics.map_full[ dbi ]++;
520 0 : FD_LOG_WARNING(( "fd_gui_hist_ts_emplace: dropping a record for dbi %d; store full and nothing left to evict", dbi ));
521 0 : }
522 0 : return NULL;
523 0 : }
524 :
525 : int
526 : fd_gui_hist_ts_append( fd_gui_t * gui,
527 : int dbi,
528 0 : void const * val ) {
529 0 : if( FD_UNLIKELY( dbi<0 || dbi>=FD_GUI_HIST_CNT ) ) { FD_LOG_WARNING(( "fd_gui_hist_ts_append: bad dbi %d", dbi )); return -1; }
530 0 : 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; }
531 0 : ulong rec_sz = fd_gui_hist_rec_sz( dbi );
532 0 : if( FD_UNLIKELY( !rec_sz ) ) { FD_LOG_WARNING(( "fd_gui_hist_ts_append: dbi %d has no record type", dbi )); return -1; }
533 0 : long stored_ts;
534 0 : fd_memcpy( &stored_ts, (uchar const *)val + fd_gui_hist_dbi_ts_off( dbi ), sizeof(stored_ts) );
535 0 : void * dst = fd_gui_hist_ts_emplace( gui, dbi, stored_ts );
536 0 : if( FD_UNLIKELY( !dst ) ) return -1;
537 0 : fd_memcpy( dst, val, rec_sz );
538 0 : return 0;
539 0 : }
540 :
541 : static void
542 0 : fd_gui_hist_iter_load( fd_gui_hist_iter_t * iter ) {
543 0 : for(;;) {
544 0 : if( fd_gui_store_ts_scan_done( &iter->_it ) ) { iter->rec = NULL; return; }
545 0 : void const * rec = iter->_it.rec; /* the gui record verbatim; self-describing */
546 0 : if( !iter->_filter || iter->_filter( rec, iter->_filter_ctx ) ) {
547 0 : iter->rec = rec;
548 0 : return;
549 0 : }
550 0 : fd_gui_store_ts_scan_next( &iter->_it );
551 0 : }
552 0 : }
553 :
554 : int
555 : fd_gui_hist_range_begin( fd_gui_t * gui,
556 : fd_gui_hist_iter_t * iter,
557 : int dbi,
558 : long lo_ns,
559 : long hi_ns,
560 : fd_gui_hist_range_filter_fn filter,
561 0 : void * filter_ctx ) {
562 0 : fd_gui_store_t * db = fd_gui_hist_db( gui );
563 0 : memset( iter, 0, sizeof(fd_gui_hist_iter_t) );
564 :
565 0 : 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; }
566 :
567 0 : FD_TEST( lo_ns!=LONG_MIN && hi_ns!=LONG_MAX ); /* open-ended queries not supported */
568 :
569 0 : long res_ns = fd_gui_hist_dbi_res_ns( dbi );
570 0 : ulong window_lo = (lo_ns<=0L) ? 0UL : fd_gui_hist_window( lo_ns, res_ns );
571 0 : ulong window_hi = fd_gui_hist_window( hi_ns, res_ns );
572 :
573 0 : iter->_dbi = dbi;
574 0 : iter->rec_sz = fd_gui_hist_rec_sz( dbi );
575 0 : iter->_filter = filter;
576 0 : iter->_filter_ctx = filter_ctx;
577 :
578 : /* Clamp the request time range to the index bounds. */
579 0 : long first_ts;
580 0 : long last_ts;
581 0 : if( FD_UNLIKELY( !fd_gui_store_ts_live_timestamp_bounds( db, (ulong)dbi, &first_ts, &last_ts ) ) ) return 0; /* empty ring */
582 :
583 0 : ulong first_window = fd_gui_hist_window( fd_long_max( first_ts, 0L ), res_ns );
584 0 : ulong last_window = fd_gui_hist_window( fd_long_max( last_ts, 0L ), res_ns );
585 0 : window_lo = fd_ulong_max( window_lo, first_window );
586 0 : window_hi = fd_ulong_min( window_hi, last_window );
587 0 : if( FD_UNLIKELY( window_lo>window_hi ) ) return 0; /* request does not overlap the ring */
588 :
589 0 : fd_gui_store_ts_scan_begin( db, &iter->_it, (ulong)dbi, window_lo, window_hi, NULL, NULL );
590 0 : fd_gui_hist_iter_load( iter );
591 0 : return 0;
592 0 : }
593 :
594 : int
595 0 : fd_gui_hist_range_next( fd_gui_hist_iter_t * iter ) {
596 0 : if( FD_UNLIKELY( !iter->rec ) ) return 0;
597 0 : if( iter->_emitted ) {
598 0 : fd_gui_store_ts_scan_next( &iter->_it );
599 0 : fd_gui_hist_iter_load( iter );
600 0 : if( !iter->rec ) return 0;
601 0 : }
602 0 : iter->_emitted = 1;
603 0 : return 1;
604 0 : }
605 :
606 : void
607 0 : fd_gui_hist_range_end( fd_gui_hist_iter_t * iter ) {
608 0 : fd_gui_store_ts_scan_end( &iter->_it );
609 0 : }
610 :
611 : void *
612 : fd_gui_hist_kv_get( fd_gui_t * gui,
613 : int dbi,
614 0 : void const * key ) {
615 0 : 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; }
616 0 : return fd_gui_store_kv_get( fd_gui_hist_db( gui ), (ulong)dbi, key );
617 0 : }
618 :
619 : void *
620 : fd_gui_hist_kv_get_slot_any( fd_gui_t * gui,
621 : int dbi,
622 0 : ulong slot ) {
623 0 : if( FD_UNLIKELY( fd_gui_hist_keyshape( dbi )!=FD_GUI_HIST_KEYSHAPE_SLOT_BANK ) ) {
624 0 : FD_LOG_WARNING(( "fd_gui_hist_kv_get_slot_any: dbi %d is not slot/bank-keyed", dbi ));
625 0 : return NULL;
626 0 : }
627 :
628 0 : fd_gui_hist_slot_key_t key = { .slot=slot, .bank_seq=ULONG_MAX };
629 0 : return fd_gui_store_kv_get_any( fd_gui_hist_db( gui ), (ulong)dbi, &key );
630 0 : }
631 :
632 : /* fd_gui_hist_ts_iter_load reads the iterator's current backend record into
633 : the typed iterator fields (or marks it done). */
634 : static void
635 0 : fd_gui_hist_ts_iter_load( fd_gui_hist_kv_slot_iter_t * iter ) {
636 0 : if( FD_UNLIKELY( fd_gui_store_kv_iter_done( &iter->_it ) ) ) {
637 0 : iter->rec = NULL;
638 0 : iter->bank_seq = ULONG_MAX;
639 0 : return;
640 0 : }
641 0 : iter->rec = iter->_it.rec;
642 0 : iter->bank_seq = ( iter->_it.key_sz>=2UL*sizeof(ulong) )
643 0 : ? ((ulong const *)iter->_it.key)[ 1 ]
644 0 : : ULONG_MAX;
645 0 : }
646 :
647 : fd_gui_hist_kv_slot_iter_t *
648 : fd_gui_hist_kv_iter_begin( fd_gui_t * gui,
649 : fd_gui_hist_kv_slot_iter_t * iter,
650 : int dbi,
651 0 : ulong slot ) {
652 0 : iter->rec = NULL;
653 0 : iter->bank_seq = ULONG_MAX;
654 0 : if( FD_UNLIKELY( fd_gui_hist_keyshape( dbi )!=FD_GUI_HIST_KEYSHAPE_SLOT_BANK ) ) {
655 0 : FD_LOG_WARNING(( "fd_gui_hist_kv_iter_begin: dbi %d is not slot/bank-keyed", dbi ));
656 0 : fd_gui_hist_slot_key_t none = { .slot=ULONG_MAX, .bank_seq=ULONG_MAX };
657 0 : fd_gui_store_kv_iter_begin( fd_gui_hist_db( gui ), &iter->_it, (ulong)dbi, &none );
658 0 : return iter;
659 0 : }
660 0 : fd_gui_hist_slot_key_t key = { .slot=slot, .bank_seq=ULONG_MAX };
661 0 : fd_gui_store_kv_iter_begin( fd_gui_hist_db( gui ), &iter->_it, (ulong)dbi, &key );
662 0 : fd_gui_hist_ts_iter_load( iter );
663 0 : return iter;
664 0 : }
665 :
666 : int
667 0 : fd_gui_hist_kv_iter_next( fd_gui_hist_kv_slot_iter_t * iter ) {
668 0 : if( FD_UNLIKELY( !iter->rec ) ) return 0;
669 0 : fd_gui_store_kv_iter_next( &iter->_it );
670 0 : fd_gui_hist_ts_iter_load( iter );
671 0 : return iter->rec!=NULL;
672 0 : }
673 :
674 : int
675 0 : fd_gui_hist_evict_step( fd_gui_t * gui ) {
676 0 : if( FD_UNLIKELY( !gui->db || !gui->hist ) ) return 0;
677 : /* Keep one shared region in reserve. */
678 0 : if( fd_gui_store_shared_free_region_cnt( gui->db )>1UL ) return 0;
679 0 : return !!fd_gui_store_reclaim( gui->db, ULONG_MAX, FD_GUI_HIST_EVICT_BATCH, fd_gui_hist_reclaim_eligible_cb, NULL );
680 0 : }
|