diff --git a/src/features/LeaderSchedule/Slots/SlotCardGrid.tsx b/src/features/LeaderSchedule/Slots/SlotCardGrid.tsx index 7773024..42cc505 100644 --- a/src/features/LeaderSchedule/Slots/SlotCardGrid.tsx +++ b/src/features/LeaderSchedule/Slots/SlotCardGrid.tsx @@ -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 { diff --git a/src/features/Overview/SlotPerformance/ComputeUnitsCard/Chart.tsx b/src/features/Overview/SlotPerformance/ComputeUnitsCard/Chart.tsx index a1d6f2d..038ec12 100644 --- a/src/features/Overview/SlotPerformance/ComputeUnitsCard/Chart.tsx +++ b/src/features/Overview/SlotPerformance/ComputeUnitsCard/Chart.tsx @@ -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,6 +125,8 @@ 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( @@ -120,11 +134,21 @@ function getChartData(computeUnits: ComputeUnits): ChartData[] { ), 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 { + 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} /> + + { if (typeof tick !== "number") return ""; @@ -961,6 +1043,20 @@ export default function Chart({ hide name="active bank tiles" /> + { + if (typeof tick !== "number") return ""; + return ( + (tick / 1_000_000_000).toFixed(3).padEnd(3, "0") + " ꜱᴏʟ" + ); + }} + /> {isActiveDragging && ( ) { if (!props.active) return; @@ -26,6 +27,14 @@ export default function ChartTooltip(props: TooltipProps) { {(Number(props.label) / 1_000_000).toString()} ms + Tips + + {getFmtStake(BigInt(props.payload?.[3]?.value || 0))} + + Prio Fee + + {getFmtStake(BigInt(props.payload?.[2]?.value || 0))} + ); } diff --git a/src/features/Overview/SlotPerformance/ComputeUnitsCard/computeUnits.module.css b/src/features/Overview/SlotPerformance/ComputeUnitsCard/computeUnits.module.css index 2f8d419..24513ca 100644 --- a/src/features/Overview/SlotPerformance/ComputeUnitsCard/computeUnits.module.css +++ b/src/features/Overview/SlotPerformance/ComputeUnitsCard/computeUnits.module.css @@ -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; diff --git a/src/features/Overview/SlotPerformance/ComputeUnitsCard/index.tsx b/src/features/Overview/SlotPerformance/ComputeUnitsCard/index.tsx index b213715..40eba74 100644 --- a/src/features/Overview/SlotPerformance/ComputeUnitsCard/index.tsx +++ b/src/features/Overview/SlotPerformance/ComputeUnitsCard/index.tsx @@ -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() { - +
diff --git a/src/features/Overview/SlotPerformance/SankeyControls.tsx b/src/features/Overview/SlotPerformance/SankeyControls.tsx index f7a9c1e..dcd5a69 100644 --- a/src/features/Overview/SlotPerformance/SankeyControls.tsx +++ b/src/features/Overview/SlotPerformance/SankeyControls.tsx @@ -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; diff --git a/src/features/Overview/SlotPerformance/SlotSankey/index.tsx b/src/features/Overview/SlotPerformance/SlotSankey/index.tsx index 1566189..5ed02a8 100644 --- a/src/features/Overview/SlotPerformance/SlotSankey/index.tsx +++ b/src/features/Overview/SlotPerformance/SlotSankey/index.tsx @@ -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; diff --git a/src/features/Overview/SlotPerformance/TilePrimaryStat.tsx b/src/features/Overview/SlotPerformance/TilePrimaryStat.tsx index 1dbfa70..76658dd 100644 --- a/src/features/Overview/SlotPerformance/TilePrimaryStat.tsx +++ b/src/features/Overview/SlotPerformance/TilePrimaryStat.tsx @@ -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] diff --git a/src/features/Overview/SlotPerformance/TilesPerformance.tsx b/src/features/Overview/SlotPerformance/TilesPerformance.tsx index 7622657..028e7bc 100644 --- a/src/features/Overview/SlotPerformance/TilesPerformance.tsx +++ b/src/features/Overview/SlotPerformance/TilesPerformance.tsx @@ -10,7 +10,7 @@ import { import { useMemo } from "react"; import { TileType } from "../../../api/types"; import { tileTypeSchema } from "../../../api/entities"; -import { useSlotQueryResponse } from "../../../hooks/useSlotQuery"; +import { useSlotQueryResponseDetailed } from "../../../hooks/useSlotQuery"; export default function TilesPerformance() { const liveTileTimers = useAtomValue(liveTileTimerfallAtom); @@ -39,7 +39,7 @@ export default function TilesPerformance() { {} as Record, ); - const query = useSlotQueryResponse(slot); + const query = useSlotQueryResponseDetailed(slot); const queryIdleData = useMemo(() => { if (!query.response?.tile_timers?.length || showLive || !tiles) return; diff --git a/src/hooks/useSlotQuery.ts b/src/hooks/useSlotQuery.ts index 7a9138a..2a58029 100644 --- a/src/hooks/useSlotQuery.ts +++ b/src/hooks/useSlotQuery.ts @@ -11,18 +11,51 @@ import memoize from "micro-memoize"; import { SendMessage } from "../api/ws/types"; import { throttle } from "lodash"; +enum SlotQueryType { + Publish = "publish", + Detailed = "detailed", + Transactions = "transactions", +} + const getSendQuery = memoize( - (wsSend: SendMessage, slot: number, isDetailed: boolean) => { + (wsSend: SendMessage, slot: number, query_type: SlotQueryType) => { return throttle( () => { - wsSend({ - topic: "slot", - key: isDetailed ? "query_detailed" : "query", - id: 1, - params: { - slot: slot, - }, - }); + switch (query_type) { + case SlotQueryType.Publish: { + wsSend({ + topic: "slot", + key: "query", + id: 1, + params: { + slot: slot, + }, + }); + break; + } + case SlotQueryType.Detailed: { + wsSend({ + topic: "slot", + key: "query_detailed", + id: 2, + params: { + slot: slot, + }, + }); + break; + } + case SlotQueryType.Transactions: { + wsSend({ + topic: "slot", + key: "query_transactions", + id: 3, + params: { + slot: slot, + }, + }); + break; + } + } }, 5_000, { trailing: false }, @@ -33,7 +66,7 @@ const getSendQuery = memoize( function useSlotQuery( slot: number | undefined, - isDetailed: boolean, + query_type: SlotQueryType, skipQuery: boolean, ) { const wsSend = useWebSocketSend(); @@ -47,9 +80,9 @@ function useSlotQuery( if (isFutureSlot) return; if (skipQuery) return; - const sendQuery = getSendQuery(wsSend, slot, isDetailed); + const sendQuery = getSendQuery(wsSend, slot, query_type); sendQuery(); - }, [isDetailed, isFutureSlot, slot, skipQuery, wsSend]); + }, [query_type, isFutureSlot, slot, skipQuery, wsSend]); useEffect(query, [query]); @@ -68,18 +101,44 @@ export function useSlotQueryPublish(slot?: number) { const skipQuery = !!publish; - const { hasWaitedForData } = useSlotQuery(slot, false, skipQuery); + const { hasWaitedForData } = useSlotQuery( + slot, + SlotQueryType.Publish, + skipQuery, + ); return { publish, hasWaitedForData }; } -export function useSlotQueryResponse(slot?: number) { +export function useSlotQueryResponseDetailed(slot?: number) { + const response = useAtomValue( + useMemo(() => getSlotResponseAtom(slot), [slot]), + ); + const skipQuery = + !!response?.waterfall && + !!response?.tile_timers && + !!response?.tile_primary_metric; + + const { hasWaitedForData } = useSlotQuery( + slot, + SlotQueryType.Detailed, + skipQuery, + ); + + return { response, hasWaitedForData }; +} + +export function useSlotQueryResponseTransactions(slot?: number) { const response = useAtomValue( useMemo(() => getSlotResponseAtom(slot), [slot]), ); const skipQuery = !!response?.transactions; - const { hasWaitedForData } = useSlotQuery(slot, true, skipQuery); + const { hasWaitedForData } = useSlotQuery( + slot, + SlotQueryType.Transactions, + skipQuery, + ); return { response, hasWaitedForData }; }