Skip to content
Merged
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
32 changes: 15 additions & 17 deletions src/api/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,15 +497,17 @@ export const epochSchema = z.discriminatedUnion("key", [
]);

const gossipNetworkHealthSchema = z.object({
push_rx_pct: z.number().nullable().optional(),
pull_response_rx_pct: z.number().nullable().optional(),
push_rx_dup_pct: z.number().nullable().optional(),
pull_response_rx_dup_pct: z.number().nullable().optional(),
push_rx_msg_bad_pct: z.number().nullable().optional(),
push_rx_entry_bad_pct: z.number().nullable().optional(),
pull_response_rx_msg_bad_pct: z.number().nullable().optional(),
pull_response_rx_entry_bad_pct: z.number().nullable().optional(),
pull_already_known_pct: z.number().nullable().optional(),
num_push_messages_rx_success: z.number().nullable().optional(),
num_push_messages_rx_failure: z.number().nullable().optional(),
num_push_entries_rx_success: z.number().nullable().optional(),
num_push_entries_rx_failure: z.number().nullable().optional(),
num_push_entries_rx_duplicate: z.number().nullable().optional(),
num_pull_response_messages_rx_success: z.number().nullable().optional(),
num_pull_response_messages_rx_failure: z.number().nullable().optional(),
num_pull_response_entries_rx_success: z.number().nullable().optional(),
num_pull_response_entries_rx_failure: z.number().nullable().optional(),
num_pull_response_entries_rx_duplicate: z.number().nullable().optional(),

total_stake: z.coerce.bigint().nullable().optional(),
total_staked_peers: z.number().nullable().optional(),
total_unstaked_peers: z.number().nullable().optional(),
Expand All @@ -531,14 +533,10 @@ const gossipStorageStatsSchema = z.object({
});

const gossipMessageStatsSchema = z.object({
bytes_rx_total: z.number().array().nullable().optional(),
count_rx_total: z.number().array().nullable().optional(),
bytes_tx_total: z.number().array().nullable().optional(),
count_tx_total: z.number().array().nullable().optional(),
bps_rx: z.number().array().nullable().optional(),
mps_rx: z.number().array().nullable().optional(),
bps_tx: z.number().array().nullable().optional(),
mps_tx: z.number().array().nullable().optional(),
num_bytes_rx: z.number().array().nullable().optional(),
num_bytes_tx: z.number().array().nullable().optional(),
num_messages_rx: z.number().array().nullable().optional(),
num_messages_tx: z.number().array().nullable().optional(),
});

export const gossipNetworkStatsSchema = z.object({
Expand Down
62 changes: 29 additions & 33 deletions src/features/StartupProgress/Firedancer/Bars.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { Flex, Text } from "@radix-ui/themes";

import styles from "./bars.module.css";
import clsx from "clsx";
import { useMeasure } from "react-use";
Expand All @@ -26,36 +24,34 @@ export function Bars({ title, value, max }: BarsProps) {
const usedWidth = barCount * (barWidth + barGap);

return (
<Flex direction="column" gap="10px" className={styles.barsContainer}>
{title && <Text className={styles.title}>{title}</Text>}
<svg
ref={ref}
preserveAspectRatio="none"
width="100%"
viewBox={`0 0 ${usedWidth} ${viewBoxHeight}`}
xmlns="http://www.w3.org/2000/svg"
>
{Array.from({ length: barCount }, (_, i) => {
const isHigh = i >= barCount * 0.95;
const isMid = !isHigh && i >= barCount * 0.85;

return (
<rect
key={i}
x={i * (usedWidth / barCount)}
width={barWidth}
height={viewBoxHeight}
ry={barWidth * 2}
className={clsx({
[styles.threshold]: i === currentIndex,
[styles.filled]: i < currentIndex,
[styles.high]: isHigh,
[styles.mid]: isMid,
})}
/>
);
})}
</svg>
</Flex>
<svg
className={styles.bars}
ref={ref}
preserveAspectRatio="none"
width="100%"
viewBox={`0 0 ${usedWidth} ${viewBoxHeight}`}
xmlns="http://www.w3.org/2000/svg"
>
{Array.from({ length: barCount }, (_, i) => {
const isHigh = i >= barCount * 0.95;
const isMid = !isHigh && i >= barCount * 0.85;

return (
<rect
key={i}
x={i * (usedWidth / barCount)}
width={barWidth}
height={viewBoxHeight}
ry={barWidth * 2}
className={clsx({
[styles.threshold]: i === currentIndex,
[styles.filled]: i < currentIndex,
[styles.high]: isHigh,
[styles.mid]: isMid,
})}
/>
);
})}
</svg>
);
}
55 changes: 48 additions & 7 deletions src/features/StartupProgress/Firedancer/GossipProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,71 @@ import styles from "./gossip.module.css";
import { Bars } from "./Bars";
import { useAtomValue } from "jotai";
import { gossipNetworkStatsAtom } from "../../../api/atoms";
import { formatBytesAsBits, getFmtStake } from "../../../utils";

const MAX_THROUGHPUT_BYTES = 1_8750_000; // 150Mbit

export function GossipProgress() {
const networkStats = useAtomValue(gossipNetworkStatsAtom);
if (!networkStats) return null;

const { health } = networkStats;
const { health, ingress, egress } = networkStats;

const connectedStake =
health.connected_stake == null ? null : getFmtStake(health.connected_stake);

const ingressThroughput =
ingress.total_throughput == null
? undefined
: formatBytesAsBits(ingress.total_throughput);

const egressThroughput =
egress.total_throughput == null
? undefined
: formatBytesAsBits(egress.total_throughput);

return (
<Flex gapX="162px">
<Flex direction="column" gap="20px">
<Flex justify="between" gap="20px" align="stretch">
<GossipCard
title="Staked Peers"
value={health.connected_staked_peers ?? null}
value={health.connected_staked_peers}
/>
<GossipCard
title="Unstaked Peers"
value={health.connected_unstaked_peers ?? null}
value={health.connected_unstaked_peers}
/>
<GossipCard title="Snapshot Peers" value={0} />
<GossipCard title="Connected Stake" value={connectedStake} />
</Flex>

<Bars title="Ingress" value={19} max={20} />
<Bars title="Egress" value={5} max={20} />
<Flex direction="column" gap="10px">
<Text className={styles.barTitle}>Ingress</Text>
<Text className={styles.barValue}>
{ingressThroughput
? `${ingressThroughput.value} ${ingressThroughput.unit}`
: "-- Mbit"}
</Text>
<Bars
title="Ingress"
value={ingress.total_throughput ?? 0}
max={MAX_THROUGHPUT_BYTES}
/>
</Flex>

<Flex direction="column" gap="10px">
<Text className={styles.barTitle}>Egress</Text>
<Text className={styles.barValue}>
{egressThroughput
? `${egressThroughput.value} ${egressThroughput.unit}`
: "-- Mbit"}
</Text>
<Bars
title="Egress"
value={egress.total_throughput ?? 0}
max={MAX_THROUGHPUT_BYTES}
/>
</Flex>
</Flex>
<Text>Stake Discovered</Text>
</Flex>
Expand All @@ -36,7 +77,7 @@ export function GossipProgress() {

interface GossipCardProps {
title: string;
value: number | null;
value?: number | string | null;
}
function GossipCard({ title, value }: GossipCardProps) {
return (
Expand Down
63 changes: 24 additions & 39 deletions src/features/StartupProgress/Firedancer/bars.module.css
Original file line number Diff line number Diff line change
@@ -1,48 +1,33 @@
.bars-container {
color: var(--boot-progress-primary-text-color);
font-size: 18px;
font-style: normal;
font-weight: 400;
line-height: normal;
.bars {
width: 100%;
height: 77px;

.title {
font-size: 28px;
font-style: normal;
font-weight: 400;
line-height: normal;
}

svg {
width: 100%;
height: 77px;
rect {
fill: var(--boot-progress-gossip-bars-color);
&.threshold {
fill: var(--app-teal);
}
&.filled {
fill: var(--boot-progress-gossip-filled-bar-color);
}

rect {
fill: var(--boot-progress-gossip-bars-color);
&.threshold {
fill: var(--app-teal);
}
&.mid {
fill: var(--boot-progress-gossip-mid-bar-color);
&.filled {
fill: var(--boot-progress-gossip-filled-bar-color);
fill: var(--boot-progress-gossip-mid-filled-bar-color);
}

&.mid {
fill: var(--boot-progress-gossip-mid-bar-color);
&.filled {
fill: var(--boot-progress-gossip-mid-filled-bar-color);
}
&.threshold {
fill: var(--boot-progress-gossip-mid-threshold-bar-color);
}
&.threshold {
fill: var(--boot-progress-gossip-mid-threshold-bar-color);
}
}

&.high {
fill: var(--boot-progress-gossip-high-bar-color);
&.filled {
fill: var(--boot-progress-gossip-high-filled-bar-color);
}
&.threshold {
fill: var(--boot-progress-gossip-high-threshold-bar-color);
}
&.high {
fill: var(--boot-progress-gossip-high-bar-color);
&.filled {
fill: var(--boot-progress-gossip-high-filled-bar-color);
}
&.threshold {
fill: var(--boot-progress-gossip-high-threshold-bar-color);
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions src/features/StartupProgress/Firedancer/gossip.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,19 @@
line-height: normal;
}
}

.bar-title {
color: var(--boot-progress-primary-text-color);
font-size: 28px;
font-style: normal;
font-weight: 400;
line-height: normal;
}

.bar-value {
color: var(--boot-progress-primary-text-color);
font-size: 18px;
font-style: normal;
font-weight: 400;
line-height: normal;
}
25 changes: 25 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,28 @@ export function getClusterColor(cluster?: Cluster) {
return clusterUnknownColor;
}
}

export function formatBytesAsBits(bytes: number): {
value: number;
unit: string;
} {
const bits = bytes * 8;
if (bits < 1_000) return { value: bits, unit: "bit" };
if (bits < 1_000_000)
return { value: getRoundedBitsValue(bits / 1_000), unit: "Kbit" };
if (bits < 1_000_000_000) {
return { value: getRoundedBitsValue(bits / 1_000_000), unit: "Mbit" };
}

return { value: getRoundedBitsValue(bits / 1_000_000_000), unit: "Gbit" };
}

/**
* Round to 1 decimal place if value is <= 10, otherwise round to nearest integer
*/
function getRoundedBitsValue(value: number) {
if (value >= 9.5) return Math.round(value);

// 1 decimal place
return Math.round(value * 10) / 10;
}