Skip to content

Commit 6500c9f

Browse files
feat: new overview tile and network stats
1 parent 353d8ed commit 6500c9f

File tree

18 files changed

+556
-19
lines changed

18 files changed

+556
-19
lines changed

src/api/atoms.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import type {
2929
GossipPeersSize,
3030
GossipPeersRowsUpdate,
3131
GossipPeersCellUpdate,
32+
LiveNetworkMetrics,
33+
TileMetrics,
3234
} from "./types";
3335
import { rafAtom } from "../atomUtils";
3436

@@ -70,6 +72,10 @@ export const estimatedSlotDurationAtom = atom<
7072

7173
export const estimatedTpsAtom = atom<EstimatedTps | undefined>(undefined);
7274

75+
export const liveNetworkMetricsAtom = atom<LiveNetworkMetrics | undefined>(
76+
undefined,
77+
);
78+
7379
export const liveTxnWaterfallAtom = rafAtom<LiveTxnWaterfall | undefined>(
7480
undefined,
7581
);
@@ -78,6 +84,8 @@ export const liveTilePrimaryMetricAtom = atom<
7884
LiveTilePrimaryMetric | undefined
7985
>(undefined);
8086

87+
export const liveTileMetricsAtom = atom<TileMetrics | undefined>(undefined);
88+
8189
export const tileTimerAtom = atom<number[] | undefined>(undefined);
8290

8391
export const bootProgressAtom = atom<BootProgress | undefined>(undefined);

src/api/consts.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
export const estimatedTpsDebounceMs = 400;
22
export const liveMetricsDebounceMs = 100;
3+
export const liveTileMetricsDebounceMs = 130;
4+
export const liveNetworkMetricsDebounceMs = 130;
35
export const waterfallDebounceMs = 100;
46
export const tileTimerDebounceMs = 25;
7+
export const gossipNetworkDebounceMs = 300;
8+
export const gossipPeerSizeDebounceMs = 1_000;

src/api/entities.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export const resetSlotSchema = z.number();
126126
export const storageSlotSchema = z.number();
127127
export const voteSlotSchema = z.number();
128128
export const slotCaughtUpSchema = z.number().nullable();
129+
export const activeForkCountSchema = z.number();
129130

130131
export const estimatedSlotDurationSchema = z.number();
131132

@@ -501,6 +502,10 @@ export const summarySchema = z.discriminatedUnion("key", [
501502
key: z.literal("slot_caught_up"),
502503
value: slotCaughtUpSchema,
503504
}),
505+
summaryTopicSchema.extend({
506+
key: z.literal("active_fork_count"),
507+
value: activeForkCountSchema,
508+
}),
504509
summaryTopicSchema.extend({
505510
key: z.literal("estimated_slot_duration_nanos"),
506511
value: estimatedSlotDurationSchema,

src/api/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ import type {
5757
gossipStorageStatsSchema,
5858
gossipMessageStatsSchema,
5959
schedulerCountsSchema,
60+
liveNetworkMetricsSchema,
61+
tileMetricsSchema,
6062
} from "./entities";
6163

6264
export type Client = z.infer<typeof clientSchema>;
@@ -102,12 +104,16 @@ export type EstimatedSlotDuration = z.infer<typeof estimatedSlotDurationSchema>;
102104

103105
export type EstimatedTps = z.infer<typeof estimatedTpsSchema>;
104106

107+
export type LiveNetworkMetrics = z.infer<typeof liveNetworkMetricsSchema>;
108+
105109
export type LiveTxnWaterfall = z.infer<typeof liveTxnWaterfallSchema>;
106110

107111
export type LiveTilePrimaryMetric = z.infer<typeof liveTilePrimaryMetricSchema>;
108112

109113
export type TilePrimaryMetric = z.infer<typeof tilePrimaryMetricSchema>;
110114

115+
export type TileMetrics = z.infer<typeof tileMetricsSchema>;
116+
111117
export type TxnWaterfallIn = z.infer<typeof txnWaterfallInSchema>;
112118
export type TxnWaterfallOut = z.infer<typeof txnWaterfallOutSchema>;
113119
export type TxnWaterfall = z.infer<typeof txnWaterfallSchema>;

src/api/useSetAtomWsData.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import {
2727
gossipPeersSizeAtom,
2828
gossipPeersRowsUpdateAtom,
2929
gossipPeersCellUpdateAtom,
30+
liveNetworkMetricsAtom,
31+
liveTileMetricsAtom,
3032
} from "./atoms";
3133
import {
3234
blockEngineSchema,
@@ -57,12 +59,14 @@ import type {
5759
EstimatedTps,
5860
GossipNetworkStats,
5961
GossipPeersSize,
62+
LiveNetworkMetrics,
6063
LiveTilePrimaryMetric,
6164
LiveTxnWaterfall,
6265
Peer,
6366
PeerRemove,
6467
RepairSlot,
6568
SlotResponse,
69+
TileMetrics,
6670
TurbineSlot,
6771
} from "./types";
6872
import { useDebouncedCallback, useThrottledCallback } from "use-debounce";
@@ -71,7 +75,11 @@ import { useServerMessages } from "./ws/utils";
7175
import { DateTime } from "luxon";
7276
import {
7377
estimatedTpsDebounceMs,
78+
gossipNetworkDebounceMs,
79+
gossipPeerSizeDebounceMs,
7480
liveMetricsDebounceMs,
81+
liveNetworkMetricsDebounceMs,
82+
liveTileMetricsDebounceMs,
7583
tileTimerDebounceMs,
7684
waterfallDebounceMs,
7785
} from "./consts";
@@ -132,6 +140,19 @@ export function useSetAtomWsData() {
132140
setEstimatedTps(value);
133141
}, estimatedTpsDebounceMs);
134142

143+
const setLiveNetworkMetrics = useSetAtom(liveNetworkMetricsAtom);
144+
const setDbLiveNetworkMetrics = useThrottledCallback(
145+
(value?: LiveNetworkMetrics) => {
146+
setLiveNetworkMetrics(value);
147+
},
148+
liveNetworkMetricsDebounceMs,
149+
);
150+
151+
const setLiveTileMetrics = useSetAtom(liveTileMetricsAtom);
152+
const setDbLiveTileMetrics = useThrottledCallback((value?: TileMetrics) => {
153+
setLiveTileMetrics(value);
154+
}, liveTileMetricsDebounceMs);
155+
135156
const setLivePrimaryMetrics = useSetAtom(liveTilePrimaryMetricAtom);
136157
const setDbLivePrimaryMetrics = useThrottledCallback(
137158
(value?: LiveTilePrimaryMetric) => {
@@ -177,15 +198,15 @@ export function useSetAtomWsData() {
177198
(value?: GossipNetworkStats) => {
178199
setGossipNetworkStats(value);
179200
},
180-
300,
201+
gossipNetworkDebounceMs,
181202
);
182203

183204
const setGossipPeersSize = useSetAtom(gossipPeersSizeAtom);
184205
const setDbGossipPeersSize = useThrottledCallback(
185206
(value?: GossipPeersSize) => {
186207
setGossipPeersSize(value);
187208
},
188-
1_000,
209+
gossipPeerSizeDebounceMs,
189210
);
190211
const setGossipPeersRows = useSetAtom(gossipPeersRowsUpdateAtom);
191212
const setGossipPeersCells = useSetAtom(gossipPeersCellUpdateAtom);
@@ -383,6 +404,13 @@ export function useSetAtomWsData() {
383404
addRepairSlots(value.repair);
384405
break;
385406
}
407+
case "live_network_metrics": {
408+
setDbLiveNetworkMetrics(value);
409+
break;
410+
}
411+
case "live_tile_metrics":
412+
setDbLiveTileMetrics(value);
413+
break;
386414
case "root_slot":
387415
case "optimistically_confirmed_slot":
388416
case "estimated_slot":
@@ -392,8 +420,7 @@ export function useSetAtomWsData() {
392420
case "storage_slot":
393421
case "vote_slot":
394422
case "slot_caught_up":
395-
case "live_network_metrics":
396-
case "live_tile_metrics":
423+
case "active_fork_count":
397424
break;
398425
}
399426
} else if (topic === "epoch") {

src/clockUtils.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
type Sub = (now: number, dt: number) => void;
22

3-
export function clockSub(intervalMs: number) {
3+
export function clockSub(_intervalMs: number) {
44
const subs = new Set<Sub>();
55
let id: number | null = null;
66
let last = performance.now();
7+
let intervalMs = _intervalMs;
78

8-
function startChartClock() {
9+
function startChartClock(newIntervalMs?: number) {
910
if (id == null) {
1011
stopChartClock();
1112
}
13+
if (newIntervalMs !== undefined) {
14+
stopChartClock();
15+
intervalMs = newIntervalMs;
16+
}
1217

1318
const loop = () => {
1419
const now = performance.now();
@@ -37,5 +42,5 @@ export function clockSub(intervalMs: number) {
3742

3843
startChartClock();
3944

40-
return { subscribeClock, stopChartClock };
45+
return { subscribeClock, stopChartClock, startChartClock };
4146
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const networkProtocols = [
2+
"turbine",
3+
"gossip",
4+
"tpu",
5+
"repair",
6+
"metrics",
7+
];
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { useAtomValue } from "jotai";
2+
import { liveNetworkMetricsAtom } from "../../../api/atoms";
3+
import Card from "../../../components/Card";
4+
import { Flex, Table, Text } from "@radix-ui/themes";
5+
import tableStyles from "../../Gossip/table.module.css";
6+
import { useEmaValue } from "../../../hooks/useEma";
7+
import { networkProtocols } from "./consts";
8+
import { formatBytesAsBits } from "../../../utils";
9+
import { Bars } from "../../StartupProgress/Firedancer/Bars";
10+
import TileSparkLine from "../SlotPerformance/TileSparkLine";
11+
import { headerGap } from "../../Gossip/consts";
12+
import type { CSSProperties } from "react";
13+
import styles from "./liveNetworkMetrics.module.css";
14+
import { sum } from "lodash";
15+
16+
const chartHeight = 18;
17+
18+
export default function LiveNetworkMetrics() {
19+
const liveNetworkMetrics = useAtomValue(liveNetworkMetricsAtom);
20+
if (!liveNetworkMetrics) return;
21+
22+
return (
23+
<Flex wrap="wrap" gap="4">
24+
<NetworkMetricsCard metrics={liveNetworkMetrics.ingress} type="Ingress" />
25+
<NetworkMetricsCard metrics={liveNetworkMetrics.egress} type="Egress" />
26+
</Flex>
27+
);
28+
}
29+
30+
interface NetworkMetricsCardProps {
31+
metrics: number[];
32+
type: "Ingress" | "Egress";
33+
}
34+
35+
function NetworkMetricsCard({ metrics, type }: NetworkMetricsCardProps) {
36+
return (
37+
<Card style={{ flexGrow: 1 }}>
38+
<Flex direction="column" height="100%" gap={headerGap}>
39+
<Text className={tableStyles.headerText}>Network {type}</Text>
40+
<Table.Root
41+
variant="ghost"
42+
className={tableStyles.root}
43+
size="1"
44+
style={{ "--bar-height": `${chartHeight}px` } as CSSProperties}
45+
>
46+
<Table.Header>
47+
<Table.Row>
48+
<Table.ColumnHeaderCell width="60px">
49+
Protocol
50+
</Table.ColumnHeaderCell>
51+
<Table.ColumnHeaderCell align="right" width="80px">
52+
Current
53+
</Table.ColumnHeaderCell>
54+
<Table.ColumnHeaderCell
55+
minWidth={{
56+
xl: "250px",
57+
lg: "160px",
58+
md: "100px",
59+
initial: "60px",
60+
}}
61+
>
62+
Utilization
63+
</Table.ColumnHeaderCell>
64+
<Table.ColumnHeaderCell
65+
align="right"
66+
width={{
67+
xl: "240px",
68+
lg: "200px",
69+
md: "100px",
70+
initial: "200px",
71+
}}
72+
>
73+
History (1m)
74+
</Table.ColumnHeaderCell>
75+
</Table.Row>
76+
</Table.Header>
77+
78+
<Table.Body>
79+
{metrics.map((value, i) => (
80+
<TableRow key={i} value={value} idx={i} />
81+
))}
82+
<TableRow
83+
value={sum(metrics)}
84+
label="Total"
85+
className={styles.totalRow}
86+
/>
87+
</Table.Body>
88+
</Table.Root>
89+
</Flex>
90+
</Card>
91+
);
92+
}
93+
94+
const maxValue = 100_000_000;
95+
96+
interface TableRowProps {
97+
value: number;
98+
idx?: number;
99+
label?: string;
100+
}
101+
102+
function TableRow({
103+
value,
104+
idx,
105+
label,
106+
...props
107+
}: TableRowProps & Table.RootProps) {
108+
const emaValue = useEmaValue(value);
109+
const formattedValue = formatBytesAsBits(emaValue);
110+
111+
return (
112+
<Table.Row {...props}>
113+
<Table.RowHeaderCell>
114+
{label ?? networkProtocols[idx ?? -1]}
115+
</Table.RowHeaderCell>
116+
<Table.Cell align="right">
117+
{formattedValue.value} {formattedValue.unit}
118+
</Table.Cell>
119+
<Table.Cell className={styles.chart}>
120+
<Flex align="center">
121+
<Bars value={emaValue} max={maxValue} barWidth={2} />
122+
</Flex>
123+
</Table.Cell>
124+
<Table.Cell className={styles.chart}>
125+
<TileSparkLine
126+
value={Math.min(1, emaValue / maxValue)}
127+
includeBg={false}
128+
windowMs={60_000}
129+
height={chartHeight}
130+
updateIntervalMs={500}
131+
tickMs={1_000}
132+
/>
133+
</Table.Cell>
134+
</Table.Row>
135+
);
136+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.chart {
2+
padding-top: 0;
3+
padding-bottom: 0;
4+
vertical-align: middle;
5+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/** Tile regimes are the cartesian product of the following two state vectors:
2+
State vector 1:
3+
running: means that at the time the run loop executed, there was no upstream message I/O for the tile to handle.
4+
processing: means that at the time the run loop executed, there was one or more messages for the tile to consume.
5+
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.
6+
7+
State Vector 2:
8+
maintenance: the portion of the run loop that executes infrequent, potentially CPU heavy tasks
9+
routine: the portion of the run loop that executes regularly, regardless of the presence of incoming messages
10+
handling: the portion of the run loop that executes as a side effect of an incoming message from an upstream producer tile
11+
*/
12+
export const regimes = [
13+
"running_maintenance",
14+
"processing_maintenance",
15+
"stalled_maintenance",
16+
"running_routine",
17+
"processing_routine",
18+
"stalled_routine",
19+
"running_handling",
20+
"processing_handling",
21+
// "stalled_handling" is an impossible state, and is therefore excluded
22+
];

0 commit comments

Comments
 (0)