From 45a9e280dffd2b43dcad7474e6c0e3d18f96bb4d Mon Sep 17 00:00:00 2001 From: Ami Suzuki Date: Wed, 26 Nov 2025 18:45:12 -0600 Subject: [PATCH] feat: remaining seconds for startup phases --- src/atoms.ts | 2 + .../StartupProgress/Firedancer/Body.tsx | 13 +- .../Firedancer/CatchingUp/BarsStats.tsx | 13 +- .../Firedancer/CatchingUp/CatchingUpBars.tsx | 7 +- .../CatchingUp/catchingUp.module.css | 3 +- .../Firedancer/CatchingUp/index.tsx | 44 +++++- .../CatchingUp/useCatchingUpRates.ts | 57 ++++---- .../Firedancer/Gossip/index.tsx | 66 ++++++--- .../Firedancer/PhaseHeader.tsx | 62 -------- .../Firedancer/{ => PhaseHeader}/Header.tsx | 12 +- .../{ => PhaseHeader}/ProgressBar.tsx | 13 +- .../{ => PhaseHeader}/header.module.css | 0 .../Firedancer/PhaseHeader/index.tsx | 110 +++++++++++++++ .../PhaseHeader/phaseHeader.module.css | 18 +++ .../{ => PhaseHeader}/progressBar.module.css | 0 .../Snapshot/SnapshotDecompressingCard.tsx | 11 +- .../Snapshot/SnapshotInsertingCard.tsx | 17 +-- .../Snapshot/SnapshotReadingCard.tsx | 7 +- .../Firedancer/Snapshot/index.tsx | 132 ++++++++++++------ .../Firedancer/body.module.css | 15 -- .../StartupProgress/Firedancer/consts.ts | 10 +- src/features/StartupProgress/atoms.ts | 88 +----------- src/hooks/useEma.ts | 2 +- 23 files changed, 375 insertions(+), 327 deletions(-) delete mode 100644 src/features/StartupProgress/Firedancer/PhaseHeader.tsx rename src/features/StartupProgress/Firedancer/{ => PhaseHeader}/Header.tsx (86%) rename src/features/StartupProgress/Firedancer/{ => PhaseHeader}/ProgressBar.tsx (80%) rename src/features/StartupProgress/Firedancer/{ => PhaseHeader}/header.module.css (100%) create mode 100644 src/features/StartupProgress/Firedancer/PhaseHeader/index.tsx create mode 100644 src/features/StartupProgress/Firedancer/PhaseHeader/phaseHeader.module.css rename src/features/StartupProgress/Firedancer/{ => PhaseHeader}/progressBar.module.css (100%) diff --git a/src/atoms.ts b/src/atoms.ts index 7fe0df45..2fcc3201 100644 --- a/src/atoms.ts +++ b/src/atoms.ts @@ -426,6 +426,8 @@ export const peersAtom = atomWithImmer>({}); export const peersListAtom = atom((get) => Object.values(get(peersAtom))); +export const peersCountAtom = atom((get) => get(peersListAtom).length); + export const peersAtomFamily = atomFamily((peer?: string) => atom((get) => (peer !== undefined ? get(peersAtom)[peer] : undefined)), ); diff --git a/src/features/StartupProgress/Firedancer/Body.tsx b/src/features/StartupProgress/Firedancer/Body.tsx index d27dd1fc..de6edfa4 100644 --- a/src/features/StartupProgress/Firedancer/Body.tsx +++ b/src/features/StartupProgress/Firedancer/Body.tsx @@ -6,9 +6,9 @@ import { isStartupProgressExpandedAtom, showStartupProgressAtom, } from "../atoms"; -import { Flex } from "@radix-ui/themes"; +import { Box, Flex } from "@radix-ui/themes"; import clsx from "clsx"; -import { Header } from "./Header"; +import { Header } from "./PhaseHeader/Header"; import { BootPhaseEnum } from "../../../api/entities"; import { bootProgressContainerElAtom } from "../../../atoms"; import Gossip from "./Gossip"; @@ -56,9 +56,8 @@ function BootProgressContent({ phase }: BootProgressContentProps) { const isNarrow = useMedia("(max-width: 750px)"); return ( - setBootProgressContainerEl(el)} - direction="column" overflowY="auto" className={clsx(styles.container, phaseClass, { [styles.collapsed]: !showStartupProgress || !isStartupProgressExpanded, @@ -66,11 +65,11 @@ function BootProgressContent({ phase }: BootProgressContentProps) { >
@@ -78,7 +77,9 @@ function BootProgressContent({ phase }: BootProgressContentProps) { {(phase === BootPhaseEnum.loading_full_snapshot || phase === BootPhaseEnum.loading_incremental_snapshot) && } {phase === BootPhaseEnum.catching_up && } + + - + ); } diff --git a/src/features/StartupProgress/Firedancer/CatchingUp/BarsStats.tsx b/src/features/StartupProgress/Firedancer/CatchingUp/BarsStats.tsx index 359c46e2..6ae0e6ff 100644 --- a/src/features/StartupProgress/Firedancer/CatchingUp/BarsStats.tsx +++ b/src/features/StartupProgress/Firedancer/CatchingUp/BarsStats.tsx @@ -3,21 +3,18 @@ import { useAtomValue } from "jotai"; import styles from "./catchingUp.module.css"; import { catchingUpStartSlotAtom, latestTurbineSlotAtom } from "./atoms"; import { completedSlotAtom } from "../../../../api/atoms"; +import type { CatchingUpRates } from "./useCatchingUpRates"; interface CatchingUpBarsProps { - catchingUpRatesRef: React.MutableRefObject<{ - totalSlotsEstimate?: number; - replaySlotsPerSecond?: number; - turbineSlotsPerSecond?: number; - }>; + catchingUpRates: CatchingUpRates; } -export function BarsStats({ catchingUpRatesRef }: CatchingUpBarsProps) { +export function BarsStats({ catchingUpRates }: CatchingUpBarsProps) { const startSlot = useAtomValue(catchingUpStartSlotAtom); const latestTurbineSlot = useAtomValue(latestTurbineSlotAtom); const latestReplaySlot = useAtomValue(completedSlotAtom); - const replayRate = catchingUpRatesRef.current.replaySlotsPerSecond; - const turbineHeadRate = catchingUpRatesRef.current.turbineSlotsPerSecond; + const replayRate = catchingUpRates.replaySlotsPerSecond; + const turbineHeadRate = catchingUpRates.turbineSlotsPerSecond; const catchUpRate = replayRate == null || turbineHeadRate == null ? undefined diff --git a/src/features/StartupProgress/Firedancer/CatchingUp/CatchingUpBars.tsx b/src/features/StartupProgress/Firedancer/CatchingUp/CatchingUpBars.tsx index 9c39894e..7c6377d9 100644 --- a/src/features/StartupProgress/Firedancer/CatchingUp/CatchingUpBars.tsx +++ b/src/features/StartupProgress/Firedancer/CatchingUp/CatchingUpBars.tsx @@ -14,15 +14,12 @@ import { Box } from "@radix-ui/themes"; import { useThrottledCallback } from "use-debounce"; import { completedSlotAtom } from "../../../../api/atoms"; import { useMeasure } from "react-use"; +import type { CatchingUpRates } from "./useCatchingUpRates"; const emptyChartData: uPlot.AlignedData = [[0], [null]]; interface CatchingUpBarsProps { - catchingUpRatesRef: React.MutableRefObject<{ - totalSlotsEstimate?: number; - replaySlotsPerSecond?: number; - turbineSlotsPerSecond?: number; - }>; + catchingUpRatesRef: React.MutableRefObject; } export function CatchingUpBars({ catchingUpRatesRef }: CatchingUpBarsProps) { const [measureRef, measureRect] = useMeasure(); diff --git a/src/features/StartupProgress/Firedancer/CatchingUp/catchingUp.module.css b/src/features/StartupProgress/Firedancer/CatchingUp/catchingUp.module.css index 4c02e4d4..361b74ac 100644 --- a/src/features/StartupProgress/Firedancer/CatchingUp/catchingUp.module.css +++ b/src/features/StartupProgress/Firedancer/CatchingUp/catchingUp.module.css @@ -5,7 +5,8 @@ padding: 14px; gap: 14px; border: 1px solid rgba(255, 255, 255, 0.1); - background: rgba(234, 103, 103, 0.05); + border-radius: 8px; + background: rgba(250, 250, 250, 0.05); color: var(--boot-progress-primary-text-color); } diff --git a/src/features/StartupProgress/Firedancer/CatchingUp/index.tsx b/src/features/StartupProgress/Firedancer/CatchingUp/index.tsx index 2ca7048f..e506f530 100644 --- a/src/features/StartupProgress/Firedancer/CatchingUp/index.tsx +++ b/src/features/StartupProgress/Firedancer/CatchingUp/index.tsx @@ -1,36 +1,66 @@ -import { Box, Card, Flex, Text } from "@radix-ui/themes"; +import { Box, Flex, Text } from "@radix-ui/themes"; import { CatchingUpBars } from "./CatchingUpBars"; import { BarsFooter } from "./BarsFooter"; import BarsLabels from "./BarsLabels"; import { useAtomValue, useSetAtom } from "jotai"; -import { catchingUpContainerElAtom, hasCatchingUpDataAtom } from "./atoms"; +import { + catchingUpContainerElAtom, + catchingUpStartSlotAtom, + hasCatchingUpDataAtom, + latestTurbineSlotAtom, +} from "./atoms"; import ShredsChart from "../../../Overview/ShredsProgression/ShredsChart"; import styles from "./catchingUp.module.css"; import CatchingUpTiles from "./CatchingUpTiles"; -import { PhaseHeader } from "../PhaseHeader"; +import PhaseHeader from "../PhaseHeader"; import useEstimateTotalSlots from "./useCatchingUpRates"; import { BarsStats } from "./BarsStats"; import { ShredsChartLegend } from "../../../Overview/ShredsProgression/ShredsChartLegend"; +import { completedSlotAtom } from "../../../../api/atoms"; +import { useMemo } from "react"; export default function CatchingUp() { const setContainerEl = useSetAtom(catchingUpContainerElAtom); const hasCatchingUpData = useAtomValue(hasCatchingUpDataAtom); const catchingUpRatesRef = useEstimateTotalSlots(); + const startSlot = useAtomValue(catchingUpStartSlotAtom); + const latestTurbineSlot = useAtomValue(latestTurbineSlotAtom); + const latestReplaySlot = useAtomValue(completedSlotAtom); + + const phaseCompletePct = useMemo(() => { + if ( + startSlot == null || + latestTurbineSlot == null || + latestReplaySlot == null + ) { + return 0; + } + + const totalSlotsToReplay = latestTurbineSlot - startSlot + 1; + if (!totalSlotsToReplay) return 0; + + const replayedSlots = latestReplaySlot - startSlot + 1; + return (100 * replayedSlots) / totalSlotsToReplay; + }, [latestReplaySlot, latestTurbineSlot, startSlot]); return ( <> - + {hasCatchingUpData && ( - + )} - + Shreds @@ -38,7 +68,7 @@ export default function CatchingUp() { - + diff --git a/src/features/StartupProgress/Firedancer/CatchingUp/useCatchingUpRates.ts b/src/features/StartupProgress/Firedancer/CatchingUp/useCatchingUpRates.ts index 25d8292e..0615243d 100644 --- a/src/features/StartupProgress/Firedancer/CatchingUp/useCatchingUpRates.ts +++ b/src/features/StartupProgress/Firedancer/CatchingUp/useCatchingUpRates.ts @@ -1,21 +1,22 @@ import { useAtomValue } from "jotai"; import { useRef, useEffect } from "react"; import { useInterval } from "react-use"; -import { useValuePerSecond } from "../useValuePerSecond"; import { completedSlotAtom } from "../../../../api/atoms"; import { catchingUpStartSlotAtom, latestTurbineSlotAtom } from "./atoms"; - -const rateCalcWindowMs = 10_000; - +import { useEmaValue } from "../../../../hooks/useEma"; + +export interface CatchingUpRates { + targetTotalSlotsEstimate?: number; + totalSlotsEstimate?: number; + replaySlotsPerSecond?: number; + turbineSlotsPerSecond?: number; + remainingSeconds?: number; +} /** * Provides a ref that estimates how many slots will be replayed in total */ export default function useCatchingUpRates() { - const catchingUpRatesRef = useRef<{ - totalSlotsEstimate?: number; - replaySlotsPerSecond?: number; - turbineSlotsPerSecond?: number; - }>({}); + const catchingUpRatesRef = useRef({}); const startSlot = useAtomValue(catchingUpStartSlotAtom); const latestTurbineSlot = useAtomValue(latestTurbineSlotAtom); const latestReplaySlot = useAtomValue(completedSlotAtom); @@ -23,14 +24,11 @@ export default function useCatchingUpRates() { const replaySlot = latestReplaySlot ?? (startSlot == null ? undefined : startSlot - 1); - const { valuePerSecond: replayRate } = useValuePerSecond( - replaySlot, - rateCalcWindowMs, - ); - const { valuePerSecond: turbineRate } = useValuePerSecond( - latestTurbineSlot, - rateCalcWindowMs, - ); + const replayRate = useEmaValue(replaySlot); + const turbineRate = useEmaValue(latestTurbineSlot); + + catchingUpRatesRef.current.replaySlotsPerSecond = replayRate; + catchingUpRatesRef.current.turbineSlotsPerSecond = turbineRate; // initialize estimate of how many slots we'll need to replay // determines initial widths @@ -44,7 +42,7 @@ export default function useCatchingUpRates() { const replaySlotsPerSecond = 400; const turbineSlotsPerSecond = 100; - const totalSlotsEsimtate = calculateTotalSlots( + const totalSlotsEstimate = calculateTotalSlots( replaySlotsPerSecond, turbineSlotsPerSecond, startSlot, @@ -53,9 +51,7 @@ export default function useCatchingUpRates() { ); catchingUpRatesRef.current = { - totalSlotsEstimate: totalSlotsEsimtate, - replaySlotsPerSecond, - turbineSlotsPerSecond, + totalSlotsEstimate, }; }, [latestReplaySlot, latestTurbineSlot, startSlot, catchingUpRatesRef]); @@ -81,6 +77,18 @@ export default function useCatchingUpRates() { latestTurbineSlot, ); + const remainingReplaySlots = + latestReplaySlot == null || newEstimate == null + ? undefined + : newEstimate + startSlot - 1 - latestReplaySlot; + const remainingSeconds = + replayRate === 0 || remainingReplaySlots == null + ? undefined + : remainingReplaySlots / replayRate; + + catchingUpRatesRef.current.remainingSeconds = remainingSeconds; + + // only update total estimate (determining number of bars) if decreasing estimate if (!newEstimate || newEstimate >= prevEstimate) return; // decrement gradually @@ -90,11 +98,8 @@ export default function useCatchingUpRates() { ); const updatedEstimate = prevEstimate - diffToApply; - catchingUpRatesRef.current = { - totalSlotsEstimate: updatedEstimate, - replaySlotsPerSecond: replayRate, - turbineSlotsPerSecond: turbineRate, - }; + + catchingUpRatesRef.current.totalSlotsEstimate = updatedEstimate; }, 500); return catchingUpRatesRef; diff --git a/src/features/StartupProgress/Firedancer/Gossip/index.tsx b/src/features/StartupProgress/Firedancer/Gossip/index.tsx index 7a78f651..fba85567 100644 --- a/src/features/StartupProgress/Firedancer/Gossip/index.tsx +++ b/src/features/StartupProgress/Firedancer/Gossip/index.tsx @@ -3,54 +3,76 @@ import { Card, Flex, Text } from "@radix-ui/themes"; import styles from "./gossip.module.css"; import { gossipNetworkStatsAtom } from "../../../../api/atoms"; import { useAtomValue } from "jotai"; -import { getFmtStake, formatBytesAsBits } from "../../../../utils"; +import { formatBytesAsBits } from "../../../../utils"; import { Bars } from "../Bars"; -import { PhaseHeader } from "../PhaseHeader"; +import PhaseHeader from "../PhaseHeader"; +import { useDebounce } from "use-debounce"; +import { lamportsPerSol } from "../../../../consts"; +import { compactZeroDecimalFormatter } from "../../../../numUtils"; +import { peersCountAtom } from "../../../../atoms"; +import { useEmaValue } from "../../../../hooks/useEma"; const MAX_THROUGHPUT_BYTES = 1_8750_000; // 150Mbit +const TOTAL_PEERS_COUNT = 5_000; export default function Gossip() { + const peersCount = useAtomValue(peersCountAtom); + const phaseCompletePct = (peersCount / TOTAL_PEERS_COUNT) * 100; + const peersCountRate = useEmaValue(peersCount); + const remainingSeconds = + peersCountRate === 0 ? undefined : TOTAL_PEERS_COUNT / peersCountRate; + const networkStats = useAtomValue(gossipNetworkStatsAtom); - if (!networkStats) return null; + const [dbNetworkStats] = useDebounce(networkStats, 100, { + maxWait: 100, + }); - const { health, ingress, egress } = networkStats; + if (!dbNetworkStats) return null; - const connectedStake = - health.connected_stake == null ? null : getFmtStake(health.connected_stake); + const { health, ingress, egress } = dbNetworkStats; - const ingressThroughput = - ingress.total_throughput == null - ? undefined - : formatBytesAsBits(ingress.total_throughput); + const solConnectedStake = Number(health.connected_stake) / lamportsPerSol; + const formattedConnectedStake = + compactZeroDecimalFormatter.format(solConnectedStake); - const egressThroughput = - egress.total_throughput == null - ? undefined - : formatBytesAsBits(egress.total_throughput); + const ingressThroughput = formatBytesAsBits(ingress.total_throughput); + const egressThroughput = formatBytesAsBits(egress.total_throughput); return ( <> - + + + - Ingress {ingressThroughput - ? `${ingressThroughput.value} ${ingressThroughput.unit}` - : "-- Mbit"} + ? `${ingressThroughput.value} ${ingressThroughput.unit}/s` + : "-- Mbit/s"} Egress {egressThroughput - ? `${egressThroughput.value} ${egressThroughput.unit}` - : "-- Mbit"} + ? `${egressThroughput.value} ${egressThroughput.unit}/s` + : "-- Mbit/s"} - {uptimeDuration == null ? "--" : getTimeTillText(uptimeDuration)} - - ); -} - -interface PhaseHeaderProps { - phase: BootPhase; -} -export function PhaseHeader({ phase }: PhaseHeaderProps) { - const step = steps[phase]; - const phasePct = useAtomValue(bootProgressBarPctAtom); - - const prevPhasesPctSum = useMemo(() => { - return ( - Object.values(steps).reduce((acc, { index, estimatedPct }) => { - if (index < step.index) { - acc += estimatedPct; - } - return acc; - }, 0) * 100 - ); - }, [step]); - - const overallPct = - prevPhasesPctSum + Math.round(step.estimatedPct * phasePct); - - return ( - - - - Elapsed - - - {step.name} - - Complete - {overallPct}% - - - - - - ); -} diff --git a/src/features/StartupProgress/Firedancer/Header.tsx b/src/features/StartupProgress/Firedancer/PhaseHeader/Header.tsx similarity index 86% rename from src/features/StartupProgress/Firedancer/Header.tsx rename to src/features/StartupProgress/Firedancer/PhaseHeader/Header.tsx index 5a88edf9..394d5384 100644 --- a/src/features/StartupProgress/Firedancer/Header.tsx +++ b/src/features/StartupProgress/Firedancer/PhaseHeader/Header.tsx @@ -1,16 +1,16 @@ import { Cross1Icon } from "@radix-ui/react-icons"; import { Flex, IconButton, Text, Tooltip } from "@radix-ui/themes"; -import fdLogo from "../../../assets/firedancer_logo.svg"; -import { Cluster } from "../../Header/Cluster"; +import fdLogo from "../../../../assets/firedancer_logo.svg"; +import { Cluster } from "../../../Header/Cluster"; import { isStartupProgressExpandedAtom, expandStartupProgressElAtom, -} from "../atoms"; +} from "../../atoms"; import styles from "./header.module.css"; import { useSetAtom, useAtomValue } from "jotai"; -import PeerIcon from "../../../components/PeerIcon"; -import { useIdentityPeer } from "../../../hooks/useIdentityPeer"; -import { bootProgressContainerElAtom } from "../../../atoms"; +import PeerIcon from "../../../../components/PeerIcon"; +import { useIdentityPeer } from "../../../../hooks/useIdentityPeer"; +import { bootProgressContainerElAtom } from "../../../../atoms"; import { useCallback } from "react"; // TODO update with newer header styles diff --git a/src/features/StartupProgress/Firedancer/ProgressBar.tsx b/src/features/StartupProgress/Firedancer/PhaseHeader/ProgressBar.tsx similarity index 80% rename from src/features/StartupProgress/Firedancer/ProgressBar.tsx rename to src/features/StartupProgress/Firedancer/PhaseHeader/ProgressBar.tsx index dcdd633f..30ca754b 100644 --- a/src/features/StartupProgress/Firedancer/ProgressBar.tsx +++ b/src/features/StartupProgress/Firedancer/PhaseHeader/ProgressBar.tsx @@ -1,17 +1,14 @@ import { Flex } from "@radix-ui/themes"; -import { useAtomValue } from "jotai"; -import { bootProgressBarPctAtom } from "../atoms"; import styles from "./progressBar.module.css"; -import { steps } from "./consts"; -import { BootPhaseEnum } from "../../../api/entities"; +import { BootPhaseEnum } from "../../../../api/entities"; +import { steps } from "../consts"; interface ProgressBarProps { stepIndex: number; + phaseCompletePct: number; } -export function ProgressBar({ stepIndex }: ProgressBarProps) { - const pctComplete = useAtomValue(bootProgressBarPctAtom); - +export function ProgressBar({ stepIndex, phaseCompletePct }: ProgressBarProps) { return ( {Object.entries(steps).map( @@ -47,7 +44,7 @@ export function ProgressBar({ stepIndex }: ProgressBarProps) {
diff --git a/src/features/StartupProgress/Firedancer/header.module.css b/src/features/StartupProgress/Firedancer/PhaseHeader/header.module.css similarity index 100% rename from src/features/StartupProgress/Firedancer/header.module.css rename to src/features/StartupProgress/Firedancer/PhaseHeader/header.module.css diff --git a/src/features/StartupProgress/Firedancer/PhaseHeader/index.tsx b/src/features/StartupProgress/Firedancer/PhaseHeader/index.tsx new file mode 100644 index 00000000..37b51283 --- /dev/null +++ b/src/features/StartupProgress/Firedancer/PhaseHeader/index.tsx @@ -0,0 +1,110 @@ +import styles from "./phaseHeader.module.css"; +import { useEffect, useMemo, useState } from "react"; +import { Box, Flex, Text } from "@radix-ui/themes"; +import { ProgressBar } from "./ProgressBar"; +import { getDurationText, getTimeTillText } from "../../../../utils"; + +import { steps } from "../consts"; +import type { BootPhase } from "../../../../api/types"; + +import { useUptimeDuration } from "../../../../hooks/useUptime"; +import { Duration } from "luxon"; +import { clamp } from "lodash"; +import { useThrottledCallback } from "use-debounce"; + +function TotalDuration() { + const uptimeDuration = useUptimeDuration(1_000); + + return ( + + {uptimeDuration == null ? "--" : getTimeTillText(uptimeDuration)} + + ); +} + +interface PhaseHeaderProps { + phase: BootPhase; + phaseCompletePct: number; + remainingSeconds?: number; +} +export default function PhaseHeader({ + phase, + phaseCompletePct, + remainingSeconds, +}: PhaseHeaderProps) { + const [remaining, setRemaining] = useState(); + const setRemainingThrottled = useThrottledCallback((value?: number) => { + const updatedRemaining = + remainingSeconds == null + ? undefined + : Math.max(Math.round(remainingSeconds), 0); + setRemaining(updatedRemaining); + }, 1_000); + + useEffect(() => { + setRemainingThrottled(remainingSeconds); + }, [setRemainingThrottled, remainingSeconds]); + + const formattedRemaining = useMemo(() => { + if (remaining == null) return undefined; + + return getDurationText( + Duration.fromObject({ + seconds: remaining, + }).rescale(), + { showOnlyTwoSignificantUnits: true }, + ); + }, [remaining]); + + const step = steps[phase]; + + const prevPhasesPctSum = useMemo(() => { + return ( + Object.values(steps).reduce((acc, { index, estimatedPct }) => { + if (index < step.index) { + acc += estimatedPct; + } + return acc; + }, 0) * 100 + ); + }, [step]); + + const overallPct = + prevPhasesPctSum + + Math.round(step.estimatedPct * clamp(phaseCompletePct, 0, 100)); + + return ( + + + + Elapsed + + + + {step.name}... + {formattedRemaining && ( + + Remaining + + ~{formattedRemaining} + + + )} + + + Complete + {overallPct}% + + + + + + ); +} diff --git a/src/features/StartupProgress/Firedancer/PhaseHeader/phaseHeader.module.css b/src/features/StartupProgress/Firedancer/PhaseHeader/phaseHeader.module.css new file mode 100644 index 00000000..14f572cc --- /dev/null +++ b/src/features/StartupProgress/Firedancer/PhaseHeader/phaseHeader.module.css @@ -0,0 +1,18 @@ +.secondary-text { + color: var(--boot-progress-secondary-text-color); +} + +.step-container { + color: var(--boot-progress-primary-text-color); + font-size: 28px; + font-weight: 400; + line-height: normal; + + .step-name { + font-weight: 600; + } + + .no-wrap { + white-space: nowrap; + } +} diff --git a/src/features/StartupProgress/Firedancer/progressBar.module.css b/src/features/StartupProgress/Firedancer/PhaseHeader/progressBar.module.css similarity index 100% rename from src/features/StartupProgress/Firedancer/progressBar.module.css rename to src/features/StartupProgress/Firedancer/PhaseHeader/progressBar.module.css diff --git a/src/features/StartupProgress/Firedancer/Snapshot/SnapshotDecompressingCard.tsx b/src/features/StartupProgress/Firedancer/Snapshot/SnapshotDecompressingCard.tsx index 16e8cd52..463b454e 100644 --- a/src/features/StartupProgress/Firedancer/Snapshot/SnapshotDecompressingCard.tsx +++ b/src/features/StartupProgress/Firedancer/Snapshot/SnapshotDecompressingCard.tsx @@ -1,4 +1,3 @@ -import { useValuePerSecond } from "../useValuePerSecond"; import { bootProgressPhaseAtom } from "../../atoms"; import { useAtomValue } from "jotai"; import { useEffect } from "react"; @@ -11,6 +10,7 @@ import { import { Flex } from "@radix-ui/themes"; import { formatBytes } from "../../../../utils"; import styles from "./snapshot.module.css"; +import { useEma } from "../../../../hooks/useEma"; interface SnapshotDecompressingCardProps { compressedCompleted?: number | null; @@ -23,11 +23,12 @@ export function SnapshotDecompressingCard({ compressedTotal, }: SnapshotDecompressingCardProps) { const phase = useAtomValue(bootProgressPhaseAtom); - const { valuePerSecond: compressedThroughput, reset: resetCompressed } = - useValuePerSecond(compressedCompleted, 1_000); + const { ema: compressedThroughput, reset: resetCompressed } = + useEma(compressedCompleted); - const { valuePerSecond: decompressedThroughput, reset: resetDecompressed } = - useValuePerSecond(decompressedCompleted, 1_000); + const { ema: decompressedThroughput, reset: resetDecompressed } = useEma( + decompressedCompleted, + ); useEffect(() => { // reset throughput history on phase change diff --git a/src/features/StartupProgress/Firedancer/Snapshot/SnapshotInsertingCard.tsx b/src/features/StartupProgress/Firedancer/Snapshot/SnapshotInsertingCard.tsx index 1dffe0f9..08753a83 100644 --- a/src/features/StartupProgress/Firedancer/Snapshot/SnapshotInsertingCard.tsx +++ b/src/features/StartupProgress/Firedancer/Snapshot/SnapshotInsertingCard.tsx @@ -1,7 +1,3 @@ -import { useValuePerSecond } from "../useValuePerSecond"; -import { bootProgressPhaseAtom } from "../../atoms"; -import { useAtomValue } from "jotai"; -import { useEffect } from "react"; import { AccountsRate, SnapshotBarsCard, @@ -13,26 +9,17 @@ import { formatBytes } from "../../../../utils"; import styles from "./snapshot.module.css"; interface SnapshotInsertingCardProps { + decompressedThroughput?: number; decompressedCompleted?: number | null; decompressedTotal?: number | null; cumulativeAccounts?: number | null; } export function SnapshotInsertingCard({ + decompressedThroughput, decompressedCompleted, decompressedTotal, cumulativeAccounts, }: SnapshotInsertingCardProps) { - const phase = useAtomValue(bootProgressPhaseAtom); - const { valuePerSecond: decompressedThroughput, reset } = useValuePerSecond( - decompressedCompleted, - 1_000, - ); - - useEffect(() => { - // reset throughput history on phase change - reset(); - }, [phase, reset]); - const throughputObj = decompressedThroughput == null ? undefined diff --git a/src/features/StartupProgress/Firedancer/Snapshot/SnapshotReadingCard.tsx b/src/features/StartupProgress/Firedancer/Snapshot/SnapshotReadingCard.tsx index ac99c512..cfa64300 100644 --- a/src/features/StartupProgress/Firedancer/Snapshot/SnapshotReadingCard.tsx +++ b/src/features/StartupProgress/Firedancer/Snapshot/SnapshotReadingCard.tsx @@ -1,4 +1,3 @@ -import { useValuePerSecond } from "../useValuePerSecond"; import { bootProgressPhaseAtom } from "../../atoms"; import { useAtomValue } from "jotai"; import { useEffect, useMemo } from "react"; @@ -11,6 +10,7 @@ import { } from "./SnapshotBarsCard"; import { formatBytes } from "../../../../utils"; import styles from "./snapshot.module.css"; +import { useEma } from "../../../../hooks/useEma"; interface SnapshotReadingCardProps { compressedCompleted?: number | null; @@ -23,10 +23,7 @@ export function SnapshotReadingCard({ readPath, }: SnapshotReadingCardProps) { const phase = useAtomValue(bootProgressPhaseAtom); - const { valuePerSecond: throughput, reset } = useValuePerSecond( - completed, - 1_000, - ); + const { ema: throughput, reset } = useEma(completed); useEffect(() => { // reset throughput history on phase change diff --git a/src/features/StartupProgress/Firedancer/Snapshot/index.tsx b/src/features/StartupProgress/Firedancer/Snapshot/index.tsx index e3d67271..a777cb6d 100644 --- a/src/features/StartupProgress/Firedancer/Snapshot/index.tsx +++ b/src/features/StartupProgress/Firedancer/Snapshot/index.tsx @@ -10,7 +10,9 @@ import SnapshotSparklineCard from "./SnapshotSparklineCard"; import { SnapshotReadingCard } from "./SnapshotReadingCard"; import { SnapshotDecompressingCard } from "./SnapshotDecompressingCard"; import { SnapshotInsertingCard } from "./SnapshotInsertingCard"; -import { PhaseHeader } from "../PhaseHeader"; +import PhaseHeader from "../PhaseHeader"; +import { useEffect } from "react"; +import { useEma } from "../../../../hooks/useEma"; const rowGap = "5"; const columnGap = "26px"; @@ -34,35 +36,53 @@ function getSnapshotValues(bootProgress: BootProgress) { loading_incremental_snapshot_insert_accounts, } = bootProgress; - if ( + const values = bootProgress.phase === BootPhaseEnum.loading_full_snapshot || !loading_incremental_snapshot_total_bytes_compressed - ) { - return { - totalCompressedBytes: loading_full_snapshot_total_bytes_compressed, - readCompressedBytes: loading_full_snapshot_read_bytes_compressed, - readPath: loading_full_snapshot_read_path, - decompressCompressedBytes: - loading_full_snapshot_decompress_bytes_compressed, - decompressDecompressedBytes: - loading_full_snapshot_decompress_bytes_decompressed, - insertDecompressedBytes: loading_full_snapshot_insert_bytes_decompressed, - insertAccounts: loading_full_snapshot_insert_accounts, - }; - } - - return { - totalCompressedBytes: loading_incremental_snapshot_total_bytes_compressed, - readCompressedBytes: loading_incremental_snapshot_read_bytes_compressed, - readPath: loading_incremental_snapshot_read_path, - decompressCompressedBytes: - loading_incremental_snapshot_decompress_bytes_compressed, - decompressDecompressedBytes: - loading_incremental_snapshot_decompress_bytes_decompressed, - insertDecompressedBytes: - loading_incremental_snapshot_insert_bytes_decompressed, - insertAccounts: loading_incremental_snapshot_insert_accounts, - }; + ? { + totalCompressedBytes: loading_full_snapshot_total_bytes_compressed, + readCompressedBytes: loading_full_snapshot_read_bytes_compressed, + readPath: loading_full_snapshot_read_path, + decompressCompressedBytes: + loading_full_snapshot_decompress_bytes_compressed, + decompressDecompressedBytes: + loading_full_snapshot_decompress_bytes_decompressed, + insertDecompressedBytes: + loading_full_snapshot_insert_bytes_decompressed, + insertAccounts: loading_full_snapshot_insert_accounts, + } + : { + totalCompressedBytes: + loading_incremental_snapshot_total_bytes_compressed, + readCompressedBytes: + loading_incremental_snapshot_read_bytes_compressed, + readPath: loading_incremental_snapshot_read_path, + decompressCompressedBytes: + loading_incremental_snapshot_decompress_bytes_compressed, + decompressDecompressedBytes: + loading_incremental_snapshot_decompress_bytes_decompressed, + insertDecompressedBytes: + loading_incremental_snapshot_insert_bytes_decompressed, + insertAccounts: loading_incremental_snapshot_insert_accounts, + }; + + const insertCompressedBytes = + values.insertDecompressedBytes && + values.decompressCompressedBytes && + values.decompressDecompressedBytes + ? values.insertDecompressedBytes * + (values.decompressCompressedBytes / values.decompressDecompressedBytes) + : 0; + + const totalDecompressedBytes = + values.totalCompressedBytes && + values.decompressCompressedBytes && + values.decompressDecompressedBytes + ? (values.totalCompressedBytes * values.decompressDecompressedBytes) / + values.decompressCompressedBytes + : 0; + + return { ...values, insertCompressedBytes, totalDecompressedBytes }; } export default function Snapshot() { @@ -73,7 +93,18 @@ export default function Snapshot() { const wrap = isNarrowScreen ? "wrap" : "nowrap"; const gap = isNarrowScreen ? columnGap : rowGap; - if (!bootProgress) return; + const snapshotValues = bootProgress + ? getSnapshotValues(bootProgress) + : undefined; + + const { ema: decompressedInputThroughput, reset } = useEma( + snapshotValues?.insertDecompressedBytes, + ); + + useEffect(() => { + // reset throughput history on phase change + reset(); + }, [bootProgress?.phase, reset]); const { totalCompressedBytes, @@ -83,27 +114,37 @@ export default function Snapshot() { decompressDecompressedBytes, insertDecompressedBytes, insertAccounts, - } = getSnapshotValues(bootProgress); - const insertCompressedBytes = - insertDecompressedBytes && - decompressCompressedBytes && - decompressDecompressedBytes - ? insertDecompressedBytes * - (decompressCompressedBytes / decompressDecompressedBytes) - : 0; + insertCompressedBytes, + totalDecompressedBytes, + } = snapshotValues ?? {}; - const totalDecompressedBytes = - totalCompressedBytes && - decompressCompressedBytes && - decompressDecompressedBytes - ? (totalCompressedBytes * decompressDecompressedBytes) / - decompressCompressedBytes - : 0; + const remainingSeconds = + decompressedInputThroughput == null || + totalDecompressedBytes == null || + insertDecompressedBytes == null + ? undefined + : Math.round( + (totalDecompressedBytes - insertDecompressedBytes) / + decompressedInputThroughput, + ); + + const phaseCompletePct = Math.min( + totalCompressedBytes && insertCompressedBytes + ? (insertCompressedBytes / totalCompressedBytes) * 100 + : 0, + 100, + ); + + if (!bootProgress || !snapshotValues) return; return ( <> - + ( (get) => get(bootProgressAtom)?.phase, @@ -46,82 +43,3 @@ export const isStartupProgressVisibleAtom = atom((get) => { } return true; }); - -export const bootProgressBarPctAtom = atom((get) => { - const bootProgress = get(bootProgressAtom); - if (!bootProgress) return 0; - - switch (bootProgress.phase) { - case BootPhaseEnum.joining_gossip: { - return 0; - } - case BootPhaseEnum.loading_full_snapshot: { - const total = bootProgress.loading_full_snapshot_total_bytes_compressed; - const insert = - bootProgress.loading_full_snapshot_insert_bytes_decompressed; - const decompress_compressed = - bootProgress.loading_full_snapshot_decompress_bytes_compressed; - const decompress_decompressed = - bootProgress.loading_full_snapshot_decompress_bytes_decompressed; - - if ( - !insert || - !decompress_compressed || - !decompress_decompressed || - !total - ) { - return 0; - } - - const insertCompleted = - insert * (decompress_compressed / decompress_decompressed); - - return Math.min(100, (insertCompleted / total) * 100); - } - case BootPhaseEnum.loading_incremental_snapshot: { - const total = - bootProgress.loading_incremental_snapshot_total_bytes_compressed; - const insert = - bootProgress.loading_incremental_snapshot_insert_bytes_decompressed; - const decompress_compressed = - bootProgress.loading_incremental_snapshot_decompress_bytes_decompressed; - const decompress_decompressed = - bootProgress.loading_incremental_snapshot_decompress_bytes_decompressed; - - if ( - !insert || - !decompress_compressed || - !decompress_decompressed || - !total - ) { - return 0; - } - - const insertCompleted = - insert * (decompress_compressed / decompress_decompressed); - - return Math.min(100, (insertCompleted / total) * 100); - } - case BootPhaseEnum.catching_up: { - const startSlot = get(catchingUpStartSlotAtom); - const latestTurbineSlot = get(latestTurbineSlotAtom); - const latestReplaySlot = get(completedSlotAtom); - - if ( - startSlot == null || - latestTurbineSlot == null || - latestReplaySlot == null - ) - return 0; - - const totalSlotsToReplay = latestTurbineSlot - startSlot + 1; - if (!totalSlotsToReplay) return 0; - - const replayedSlots = latestReplaySlot - startSlot + 1; - return (100 * replayedSlots) / totalSlotsToReplay; - } - case BootPhaseEnum.running: { - return 0; - } - } -}); diff --git a/src/hooks/useEma.ts b/src/hooks/useEma.ts index 89401a40..3b0ac968 100644 --- a/src/hooks/useEma.ts +++ b/src/hooks/useEma.ts @@ -15,7 +15,7 @@ const defaultUseEmaOptions = { export function useEma( cumulativeValue: number | null | undefined, - _options: UseEmaOptions, + _options?: UseEmaOptions, ) { const { forceUpdateIntervalMs, halfLifeMs, initMinSamples } = { ...defaultUseEmaOptions,