From 92e1abcf3ab82181cb2411c4a9ad9bab1ac693a3 Mon Sep 17 00:00:00 2001 From: jherrera-jump Date: Mon, 3 Nov 2025 22:58:58 +0000 Subject: [PATCH] gui: catchup shred history --- book/api/websocket.md | 30 ++++++++-- src/disco/gui/fd_gui.c | 1 + src/disco/gui/fd_gui_printf.c | 110 ++++++++++++++++++++++++++++++---- 3 files changed, 125 insertions(+), 16 deletions(-) diff --git a/book/api/websocket.md b/book/api/websocket.md index 1165699992..fe55d5f6ca 100644 --- a/book/api/websocket.md +++ b/book/api/websocket.md @@ -324,9 +324,11 @@ Frankendancer client will always publish `null` for this message | *Once* | `CatchUpHistory` | see below | This validator records a history of all slots that were received from -turbine as well as slots for which a repair request was made while it is +turbine or repair responses, as well as shred events that occurred while catching up. After catching up, slots are no longer recorded in this -history. +history. For repair and turbine slots, the history is available for the +lifetime of the validator. Shred events are only available if the +validator is in the catching up phase. ::: details Example @@ -336,13 +338,29 @@ history. "key": "catch_up_history", "value": { "repair": [11, 12, 13, ...], - "turbine": [21, 22, 23, ...] + "turbine": [21, 22, 23, ...], + "shreds": { + "reference_slot": 289245044, + "reference_ts": "1739657041588242791", + "slot_delta": [0, 0], + "shred_idx": [1234, null], + "event": [0, 1], + "event_ts_delta": ["1000000", "2000000"] + } } } ``` ::: +**`CatchUpHistory`** +| Field | Type | Description | +|------------|---------------|-------------| +| repair | `number[]` | A list of all slots for which a repair shred was received that are older than `summary.caught_up_slot` | +| turbine | `number[]` | A list of all slots for which a turbine shred was received that are older than `summary.caught_up_slot` | +| shreds | `SlotShreds` | A list of shred events which have occurred for this validator in the past 15 seconds. If the validator has already caught up, or has not yet started catching up, then `null` | + + #### `summary.startup_time_nanos` | frequency | type | example | |-----------|----------|---------------------| @@ -1805,7 +1823,7 @@ rooted. #### `slot.live_shreds` | frequency | type | example | |-------------|---------------|---------| -| *10ms* | `SlotShred[]` | below | +| *10ms* | `SlotShreds` | below | The validator sends a continous stream of update messages with detailed information about the time and duration of different shred state @@ -1831,7 +1849,7 @@ and is broadcast to all WebSocket clients. ::: -**`SlotShred`** +**`SlotShreds`** | Field | Type | Description | |-----------------|--------------------|-------------| | reference_slot | `number`   | The smallest slot number across all the shreds in a given message | @@ -1844,7 +1862,7 @@ and is broadcast to all WebSocket clients. #### `slot.query_shreds` | frequency | type | example | |-------------|---------------|---------| -| *Request* | `SlotShred[]\null` | below | +| *Request* | `SlotShreds\|null` | below | | param | type | description | |-------|----------|-------------| diff --git a/src/disco/gui/fd_gui.c b/src/disco/gui/fd_gui.c index 3b5ee81655..1c37a8fd46 100644 --- a/src/disco/gui/fd_gui.c +++ b/src/disco/gui/fd_gui.c @@ -169,6 +169,7 @@ fd_gui_new( void * shmem, gui->summary.boot_progress.loading_snapshot[ i ].read_path[ 0 ] = '\0'; gui->summary.boot_progress.loading_snapshot[ i ].insert_path[ 0 ] = '\0'; } + gui->summary.boot_progress.catching_up_time_nanos = 0L; gui->summary.boot_progress.catching_up_first_replay_slot = ULONG_MAX; } else { fd_memset( &gui->summary.boot_progress, 0, sizeof(gui->summary.boot_progress) ); diff --git a/src/disco/gui/fd_gui_printf.c b/src/disco/gui/fd_gui_printf.c index 3bf6db2d7d..234a9458d2 100644 --- a/src/disco/gui/fd_gui_printf.c +++ b/src/disco/gui/fd_gui_printf.c @@ -277,6 +277,88 @@ fd_gui_printf_catch_up_history( fd_gui_t * gui ) { } } jsonp_close_array( gui->http ); + + if( FD_LIKELY( gui->summary.boot_progress.phase==FD_GUI_BOOT_PROGRESS_TYPE_CATCHING_UP ) ) { + ulong min_slot = ULONG_MAX; + long min_ts = LONG_MAX; + +#define SHREDS_REV_ITER( age_ns, code_staged, code_archive ) \ + do { \ + if( FD_UNLIKELY( gui->summary.boot_progress.catching_up_time_nanos==0L ) ) break; \ + for( ulong i=gui->shreds.staged_tail; i>gui->shreds.staged_head; i-- ) { \ + fd_gui_slot_staged_shred_event_t * event = &gui->shreds.staged[ (i-1UL) % FD_GUI_SHREDS_STAGING_SZ ]; \ + if( FD_UNLIKELY( event->timestamp < gui->summary.boot_progress.catching_up_time_nanos - age_ns ) ) break; \ + do { code_staged } while(0); \ + } \ + fd_gui_slot_t * s = fd_gui_get_slot( gui, gui->shreds.history_slot ); \ + while( s \ + && s->shreds.start_offset!=ULONG_MAX \ + && s->shreds.end_offset!=ULONG_MAX \ + && s->shreds.end_offset>s->shreds.start_offset \ + && gui->shreds.history[ (s->shreds.end_offset-1UL) % FD_GUI_SHREDS_HISTORY_SZ ].timestamp + age_ns > gui->summary.boot_progress.catching_up_time_nanos ) { \ + for( ulong i=s->shreds.end_offset; i>s->shreds.start_offset; i++ ) { \ + fd_gui_slot_history_shred_event_t * event = &gui->shreds.history[ (i-1UL) % FD_GUI_SHREDS_HISTORY_SZ ]; (void)event; \ + do { code_archive } while (0); \ + } \ + s = fd_gui_get_slot( gui, s->parent_slot ); \ + } \ + } while(0); + + SHREDS_REV_ITER( + 15000000000, + { + min_slot = fd_ulong_min( min_slot, event->slot ); + min_ts = fd_long_min( min_ts, event->timestamp ); + }, + { + min_slot = fd_ulong_min( min_slot, s->slot ); + min_ts = fd_long_min( min_ts, event->timestamp ); + } + ) + + jsonp_open_object( gui->http, "shreds" ); + jsonp_ulong ( gui->http, "reference_slot", min_slot ); + jsonp_long_as_str( gui->http, "reference_ts", min_ts ); + + jsonp_open_array( gui->http, "slot_delta" ); + SHREDS_REV_ITER( + 15000000000L, + { jsonp_ulong( gui->http, NULL, event->slot-min_slot ); }, + { jsonp_ulong( gui->http, NULL, s->slot-min_slot ); } + ) + jsonp_close_array( gui->http ); + jsonp_open_array( gui->http, "shred_idx" ); + SHREDS_REV_ITER( + 15000000000L, + { + if( FD_LIKELY( event->shred_idx!=USHORT_MAX ) ) jsonp_ulong( gui->http, NULL, event->shred_idx ); + else jsonp_null ( gui->http, NULL ); + }, + { + if( FD_LIKELY( event->shred_idx!=USHORT_MAX ) ) jsonp_ulong( gui->http, NULL, event->shred_idx ); + else jsonp_null ( gui->http, NULL ); + } + ) + jsonp_close_array( gui->http ); + jsonp_open_array( gui->http, "event" ); + SHREDS_REV_ITER( + 15000000000L, + { jsonp_ulong( gui->http, NULL, event->event ); }, + { jsonp_ulong( gui->http, NULL, event->event ); } + ) + jsonp_close_array( gui->http ); + jsonp_open_array( gui->http, "event_ts_delta" ); + SHREDS_REV_ITER( + 15000000000L, + { jsonp_long_as_str( gui->http, NULL, event->timestamp-min_ts ); }, + { jsonp_long_as_str( gui->http, NULL, event->timestamp-min_ts ); } + ) + jsonp_close_array( gui->http ); + jsonp_close_object( gui->http ); + } else { + jsonp_null( gui->http, "shreds" ); + } + jsonp_close_object( gui->http ); jsonp_close_envelope( gui->http ); } @@ -2287,37 +2369,45 @@ void fd_gui_printf_slot_shred_updates( fd_gui_t * gui, ulong _slot, ulong id ) { - ulong _start_offset = gui->slots[ _slot % FD_GUI_SLOTS_CNT ]->shreds.start_offset; - ulong _end_offset = gui->slots[ _slot % FD_GUI_SLOTS_CNT ]->shreds.end_offset; + fd_gui_slot_t * slot = fd_gui_get_slot( gui, _slot ); + + if( FD_UNLIKELY( !slot || slot->shreds.end_offset <= gui->shreds.history_tail-FD_GUI_SHREDS_HISTORY_SZ ) ) { + jsonp_open_envelope( gui->http, "slot", "query_shreds" ); + jsonp_ulong( gui->http, "id", id ); + jsonp_null( gui->http, "value" ); + jsonp_close_envelope( gui->http ); + return; + } + + ulong _start_offset = slot->shreds.start_offset; + ulong _end_offset = slot->shreds.end_offset; - ulong min_slot = ULONG_MAX; long min_ts = LONG_MAX; for( ulong i=_start_offset; i<_end_offset; i++ ) { - min_slot = fd_ulong_min( min_slot, gui->shreds.staged[ i % FD_GUI_SHREDS_STAGING_SZ ].slot ); - min_ts = fd_long_min ( min_ts, gui->shreds.staged[ i % FD_GUI_SHREDS_STAGING_SZ ].timestamp ); + min_ts = fd_long_min ( min_ts, gui->shreds.history[ i % FD_GUI_SHREDS_HISTORY_SZ ].timestamp ); } jsonp_open_envelope( gui->http, "slot", "query_shreds" ); jsonp_ulong( gui->http, "id", id ); jsonp_open_object( gui->http, "value" ); - jsonp_ulong ( gui->http, "reference_slot", min_slot ); + jsonp_ulong ( gui->http, "reference_slot", _slot ); jsonp_long_as_str( gui->http, "reference_ts", min_ts ); jsonp_open_array( gui->http, "slot_delta" ); - for( ulong i=_start_offset; i<_end_offset; i++ ) jsonp_ulong( gui->http, NULL, gui->shreds.staged[ i % FD_GUI_SHREDS_STAGING_SZ ].slot-min_slot ); + for( ulong i=_start_offset; i<_end_offset; i++ ) jsonp_ulong( gui->http, NULL, 0UL ); jsonp_close_array( gui->http ); jsonp_open_array( gui->http, "shred_idx" ); for( ulong i=_start_offset; i<_end_offset; i++ ) { - if( FD_LIKELY( gui->shreds.staged[ i % FD_GUI_SHREDS_STAGING_SZ ].shred_idx!=USHORT_MAX ) ) jsonp_ulong( gui->http, NULL, gui->shreds.staged[ i % FD_GUI_SHREDS_STAGING_SZ ].shred_idx ); + if( FD_LIKELY( gui->shreds.staged[ i % FD_GUI_SHREDS_STAGING_SZ ].shred_idx!=USHORT_MAX ) ) jsonp_ulong( gui->http, NULL, gui->shreds.history[ i % FD_GUI_SHREDS_HISTORY_SZ ].shred_idx ); else jsonp_null ( gui->http, NULL ); } jsonp_close_array( gui->http ); jsonp_open_array( gui->http, "event" ); - for( ulong i=_start_offset; i<_end_offset; i++ ) jsonp_ulong( gui->http, NULL, gui->shreds.staged[ i % FD_GUI_SHREDS_STAGING_SZ ].event ); + for( ulong i=_start_offset; i<_end_offset; i++ ) jsonp_ulong( gui->http, NULL, gui->shreds.history[ i % FD_GUI_SHREDS_HISTORY_SZ ].event ); jsonp_close_array( gui->http ); jsonp_open_array( gui->http, "event_ts_delta" ); - for( ulong i=_start_offset; i<_end_offset; i++ ) jsonp_long_as_str( gui->http, NULL, gui->shreds.staged[ i % FD_GUI_SHREDS_STAGING_SZ ].timestamp-min_ts ); + for( ulong i=_start_offset; i<_end_offset; i++ ) jsonp_long_as_str( gui->http, NULL, gui->shreds.history[ i % FD_GUI_SHREDS_HISTORY_SZ ].timestamp-min_ts ); jsonp_close_array( gui->http ); jsonp_close_object( gui->http ); jsonp_close_envelope( gui->http );