From 0a2c87d633fb2f2736417f054855818ca21e54a4 Mon Sep 17 00:00:00 2001 From: Ami Suzuki Date: Tue, 2 Dec 2025 10:53:25 -0600 Subject: [PATCH] chore: check server time to delete shred slots --- src/api/atoms.ts | 8 + src/api/consts.ts | 4 + src/api/entities.ts | 5 + src/api/types.ts | 6 + src/api/useSetAtomWsData.ts | 35 ++- src/atoms.ts | 9 +- src/clockUtils.ts | 11 +- src/colors.ts | 3 + .../Overview/LiveNetworkMetrics/consts.ts | 7 + .../Overview/LiveNetworkMetrics/index.tsx | 137 ++++++++++ .../liveNetworkMetrics.module.css | 5 + .../Overview/LiveTileMetrics/consts.ts | 22 ++ .../Overview/LiveTileMetrics/index.tsx | 238 ++++++++++++++++++ .../liveTileMetrics.module.css | 48 ++++ .../Overview/ShredsProgression/atoms.ts | 4 +- .../shredsProgressionPlugin.ts | 11 +- .../Overview/SlotPerformance/TileCard.tsx | 3 +- .../SlotPerformance/TileSparkLine.tsx | 22 +- .../SlotPerformance/useTileSparkline.ts | 25 +- src/features/Overview/index.tsx | 4 + .../StartupProgress/Firedancer/Bars.tsx | 9 +- .../Firedancer/bars.module.css | 2 +- src/utils.ts | 2 +- 23 files changed, 584 insertions(+), 36 deletions(-) create mode 100644 src/features/Overview/LiveNetworkMetrics/consts.ts create mode 100644 src/features/Overview/LiveNetworkMetrics/index.tsx create mode 100644 src/features/Overview/LiveNetworkMetrics/liveNetworkMetrics.module.css create mode 100644 src/features/Overview/LiveTileMetrics/consts.ts create mode 100644 src/features/Overview/LiveTileMetrics/index.tsx create mode 100644 src/features/Overview/LiveTileMetrics/liveTileMetrics.module.css diff --git a/src/api/atoms.ts b/src/api/atoms.ts index a6460a37..b14dc89c 100644 --- a/src/api/atoms.ts +++ b/src/api/atoms.ts @@ -30,6 +30,8 @@ import type { GossipPeersRowsUpdate, GossipPeersCellUpdate, ServerTimeNanos, + LiveNetworkMetrics, + TileMetrics, } from "./types"; import { rafAtom } from "../atomUtils"; @@ -72,6 +74,10 @@ export const estimatedSlotDurationAtom = atom< export const estimatedTpsAtom = atom(undefined); +export const liveNetworkMetricsAtom = atom( + undefined, +); + export const liveTxnWaterfallAtom = rafAtom( undefined, ); @@ -80,6 +86,8 @@ export const liveTilePrimaryMetricAtom = atom< LiveTilePrimaryMetric | undefined >(undefined); +export const liveTileMetricsAtom = atom(undefined); + export const tileTimerAtom = atom(undefined); export const bootProgressAtom = atom(undefined); diff --git a/src/api/consts.ts b/src/api/consts.ts index 200c3c3e..95157a57 100644 --- a/src/api/consts.ts +++ b/src/api/consts.ts @@ -1,4 +1,8 @@ export const estimatedTpsDebounceMs = 400; export const liveMetricsDebounceMs = 100; +export const liveTileMetricsDebounceMs = 130; +export const liveNetworkMetricsDebounceMs = 130; export const waterfallDebounceMs = 100; export const tileTimerDebounceMs = 25; +export const gossipNetworkDebounceMs = 300; +export const gossipPeerSizeDebounceMs = 1_000; diff --git a/src/api/entities.ts b/src/api/entities.ts index ce4e160e..bec1619b 100644 --- a/src/api/entities.ts +++ b/src/api/entities.ts @@ -128,6 +128,7 @@ export const resetSlotSchema = z.number().nullable(); export const storageSlotSchema = z.number().nullable(); export const voteSlotSchema = z.number(); export const slotCaughtUpSchema = z.number().nullable(); +export const activeForkCountSchema = z.number(); export const estimatedSlotDurationSchema = z.number(); @@ -503,6 +504,10 @@ export const summarySchema = z.discriminatedUnion("key", [ key: z.literal("slot_caught_up"), value: slotCaughtUpSchema, }), + summaryTopicSchema.extend({ + key: z.literal("active_fork_count"), + value: activeForkCountSchema, + }), summaryTopicSchema.extend({ key: z.literal("estimated_slot_duration_nanos"), value: estimatedSlotDurationSchema, diff --git a/src/api/types.ts b/src/api/types.ts index 2f9c0b79..8f751ef4 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -58,6 +58,8 @@ import type { gossipMessageStatsSchema, schedulerCountsSchema, serverTimeNanosSchema, + liveNetworkMetricsSchema, + tileMetricsSchema, } from "./entities"; export type Client = z.infer; @@ -105,12 +107,16 @@ export type EstimatedSlotDuration = z.infer; export type EstimatedTps = z.infer; +export type LiveNetworkMetrics = z.infer; + export type LiveTxnWaterfall = z.infer; export type LiveTilePrimaryMetric = z.infer; export type TilePrimaryMetric = z.infer; +export type TileMetrics = z.infer; + export type TxnWaterfallIn = z.infer; export type TxnWaterfallOut = z.infer; export type TxnWaterfall = z.infer; diff --git a/src/api/useSetAtomWsData.ts b/src/api/useSetAtomWsData.ts index 2ee403bb..ebaa4c8c 100644 --- a/src/api/useSetAtomWsData.ts +++ b/src/api/useSetAtomWsData.ts @@ -28,6 +28,8 @@ import { gossipPeersRowsUpdateAtom, gossipPeersCellUpdateAtom, serverTimeNanosAtom, + liveNetworkMetricsAtom, + liveTileMetricsAtom, } from "./atoms"; import { blockEngineSchema, @@ -58,12 +60,14 @@ import type { EstimatedTps, GossipNetworkStats, GossipPeersSize, + LiveNetworkMetrics, LiveTilePrimaryMetric, LiveTxnWaterfall, Peer, PeerRemove, RepairSlot, SlotResponse, + TileMetrics, TurbineSlot, } from "./types"; import { useDebouncedCallback, useThrottledCallback } from "use-debounce"; @@ -72,7 +76,11 @@ import { useServerMessages } from "./ws/utils"; import { DateTime } from "luxon"; import { estimatedTpsDebounceMs, + gossipNetworkDebounceMs, + gossipPeerSizeDebounceMs, liveMetricsDebounceMs, + liveNetworkMetricsDebounceMs, + liveTileMetricsDebounceMs, tileTimerDebounceMs, waterfallDebounceMs, } from "./consts"; @@ -133,6 +141,19 @@ export function useSetAtomWsData() { setEstimatedTps(value); }, estimatedTpsDebounceMs); + const setLiveNetworkMetrics = useSetAtom(liveNetworkMetricsAtom); + const setDbLiveNetworkMetrics = useThrottledCallback( + (value?: LiveNetworkMetrics) => { + setLiveNetworkMetrics(value); + }, + liveNetworkMetricsDebounceMs, + ); + + const setLiveTileMetrics = useSetAtom(liveTileMetricsAtom); + const setDbLiveTileMetrics = useThrottledCallback((value?: TileMetrics) => { + setLiveTileMetrics(value); + }, liveTileMetricsDebounceMs); + const setLivePrimaryMetrics = useSetAtom(liveTilePrimaryMetricAtom); const setDbLivePrimaryMetrics = useThrottledCallback( (value?: LiveTilePrimaryMetric) => { @@ -178,7 +199,7 @@ export function useSetAtomWsData() { (value?: GossipNetworkStats) => { setGossipNetworkStats(value); }, - 300, + gossipNetworkDebounceMs, ); const setGossipPeersSize = useSetAtom(gossipPeersSizeAtom); @@ -186,7 +207,7 @@ export function useSetAtomWsData() { (value?: GossipPeersSize) => { setGossipPeersSize(value); }, - 1_000, + gossipPeerSizeDebounceMs, ); const setGossipPeersRows = useSetAtom(gossipPeersRowsUpdateAtom); const setGossipPeersCells = useSetAtom(gossipPeersCellUpdateAtom); @@ -389,6 +410,13 @@ export function useSetAtomWsData() { setServerTimeNanos(value); break; } + case "live_network_metrics": { + setDbLiveNetworkMetrics(value); + break; + } + case "live_tile_metrics": + setDbLiveTileMetrics(value); + break; case "root_slot": case "optimistically_confirmed_slot": case "estimated_slot": @@ -398,8 +426,7 @@ export function useSetAtomWsData() { case "storage_slot": case "vote_slot": case "slot_caught_up": - case "live_network_metrics": - case "live_tile_metrics": + case "active_fork_count": break; } } else if (topic === "epoch") { diff --git a/src/atoms.ts b/src/atoms.ts index 2fcc3201..8c4ab929 100644 --- a/src/atoms.ts +++ b/src/atoms.ts @@ -1,10 +1,11 @@ import { atom } from "jotai"; -import { slotsPerLeader } from "./consts"; +import { nsPerMs, slotsPerLeader } from "./consts"; import { atomWithImmer } from "jotai-immer"; import { bootProgressAtom, estimatedSlotDurationAtom, identityKeyAtom, + serverTimeNanosAtom, skippedSlotsAtom, startupProgressAtom, } from "./api/atoms"; @@ -695,3 +696,9 @@ export const [ }), ]; })(); + +export const serverTimeMsAtom = atom((get) => { + const serverTimeNanos = get(serverTimeNanosAtom); + if (serverTimeNanos == null) return undefined; + return Math.round(serverTimeNanos / nsPerMs); +}); diff --git a/src/clockUtils.ts b/src/clockUtils.ts index 798ccf28..6a4b2e2e 100644 --- a/src/clockUtils.ts +++ b/src/clockUtils.ts @@ -1,14 +1,19 @@ type Sub = (now: number, dt: number) => void; -export function clockSub(intervalMs: number) { +export function clockSub(_intervalMs: number) { const subs = new Set(); let id: number | null = null; let last = performance.now(); + let intervalMs = _intervalMs; - function startChartClock() { + function startChartClock(newIntervalMs?: number) { if (id == null) { stopChartClock(); } + if (newIntervalMs !== undefined) { + stopChartClock(); + intervalMs = newIntervalMs; + } const loop = () => { const now = performance.now(); @@ -37,5 +42,5 @@ export function clockSub(intervalMs: number) { startChartClock(); - return { subscribeClock, stopChartClock }; + return { subscribeClock, stopChartClock, startChartClock }; } diff --git a/src/colors.ts b/src/colors.ts index e3188848..137cf306 100644 --- a/src/colors.ts +++ b/src/colors.ts @@ -283,3 +283,6 @@ export const slotsListFutureSlotColor = "#878787"; export const slotsListCurrentSlotBoxShadowColor = "rgba(191, 135, 253, 0.13)"; export const slotsListCurrentSlotNumberBackgroundColor = "#283551"; export const slotsListNextLeaderProgressBarColor = "#37a4bc"; + +// Tile charts +export const tileChartDarkBackground = "#0000001F"; diff --git a/src/features/Overview/LiveNetworkMetrics/consts.ts b/src/features/Overview/LiveNetworkMetrics/consts.ts new file mode 100644 index 00000000..7829beb1 --- /dev/null +++ b/src/features/Overview/LiveNetworkMetrics/consts.ts @@ -0,0 +1,7 @@ +export const networkProtocols = [ + "turbine", + "gossip", + "tpu", + "repair", + "metrics", +]; diff --git a/src/features/Overview/LiveNetworkMetrics/index.tsx b/src/features/Overview/LiveNetworkMetrics/index.tsx new file mode 100644 index 00000000..357a1e1b --- /dev/null +++ b/src/features/Overview/LiveNetworkMetrics/index.tsx @@ -0,0 +1,137 @@ +import { useAtomValue } from "jotai"; +import { liveNetworkMetricsAtom } from "../../../api/atoms"; +import Card from "../../../components/Card"; +import { Flex, Table, Text } from "@radix-ui/themes"; +import tableStyles from "../../Gossip/table.module.css"; +import { useEmaValue } from "../../../hooks/useEma"; +import { networkProtocols } from "./consts"; +import { formatBytesAsBits } from "../../../utils"; +import { Bars } from "../../StartupProgress/Firedancer/Bars"; +import TileSparkLine from "../SlotPerformance/TileSparkLine"; +import { headerGap } from "../../Gossip/consts"; +import type { CSSProperties } from "react"; +import styles from "./liveNetworkMetrics.module.css"; +import { sum } from "lodash"; +import { tileChartDarkBackground } from "../../../colors"; + +const chartHeight = 18; + +export default function LiveNetworkMetrics() { + const liveNetworkMetrics = useAtomValue(liveNetworkMetricsAtom); + if (!liveNetworkMetrics) return; + + return ( + + + + + ); +} + +interface NetworkMetricsCardProps { + metrics: number[]; + type: "Ingress" | "Egress"; +} + +function NetworkMetricsCard({ metrics, type }: NetworkMetricsCardProps) { + return ( + + + Network {type} + + + + + Protocol + + + Current + + + Utilization + + + History (1m) + + + + + + {metrics.map((value, i) => ( + + ))} + + + + + + ); +} + +const maxValue = 100_000_000; + +interface TableRowProps { + value: number; + idx?: number; + label?: string; +} + +function TableRow({ + value, + idx, + label, + ...props +}: TableRowProps & Table.RootProps) { + const emaValue = useEmaValue(value); + const formattedValue = formatBytesAsBits(emaValue); + + return ( + + + {label ?? networkProtocols[idx ?? -1]} + + + {formattedValue.value} {formattedValue.unit} + + + + + + + + + + + ); +} diff --git a/src/features/Overview/LiveNetworkMetrics/liveNetworkMetrics.module.css b/src/features/Overview/LiveNetworkMetrics/liveNetworkMetrics.module.css new file mode 100644 index 00000000..dd3fa120 --- /dev/null +++ b/src/features/Overview/LiveNetworkMetrics/liveNetworkMetrics.module.css @@ -0,0 +1,5 @@ +.chart { + padding-top: 0; + padding-bottom: 0; + vertical-align: middle; +} diff --git a/src/features/Overview/LiveTileMetrics/consts.ts b/src/features/Overview/LiveTileMetrics/consts.ts new file mode 100644 index 00000000..c8637298 --- /dev/null +++ b/src/features/Overview/LiveTileMetrics/consts.ts @@ -0,0 +1,22 @@ +/** Tile regimes are the cartesian product of the following two state vectors: + State vector 1: + running: means that at the time the run loop executed, there was no upstream message I/O for the tile to handle. + processing: means that at the time the run loop executed, there was one or more messages for the tile to consume. + stalled: means that at the time the run loop executed, a downstream consumer of the messages produced by this tile is slow or stalled, and the message link for that consumer has filled up. This state causes the tile to stop processing upstream messages. + + State Vector 2: + maintenance: the portion of the run loop that executes infrequent, potentially CPU heavy tasks + routine: the portion of the run loop that executes regularly, regardless of the presence of incoming messages + handling: the portion of the run loop that executes as a side effect of an incoming message from an upstream producer tile + */ +export const regimes = [ + "running_maintenance", + "processing_maintenance", + "stalled_maintenance", + "running_routine", + "processing_routine", + "stalled_routine", + "running_handling", + "processing_handling", + // "stalled_handling" is an impossible state, and is therefore excluded +]; diff --git a/src/features/Overview/LiveTileMetrics/index.tsx b/src/features/Overview/LiveTileMetrics/index.tsx new file mode 100644 index 00000000..7ee2bb0a --- /dev/null +++ b/src/features/Overview/LiveTileMetrics/index.tsx @@ -0,0 +1,238 @@ +import { useAtomValue } from "jotai"; +import { + liveTileMetricsAtom, + tilesAtom, + tileTimerAtom, +} from "../../../api/atoms"; +import Card from "../../../components/Card"; +import { Flex, Table, Text } from "@radix-ui/themes"; +import tableStyles from "../../Gossip/table.module.css"; +import styles from "./liveTileMetrics.module.css"; +import { Bars } from "../../StartupProgress/Firedancer/Bars"; +import TileSparkLine from "../SlotPerformance/TileSparkLine"; +import { headerGap } from "../../Gossip/consts"; +import type { Tile, TileMetrics } from "../../../api/types"; +import clsx from "clsx"; +import { useHarmonicIntervalFn, usePrevious } from "react-use"; +import { memo, useEffect, useRef, useState, type CSSProperties } from "react"; +import type { CellProps } from "@radix-ui/themes/components/table"; +import { tileChartDarkBackground } from "../../../colors"; + +const chartHeight = 18; + +export default function LiveTileMetrics() { + return ( + + + Tiles + + + + ); +} + +const MLiveMetricTable = memo(function LiveMetricsTable() { + const tiles = useAtomValue(tilesAtom); + const liveTileMetrics = useAtomValue(liveTileMetricsAtom); + + if (!tiles || !liveTileMetrics) return; + + return ( + + + + + + + + + + + + + + + + + + Name + Heartbeat + Nivcsw + Nvcsw + Backp + + Backp Count + + Utilization + History (1m) + % Hkeep + % Wait + % Backp + % Work + + + + + {tiles.map((tile, i) => ( + + ))} + + + ); +}); + +interface TableRowProps { + tile: Tile; + liveTileMetrics: TileMetrics; + idx: number; +} + +function TableRow({ tile, liveTileMetrics, idx }: TableRowProps) { + const alive = liveTileMetrics.alive[idx]; + const nivcsw = liveTileMetrics.nivcsw[idx]; + const nvcsw = liveTileMetrics.nvcsw[idx]; + const inBackpressure = liveTileMetrics.in_backp[idx]; + const backPressureCount = liveTileMetrics.backp_msgs[idx]; + + const prevNivcsw = usePrevious(nivcsw); + const prevNvcsw = usePrevious(nvcsw); + const prevBackPressureCount = usePrevious(backPressureCount); + + const timers = liveTileMetrics.timers[idx]; + for (let i = 0; i < timers.length; i++) { + if (timers[i] === -1) timers[i] = 0; + } + + const hKeepPct = timers[0] + timers[1] + timers[2]; + const waitPct = timers[3] + timers[6]; + const backpPct = timers[5]; + const workPct = timers[1] + timers[7]; + + return ( + + + {tile.kind}:{tile.kind_id} + + + {alive ? "Live" : "Dead"} + + + {nivcsw.toLocaleString()} | + + +{(nivcsw - (prevNivcsw ?? 0)).toLocaleString()} + + + + {nvcsw.toLocaleString()} | + + +{(nvcsw - (prevNvcsw ?? 0)).toLocaleString()} + + + + {inBackpressure ? "Yes" : "-"} + + + {backPressureCount.toLocaleString()} | + + +{(backPressureCount - (prevBackPressureCount ?? 0)).toLocaleString()} + + + + 1 })} + /> + + 0 })} + /> + + + ); +} + +interface PctCellProps { + pct: number; +} + +function PctCell({ pct, ...props }: PctCellProps & CellProps) { + return ( + + {pct.toFixed(2)}% + + ); +} + +interface UtilizationProps { + idx: number; +} + +const updateIntervalMs = 300; + +const MUtilization = memo(function Utilization({ idx }: UtilizationProps) { + const tileTimers = useAtomValue(tileTimerAtom); + const pct = 1 - Math.max(0, tileTimers?.[idx] ?? 0); + const rollingSum = useRef({ count: 0, sum: 0 }); + const [avgValue, setAvgValue] = useState(pct); + + useEffect(() => { + rollingSum.current.count++; + rollingSum.current.sum += pct; + }, [pct]); + + useHarmonicIntervalFn(() => { + if (rollingSum.current.count === 0) return; + + setAvgValue(rollingSum.current.sum / rollingSum.current.count); + rollingSum.current = { count: 0, sum: 0 }; + }, updateIntervalMs); + + return ( + <> + + + + + + + + + + ); +}); diff --git a/src/features/Overview/LiveTileMetrics/liveTileMetrics.module.css b/src/features/Overview/LiveTileMetrics/liveTileMetrics.module.css new file mode 100644 index 00000000..183f2333 --- /dev/null +++ b/src/features/Overview/LiveTileMetrics/liveTileMetrics.module.css @@ -0,0 +1,48 @@ +.row { + .green { + color: var(--green-9); + } + + .red { + color: var(--red-9); + } + + td { + text-align: left; + &[align="right"] { + text-align: right; + } + vertical-align: middle; + + &.pct-gradient { + color: color-mix( + in srgb, + var(--tile-busy-green-color), + var(--tile-busy-red-color) var(--pct) + ); + } + + &.no-padding { + padding-top: 0; + padding-bottom: 0; + } + + .increment-text { + min-width: 5ch; + display: inline-block; + font-variant-numeric: tabular-nums; + } + } +} + +.header { + position: sticky; + top: 0; + background: rgb(19, 23, 32); +} + +.table { + table { + table-layout: fixed; + } +} diff --git a/src/features/Overview/ShredsProgression/atoms.ts b/src/features/Overview/ShredsProgression/atoms.ts index 162bfa5a..1f9a91d1 100644 --- a/src/features/Overview/ShredsProgression/atoms.ts +++ b/src/features/Overview/ShredsProgression/atoms.ts @@ -5,6 +5,7 @@ import { delayMs, xRangeMs } from "./const"; import { nsPerMs, slotsPerLeader } from "../../../consts"; import { getSlotGroupLeader } from "../../../utils"; import { startupFinalTurbineHeadAtom } from "../../StartupProgress/atoms"; +import { serverTimeMsAtom } from "../../../atoms"; type ShredEventTsDeltaMs = number | undefined; /** @@ -169,11 +170,10 @@ export function createLiveShredsAtoms() { set(_liveShredsAtom, (prev) => { const slotRange = get(_slotRangeAtom); + const now = get(serverTimeMsAtom) ?? Date.now(); if (!prev || !slotRange) return prev; - const now = Date.now(); - if (isStartup) { // During startup, we only show event dots, not spans. Delete slots without events in chart view for ( diff --git a/src/features/Overview/ShredsProgression/shredsProgressionPlugin.ts b/src/features/Overview/ShredsProgression/shredsProgressionPlugin.ts index 49fc9ca8..a0cb349e 100644 --- a/src/features/Overview/ShredsProgression/shredsProgressionPlugin.ts +++ b/src/features/Overview/ShredsProgression/shredsProgressionPlugin.ts @@ -18,12 +18,11 @@ import { shredReplayStartedColor, shredSkippedColor, } from "../../../colors"; -import { skippedClusterSlotsAtom } from "../../../atoms"; +import { serverTimeMsAtom, skippedClusterSlotsAtom } from "../../../atoms"; import { clamp } from "lodash"; import { ShredEvent } from "../../../api/entities"; import { getSlotGroupLabelId, getSlotLabelId } from "./utils"; -import { nsPerMs, slotsPerLeader } from "../../../consts"; -import { serverTimeNanosAtom } from "../../../api/atoms"; +import { slotsPerLeader } from "../../../consts"; const store = getDefaultStore(); export const shredsXScaleKey = "shredsXScaleKey"; @@ -56,7 +55,7 @@ export function shredsProgressionPlugin( const minCompletedSlot = store.get(atoms.minCompletedSlot); const skippedSlotsCluster = store.get(skippedClusterSlotsAtom); const rangeAfterStartup = store.get(atoms.rangeAfterStartup); - const serverTimeNanos = store.get(serverTimeNanosAtom); + const serverTimeMs = store.get(serverTimeMsAtom); const maxX = u.scales[shredsXScaleKey].max; @@ -64,7 +63,7 @@ export function shredsProgressionPlugin( !liveShreds || !slotRange || maxX == null || - serverTimeNanos == null + serverTimeMs == null ) { return; } @@ -81,7 +80,7 @@ export function shredsProgressionPlugin( } // Offset to convert shred event delta to chart x value - const delayedNow = Math.trunc(serverTimeNanos / nsPerMs) - delayMs; + const delayedNow = serverTimeMs - delayMs; const tsXValueOffset = delayedNow - liveShreds.referenceTs; diff --git a/src/features/Overview/SlotPerformance/TileCard.tsx b/src/features/Overview/SlotPerformance/TileCard.tsx index ff2165c6..4f071802 100644 --- a/src/features/Overview/SlotPerformance/TileCard.tsx +++ b/src/features/Overview/SlotPerformance/TileCard.tsx @@ -12,6 +12,7 @@ import { useMeasure } from "react-use"; import type React from "react"; import { useLastDefinedValue, useTileSparkline } from "./useTileSparkline"; import clsx from "clsx"; +import { tileChartDarkBackground } from "../../../colors"; interface TileCardProps { header: string; @@ -79,7 +80,7 @@ export default function TileCard({ value={avgBusy} queryBusy={aggQueryBusyPerTs} height={sparklineHeight} - background={isDark ? "#0000001F" : undefined} + background={isDark ? tileChartDarkBackground : undefined} /> (); @@ -42,6 +50,7 @@ export default function TileSparkLine({ height, width, updateIntervalMs, + tickMs, }); return ( @@ -54,6 +63,7 @@ export default function TileSparkLine({ pxPerTick={pxPerTick} tickMs={chartTickMs} isLive={isLive} + strokeWidth={strokeWidth} /> ); } @@ -71,6 +81,7 @@ interface SparklineProps { pxPerTick: number; tickMs: number; isLive: boolean; + strokeWidth?: number; } export function Sparkline({ svgRef, @@ -82,6 +93,7 @@ export function Sparkline({ pxPerTick, tickMs, isLive, + strokeWidth = strokeLineWidth, }: SparklineProps) { const gRef = useRef(null); const polyRef = useRef(null); @@ -90,11 +102,11 @@ export function Sparkline({ // where the gradient colors start / end, given y scale and offset const gradientRange: SparklineRange = useMemo(() => { const scale = range[1] - range[0]; - const gradientHeight = (height - strokeLineWidth * 2) / scale; + const gradientHeight = (height - strokeWidth * 2) / scale; const top = gradientHeight * (range[1] - 1); const bottom = top + gradientHeight; return [bottom, top]; - }, [height, range]); + }, [height, range, strokeWidth]); const points = useMemo( () => scaledDataPoints.map(({ x, y }) => `${x},${y}`).join(" "), @@ -156,7 +168,7 @@ export function Sparkline({ >(); +clocks.set(defaultTickMs, clockSub(defaultTickMs)); function setDataWindow( data: (PointSample | undefined)[], @@ -105,6 +106,7 @@ interface UseScaledDataPointsProps { width: number; updateIntervalMs: number; stopShifting?: boolean; + tickMs?: number; } export function useScaledDataPoints({ @@ -115,6 +117,7 @@ export function useScaledDataPoints({ width: _width, updateIntervalMs, stopShifting, + tickMs = defaultTickMs, }: UseScaledDataPointsProps) { const [scaledDataPoints, setScaledDataPoints] = useState< { x: number; y: number }[] @@ -134,7 +137,7 @@ export function useScaledDataPoints({ width, windowMs, }; - }, [_width, _windowMs, queryBusy]); + }, [_width, _windowMs, queryBusy, tickMs]); const busyDataRef = useRef([ { value: undefined, ts: performance.now() - windowMs }, @@ -212,13 +215,19 @@ export function useScaledDataPoints({ } // live else { - const unsub = clock.subscribeClock((tEnd) => { - tick(busyDataRef.current, tEnd); - }); + if (!clocks.has(tickMs)) { + clocks.set(tickMs, clockSub(tickMs)); + } + const clock = clocks.get(tickMs); + if (clock) { + const unsub = clock.subscribeClock((tEnd) => { + tick(busyDataRef.current, tEnd); + }); - return unsub; + return unsub; + } } - }, [height, queryBusy, width, windowMs]); + }, [height, queryBusy, tickMs, width, windowMs]); return { scaledDataPoints, diff --git a/src/features/Overview/index.tsx b/src/features/Overview/index.tsx index 00a83a71..aa9d6ca5 100644 --- a/src/features/Overview/index.tsx +++ b/src/features/Overview/index.tsx @@ -5,6 +5,8 @@ import ValidatorsCard from "./ValidatorsCard"; import SlotStatusCard from "./StatusCard"; import EpochCard from "./EpochCard"; import ShredsProgression from "./ShredsProgression"; +import LiveNetworkMetrics from "./LiveNetworkMetrics"; +import LiveTileMetrics from "./LiveTileMetrics"; export default function Overview() { return ( @@ -17,6 +19,8 @@ export default function Overview() { + + ); } diff --git a/src/features/StartupProgress/Firedancer/Bars.tsx b/src/features/StartupProgress/Firedancer/Bars.tsx index 81baf508..bf1aec35 100644 --- a/src/features/StartupProgress/Firedancer/Bars.tsx +++ b/src/features/StartupProgress/Firedancer/Bars.tsx @@ -3,17 +3,18 @@ import clsx from "clsx"; import { clamp } from "lodash"; import { useMeasure } from "react-use"; -const barWidth = 4; -const barGap = barWidth * 1.5; +const _barWidth = 2; const viewBoxHeight = 1000; interface BarsProps { value: number; max: number; + barWidth?: number; } -export function Bars({ value, max }: BarsProps) { +export function Bars({ value, max, barWidth = _barWidth }: BarsProps) { const [ref, { width }] = useMeasure(); + const barGap = barWidth * 1.5; const barCount = Math.trunc(width / (barWidth + barGap)); // only show empty bars if no max, or value is actually 0 @@ -34,7 +35,7 @@ export function Bars({ value, max }: BarsProps) { xmlns="http://www.w3.org/2000/svg" > {Array.from({ length: barCount }, (_, i) => { - const isHigh = i >= barCount * 0.95; + const isHigh = i >= barCount * 0.95 || i === barCount - 1; const isMid = !isHigh && i >= barCount * 0.85; return ( diff --git a/src/features/StartupProgress/Firedancer/bars.module.css b/src/features/StartupProgress/Firedancer/bars.module.css index aa432704..5f1c57d8 100644 --- a/src/features/StartupProgress/Firedancer/bars.module.css +++ b/src/features/StartupProgress/Firedancer/bars.module.css @@ -1,6 +1,6 @@ .bars { width: 100%; - height: 50px; + height: var(--bar-height, 50px); rect { fill: var(--boot-progress-gossip-bars-color); diff --git a/src/utils.ts b/src/utils.ts index ce129a7d..32c71db7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -289,7 +289,7 @@ export function formatBytesAsBits(bytes: number): { unit: string; } { const bits = bytes * 8; - if (bits < 1_000) return { value: bits, unit: "b" }; + if (bits < 1_000) return { value: getRoundedBitsValue(bits), unit: "b" }; if (bits < 1_000_000) return { value: getRoundedBitsValue(bits / 1_000), unit: "Kb" }; if (bits < 1_000_000_000) {