Skip to content

tidy slot progression chart #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 25, 2025
Merged
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
3 changes: 2 additions & 1 deletion src/features/LeaderSchedule/Slots/SlotCardGrid.tsx
Original file line number Diff line number Diff line change
@@ -219,7 +219,8 @@ function getRowValues(publish: SlotPublish): RowValues {
const computeUnits = fixValue(publish?.compute_units ?? 0);
const computeUnitsPct =
publish.compute_units != null
? (publish.compute_units / 48_000_000) * 100
? (publish.compute_units / (publish.max_compute_units ?? 48_000_000)) *
100
: 0;

return {
124 changes: 110 additions & 14 deletions src/features/Overview/SlotPerformance/ComputeUnitsCard/Chart.tsx
Original file line number Diff line number Diff line change
@@ -51,6 +51,8 @@ interface ChartData {
timestampNanos: number;
computeUnits: number;
activeBankCount: number;
priority_fees_lamports: number;
tips_lamports: number;
}

const minRangeNanos = 50_000;
@@ -60,9 +62,9 @@ const wheelScrollSpeed = 1 / 3_000;
const smNanosThreshold = 5_000_000;
const mdNanosThreshold = 50_000_000;
const _segmentColors = [
{ fill: "#1E9C50", opacity: 0 },
{ fill: "#1E9C50", opacity: 0.15 },
{ fill: "#AE5511", opacity: 0.15 },
{ fill: "#CF321D", opacity: 0.15 },
{ fill: "#F40505", opacity: 0.15 },
{ fill: "#F40505", opacity: 0.2 },
];
@@ -71,9 +73,7 @@ const getSegmentColor = (index: number) => {
};
const cuAxisId = "computeUnits";
const bankCountAxisId = "activeBankCount";
const defaultCuTicks = [
8_000_000, 16_000_000, 24_000_000, 32_000_000, 40_000_000, 48_000_000,
];
const incomeAxisId = "income";

const cusPerNs = 1 / 8;
const tickLabelWidth = 110;
@@ -103,6 +103,18 @@ function getChartData(computeUnits: ComputeUnits): ChartData[] {
: -computeUnits.txn_max_compute_units[txn_idx] +
computeUnits.txn_compute_units_consumed[txn_idx]
: 0;
const priority_fee =
!event.start &&
computeUnits.txn_landed[txn_idx] &&
computeUnits.txn_error_code[txn_idx] === 0
? Number(computeUnits.txn_priority_fee[txn_idx])
: 0;
const tip =
!event.start &&
computeUnits.txn_landed[txn_idx] &&
computeUnits.txn_error_code[txn_idx] === 0
? Number(computeUnits.txn_tips[txn_idx])
: 0;

const prev = chartData[chartData.length - 1];
activeBanks[computeUnits.txn_bank_idx[txn_idx]] = event.start;
@@ -113,18 +125,30 @@ function getChartData(computeUnits: ComputeUnits): ChartData[] {
if (i > 0 && events[i - 1].timestampNanos === event.timestampNanos) {
prev.computeUnits += cus_delta;
prev.activeBankCount = activeBankCount;
prev.priority_fees_lamports += priority_fee;
prev.tips_lamports += tip;
} else {
chartData.push({
timestampNanos: Number(
event.timestampNanos - computeUnits.start_timestamp_nanos,
),
computeUnits: prev.computeUnits + cus_delta,
activeBankCount,
priority_fees_lamports: prev.priority_fees_lamports + priority_fee,
tips_lamports: prev.tips_lamports + tip,
});
}
return chartData;
},
[{ timestampNanos: 0, computeUnits: 0, activeBankCount: 0 }],
[
{
timestampNanos: 0,
computeUnits: 0,
activeBankCount: 0,
priority_fees_lamports: 0,
tips_lamports: 0,
},
],
);
}

@@ -136,9 +160,10 @@ const getXTicks = memoize(function getXTicks(
return prettyIntervals(tsMinNanos, tsMaxNanos, intervalCount);
});

function getDataDomain(
function getDataRangebyZoomWindow(
data: ChartData[],
maxComputeUnits: number,
field: keyof ChartData,
maxRangeValue: number,
zoomRange: ZoomRange | undefined,
): Domain | undefined {
if (!data.length) return;
@@ -161,8 +186,8 @@ function getDataDomain(
// if (pt.timestampNanos > endTime) break;

if (pt !== undefined) {
min = Math.min(min, pt.computeUnits);
max = Math.max(max, pt.computeUnits);
min = Math.min(min, pt[field]);
max = Math.max(max, pt[field]);
}

// Doing the check after the min/max means we included an additional data point past
@@ -173,7 +198,22 @@ function getDataDomain(
if (!Number.isFinite(min) || !Number.isFinite(max)) return;

const domain = extendDomain([min, max], 100);
return [Math.max(0, domain[0]), Math.min(maxComputeUnits, domain[1])];
return [Math.max(0, domain[0]), Math.min(maxRangeValue, domain[1])];
}

function getDefaultYAxisTicks(
numTicks: number,
maxRangeValue: number,
approxBottomPadding: number,
interval: number,
): Array<number> {
const step =
Math.ceil(
(maxRangeValue - approxBottomPadding) / (numTicks - 1) / interval,
) * interval;
return Array.from({ length: numTicks }, (_, i) =>
Math.min(maxRangeValue - i * step, maxRangeValue),
);
}

function getCuByTs({
@@ -375,9 +415,31 @@ export default function Chart({
[xDomain, xLabelCount],
);

const defaultCuTicks = useMemo(
() => getDefaultYAxisTicks(6, maxComputeUnits, 8_000_000, 1_000_000),
[maxComputeUnits],
);

const maxIncomeValue = Math.max(
data.reduce((sum, d) => sum + d.tips_lamports, 0),
data.reduce((sum, d) => sum + d.priority_fees_lamports, 0),
12_000_000,
);
const defaultIncomeTicks = useMemo(
() => getDefaultYAxisTicks(6, maxIncomeValue, 2_000_000, 1_000_000),
[maxIncomeValue],
);

const cuDomain = useMemo(
() =>
fitYToData ? getDataDomain(data, maxComputeUnits, zoomRange) : undefined,
fitYToData
? getDataRangebyZoomWindow(
data,
"computeUnits",
maxComputeUnits,
zoomRange,
)
: undefined,
[maxComputeUnits, data, fitYToData, zoomRange],
);

@@ -766,7 +828,7 @@ export default function Chart({
yAxisId={bankCountAxisId}
type="stepAfter"
dataKey="activeBankCount"
stroke="#BA7B1D"
stroke="rgba(117, 77, 18, 1)"
strokeWidth={
useActiveBanksLargeStroke
? 0.9
@@ -919,12 +981,32 @@ export default function Chart({
yAxisId={cuAxisId}
type="stepAfter"
dataKey="computeUnits"
stroke="#1288F6"
stroke="rgba(105, 105, 255, 1)"
strokeWidth={1.3}
dot={false}
name="CUs"
isAnimationActive={false}
/>
<Line
yAxisId={incomeAxisId}
type="stepAfter"
dataKey="priority_fees_lamports"
stroke="rgba(82, 227, 203, 1)"
strokeWidth={1.3}
dot={false}
name="Income"
isAnimationActive={false}
/>
<Line
yAxisId={incomeAxisId}
type="stepAfter"
dataKey="tips_lamports"
stroke="rgba(84, 211, 94, 1)"
strokeWidth={1.3}
dot={false}
name="Income"
isAnimationActive={false}
/>
<XAxis
dataKey="timestampNanos"
scale="time"
@@ -945,7 +1027,7 @@ export default function Chart({
ticks={cuDomain ? undefined : defaultCuTicks}
allowDataOverflow={!!cuDomain}
minTickGap={0}
orientation="right"
orientation="left"
tickFormatter={(tick) => {
if (typeof tick !== "number") return "";

@@ -961,6 +1043,20 @@ export default function Chart({
hide
name="active bank tiles"
/>
<YAxis
yAxisId={incomeAxisId}
scale="linear"
type="number"
domain={["auto", "dataMax + 250000"]}
ticks={defaultIncomeTicks}
orientation="right"
tickFormatter={(tick) => {
if (typeof tick !== "number") return "";
return (
(tick / 1_000_000_000).toFixed(3).padEnd(3, "0") + " ꜱᴏʟ"
);
}}
/>

{isActiveDragging && (
<ReferenceArea
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ import { TooltipProps } from "recharts";
import styles from "./computeUnits.module.css";
import { Text } from "@radix-ui/themes";
import clsx from "clsx";
import { getFmtStake } from "../../../../utils";

export default function ChartTooltip(props: TooltipProps<number, string>) {
if (!props.active) return;
@@ -26,6 +27,14 @@ export default function ChartTooltip(props: TooltipProps<number, string>) {
<Text className={styles.elapsedTime}>
{(Number(props.label) / 1_000_000).toString()} ms
</Text>
<Text className={clsx(styles.tips, styles.label)}>Tips</Text>
<Text className={styles.tips}>
{getFmtStake(BigInt(props.payload?.[3]?.value || 0))}
</Text>
<Text className={clsx(styles.prioFee, styles.label)}>Prio Fee</Text>
<Text className={styles.prioFee}>
{getFmtStake(BigInt(props.payload?.[2]?.value || 0))}
</Text>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -15,17 +15,25 @@
}

.active-banks {
color: #ba7b1d;
color: rgba(117, 77, 18, 1);
}

.compute-units {
color: #1d77cb;
color: rgba(105, 105, 255, 1);
}

.elapsed-time {
color: #6a6a6e;
}

.prio-fee {
color: rgba(82, 227, 203, 1);
}

.tips {
color: rgba(84, 211, 94, 1);
}

.label {
font-weight: 600;
text-align: left;
@@ -39,12 +47,12 @@
max-height: 600px;
position: relative;
margin-left: -8px;
margin-right: -16px;
margin-right: -8px;

.legend {
position: absolute;
top: 30px;
left: 20px;
left: 80px;
display: flex;
flex-direction: column;
border-radius: 5px;
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useAtomValue } from "jotai";
import { useSlotQueryResponse } from "../../../../hooks/useSlotQuery";
import { useSlotQueryResponseTransactions } from "../../../../hooks/useSlotQuery";
import { selectedSlotAtom, tileCountAtom } from "../atoms";
import Card from "../../../../components/Card";
import CardHeader from "../../../../components/CardHeader";
@@ -11,7 +11,7 @@ import Actions from "./Actions";

export default function ComputeUnitsCard() {
const slot = useAtomValue(selectedSlotAtom);
const query = useSlotQueryResponse(slot);
const query = useSlotQueryResponseTransactions(slot);

const tileCount = useAtomValue(tileCountAtom);
const bankTileCount = tileCount["bank"];
@@ -22,7 +22,7 @@ export default function ComputeUnitsCard() {
<Card style={{ marginTop: "8px" }}>
<Flex direction="column" height="100%" gap="2">
<Flex gap="3">
<CardHeader text="CU Progression" />
<CardHeader text="Slot Progression" />
<Actions />
</Flex>
<div className={styles.chart}>
4 changes: 2 additions & 2 deletions src/features/Overview/SlotPerformance/SankeyControls.tsx
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import { Text, Tooltip } from "@radix-ui/themes";
import * as ToggleGroup from "@radix-ui/react-toggle-group";
import { useAtom, useAtomValue } from "jotai";
import { DisplayType, sankeyDisplayTypeAtom, selectedSlotAtom } from "./atoms";
import { useSlotQueryResponse } from "../../../hooks/useSlotQuery";
import { useSlotQueryResponseDetailed } from "../../../hooks/useSlotQuery";
import { fixValue } from "../../../utils";
import { useMemo } from "react";
import { lamportsPerSol } from "../../../consts";
@@ -52,7 +52,7 @@ export default function SankeyControls() {

function SlotStats() {
const selectedSlot = useAtomValue(selectedSlotAtom);
const query = useSlotQueryResponse(selectedSlot);
const query = useSlotQueryResponseDetailed(selectedSlot);

const values = useMemo(() => {
if (!query.response?.publish) return;
4 changes: 2 additions & 2 deletions src/features/Overview/SlotPerformance/SlotSankey/index.tsx
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import { TxnWaterfall, TxnWaterfallOut } from "../../../../api/types";
import { Flex, Spinner, Text } from "@radix-ui/themes";
import { SlotNode, slotNodes } from "./consts";
import { sum } from "lodash";
import { useSlotQueryResponse } from "../../../../hooks/useSlotQuery";
import { useSlotQueryResponseDetailed } from "../../../../hooks/useSlotQuery";

function getGetValue({
displayType,
@@ -342,7 +342,7 @@ function SlotSankey({ slot }: { slot?: number }) {
const displayType = useAtomValue(sankeyDisplayTypeAtom);
const liveWaterfall = useAtomValue(liveWaterfallAtom);

const query = useSlotQueryResponse(slot);
const query = useSlotQueryResponseDetailed(slot);

const data = useMemo(() => {
const waterfall = liveWaterfall ?? query.response?.waterfall;
4 changes: 2 additions & 2 deletions src/features/Overview/SlotPerformance/TilePrimaryStat.tsx
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import { Text } from "@radix-ui/themes";
import { TilePrimaryMetric } from "../../../api/types";
import { selectedSlotAtom } from "./atoms";
import byteSize from "byte-size";
import { useSlotQueryResponse } from "../../../hooks/useSlotQuery";
import { useSlotQueryResponseDetailed } from "../../../hooks/useSlotQuery";

interface TilePrimaryStatProps {
type: keyof TilePrimaryMetric;
@@ -16,7 +16,7 @@ export default function TilePrimaryStat({ type, label }: TilePrimaryStatProps) {
const slot = useAtomValue(selectedSlotAtom);
const showLive = !slot;
const primaryMetric = useAtomValue(liveTilePrimaryMetricAtom);
const query = useSlotQueryResponse(showLive ? undefined : slot);
const query = useSlotQueryResponseDetailed(showLive ? undefined : slot);

const stat = showLive
? primaryMetric?.tile_primary_metric?.[type]
Loading