Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,8 @@ export const peersAtom = atomWithImmer<Record<string, Peer>>({});

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)),
);
Expand Down
13 changes: 7 additions & 6 deletions src/features/StartupProgress/Firedancer/Body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -56,29 +56,30 @@ function BootProgressContent({ phase }: BootProgressContentProps) {
const isNarrow = useMedia("(max-width: 750px)");

return (
<Flex
<Box
ref={(el: HTMLDivElement) => setBootProgressContainerEl(el)}
direction="column"
overflowY="auto"
className={clsx(styles.container, phaseClass, {
[styles.collapsed]: !showStartupProgress || !isStartupProgressExpanded,
})}
>
<Flex
direction="column"
height="100%"
width="100%"
maxWidth={appMaxWidth}
mx="auto"
px={isNarrow ? "20px" : "89px"}
pb="20px"
>
<Header />

{phase === BootPhaseEnum.joining_gossip && <Gossip />}
{(phase === BootPhaseEnum.loading_full_snapshot ||
phase === BootPhaseEnum.loading_incremental_snapshot) && <Snapshot />}
{phase === BootPhaseEnum.catching_up && <CatchingUp />}

<Box pb="20px" />
</Flex>
</Flex>
</Box>
);
}
13 changes: 5 additions & 8 deletions src/features/StartupProgress/Firedancer/CatchingUp/BarsStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,12 @@ import { catchingUpBarsPlugin } from "./catchingUpBarsPlugin";
import { Box } from "@radix-ui/themes";
import { useThrottledCallback } from "use-debounce";
import { completedSlotAtom } from "../../../../api/atoms";
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<CatchingUpRates>;
}
export function CatchingUpBars({ catchingUpRatesRef }: CatchingUpBarsProps) {
const startSlot = useAtomValue(catchingUpStartSlotAtom);
Expand Down
38 changes: 34 additions & 4 deletions src/features/StartupProgress/Firedancer/CatchingUp/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,59 @@ 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 { 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 (
<>
<PhaseHeader phase="catching_up" />
<PhaseHeader
phase="catching_up"
phaseCompletePct={phaseCompletePct}
remainingSeconds={catchingUpRatesRef.current.remainingSeconds}
/>
<Flex direction="column" height="100%" mt="8px" gap="8px">
{hasCatchingUpData && (
<Flex ref={setContainerEl} direction="column" gap="5px">
<BarsLabels />
<CatchingUpBars catchingUpRatesRef={catchingUpRatesRef} />
<BarsFooter />
<BarsStats catchingUpRatesRef={catchingUpRatesRef} />
<BarsStats catchingUpRates={catchingUpRatesRef.current} />
</Flex>
)}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,34 @@
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<CatchingUpRates>({});
const startSlot = useAtomValue(catchingUpStartSlotAtom);
const latestTurbineSlot = useAtomValue(latestTurbineSlotAtom);
const latestReplaySlot = useAtomValue(completedSlotAtom);

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
Expand All @@ -44,7 +42,7 @@ export default function useCatchingUpRates() {

const replaySlotsPerSecond = 400;
const turbineSlotsPerSecond = 100;
const totalSlotsEsimtate = calculateTotalSlots(
const totalSlotsEstimate = calculateTotalSlots(
replaySlotsPerSecond,
turbineSlotsPerSecond,
startSlot,
Expand All @@ -53,9 +51,7 @@ export default function useCatchingUpRates() {
);

catchingUpRatesRef.current = {
totalSlotsEstimate: totalSlotsEsimtate,
replaySlotsPerSecond,
turbineSlotsPerSecond,
totalSlotsEstimate,
};
}, [latestReplaySlot, latestTurbineSlot, startSlot, catchingUpRatesRef]);

Expand All @@ -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
Expand All @@ -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;
Expand Down
66 changes: 44 additions & 22 deletions src/features/StartupProgress/Firedancer/Gossip/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
<PhaseHeader phase="joining_gossip" />
<PhaseHeader
phase="joining_gossip"
phaseCompletePct={phaseCompletePct}
remainingSeconds={remainingSeconds}
/>

<Flex gapX="162px" mt="52px">
<Flex direction="column" gap="20px" flexGrow="1" flexBasis="1">
<Flex justify="between" gap="20px" align="stretch">
<GossipCard
title="Staked Peers"
value={health.connected_staked_peers}
value={health.connected_staked_peers.toLocaleString(undefined, {
maximumFractionDigits: 0,
})}
/>
<GossipCard
title="Unstaked Peers"
value={health.connected_unstaked_peers}
value={health.connected_unstaked_peers.toLocaleString(undefined, {
maximumFractionDigits: 0,
})}
/>
<GossipCard
title="Connected Stake"
value={formattedConnectedStake}
/>
<GossipCard title="Connected Stake" value={connectedStake} />
</Flex>

<Flex direction="column" gap="10px">
<Text className={styles.barTitle}>Ingress</Text>
<Text className={styles.barValue}>
{ingressThroughput
? `${ingressThroughput.value} ${ingressThroughput.unit}`
: "-- Mbit"}
? `${ingressThroughput.value} ${ingressThroughput.unit}/s`
: "-- Mbit/s"}
</Text>
<Bars
value={ingress.total_throughput ?? 0}
Expand All @@ -62,8 +84,8 @@ export default function Gossip() {
<Text className={styles.barTitle}>Egress</Text>
<Text className={styles.barValue}>
{egressThroughput
? `${egressThroughput.value} ${egressThroughput.unit}`
: "-- Mbit"}
? `${egressThroughput.value} ${egressThroughput.unit}/s`
: "-- Mbit/s"}
</Text>
<Bars
value={egress.total_throughput ?? 0}
Expand Down
Loading