Remove Solanabeach dependency from Explorer (#12463)
* remove solana beach socket dependency * remove socket.io dependency * timeout / retry button for cluster stats * update web3 version, add EpochInfo typing, handle no samples case * derive max TPS from final downsampled arrays * change block time to slot time
This commit is contained in:
@@ -4,12 +4,13 @@ import CountUp from "react-countup";
|
||||
import {
|
||||
usePerformanceInfo,
|
||||
PERF_UPDATE_SEC,
|
||||
PerformanceInfo,
|
||||
} from "providers/stats/solanaBeach";
|
||||
ClusterStatsStatus,
|
||||
} from "providers/stats/solanaClusterStats";
|
||||
import classNames from "classnames";
|
||||
import { TableCardBody } from "components/common/TableCardBody";
|
||||
import { useCluster, Cluster } from "providers/cluster";
|
||||
import { ChartOptions, ChartTooltipModel } from "chart.js";
|
||||
import { PerformanceInfo } from "providers/stats/solanaPerformanceInfo";
|
||||
import { StatsNotReady } from "pages/ClusterStatsPage";
|
||||
|
||||
export function TpsCard() {
|
||||
return (
|
||||
@@ -24,26 +25,12 @@ export function TpsCard() {
|
||||
|
||||
function TpsCardBody() {
|
||||
const performanceInfo = usePerformanceInfo();
|
||||
const { cluster } = useCluster();
|
||||
|
||||
const statsAvailable =
|
||||
cluster === Cluster.MainnetBeta || cluster === Cluster.Testnet;
|
||||
if (!statsAvailable) {
|
||||
if (performanceInfo.status !== ClusterStatsStatus.Ready) {
|
||||
return (
|
||||
<div className="card-body text-center">
|
||||
<div className="text-muted">
|
||||
Stats are not available for this cluster
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!performanceInfo) {
|
||||
return (
|
||||
<div className="card-body text-center">
|
||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||
Loading
|
||||
</div>
|
||||
<StatsNotReady
|
||||
error={performanceInfo.status === ClusterStatsStatus.Error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,15 +41,15 @@ type Series = "short" | "medium" | "long";
|
||||
const SERIES: Series[] = ["short", "medium", "long"];
|
||||
const SERIES_INFO = {
|
||||
short: {
|
||||
label: (index: number) => Math.floor(index / 4),
|
||||
label: (index: number) => index,
|
||||
interval: "30m",
|
||||
},
|
||||
medium: {
|
||||
label: (index: number) => index,
|
||||
label: (index: number) => index * 4,
|
||||
interval: "2h",
|
||||
},
|
||||
long: {
|
||||
label: (index: number) => 3 * index,
|
||||
label: (index: number) => index * 12,
|
||||
interval: "6h",
|
||||
},
|
||||
};
|
||||
|
@@ -1,16 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
import { TableCardBody } from "components/common/TableCardBody";
|
||||
import { Slot } from "components/common/Slot";
|
||||
import {
|
||||
ClusterStatsStatus,
|
||||
useDashboardInfo,
|
||||
usePerformanceInfo,
|
||||
useSetActive,
|
||||
} from "providers/stats/solanaBeach";
|
||||
useStatsProvider,
|
||||
} from "providers/stats/solanaClusterStats";
|
||||
import { slotsToHumanString } from "utils";
|
||||
import { useCluster, Cluster } from "providers/cluster";
|
||||
import { useCluster } from "providers/cluster";
|
||||
import { TpsCard } from "components/TpsCard";
|
||||
|
||||
const CLUSTER_STATS_TIMEOUT = 10000;
|
||||
|
||||
export function ClusterStatsPage() {
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
@@ -32,44 +34,33 @@ export function ClusterStatsPage() {
|
||||
function StatsCardBody() {
|
||||
const dashboardInfo = useDashboardInfo();
|
||||
const performanceInfo = usePerformanceInfo();
|
||||
const setSocketActive = useSetActive();
|
||||
const { setActive } = useStatsProvider();
|
||||
const { cluster } = useCluster();
|
||||
|
||||
React.useEffect(() => {
|
||||
setSocketActive(true);
|
||||
return () => setSocketActive(false);
|
||||
}, [setSocketActive, cluster]);
|
||||
setActive(true);
|
||||
return () => setActive(false);
|
||||
}, [setActive, cluster]);
|
||||
|
||||
const statsAvailable =
|
||||
cluster === Cluster.MainnetBeta || cluster === Cluster.Testnet;
|
||||
if (!statsAvailable) {
|
||||
return (
|
||||
<div className="card-body text-center">
|
||||
<div className="text-muted">
|
||||
Stats are not available for this cluster
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
if (
|
||||
performanceInfo.status !== ClusterStatsStatus.Ready ||
|
||||
dashboardInfo.status !== ClusterStatsStatus.Ready
|
||||
) {
|
||||
const error =
|
||||
performanceInfo.status === ClusterStatsStatus.Error ||
|
||||
dashboardInfo.status === ClusterStatsStatus.Error;
|
||||
return <StatsNotReady error={error} />;
|
||||
}
|
||||
|
||||
if (!dashboardInfo || !performanceInfo) {
|
||||
return (
|
||||
<div className="card-body text-center">
|
||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||
Loading
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { avgBlockTime_1h, avgBlockTime_1min, epochInfo } = dashboardInfo;
|
||||
const hourlyBlockTime = Math.round(1000 * avgBlockTime_1h);
|
||||
const averageBlockTime = Math.round(1000 * avgBlockTime_1min) + "ms";
|
||||
const { avgSlotTime_1h, avgSlotTime_1min, epochInfo } = dashboardInfo;
|
||||
const hourlySlotTime = Math.round(1000 * avgSlotTime_1h);
|
||||
const averageSlotTime = Math.round(1000 * avgSlotTime_1min) + "ms";
|
||||
const { slotIndex, slotsInEpoch } = epochInfo;
|
||||
const currentEpoch = epochInfo.epoch.toString();
|
||||
const epochProgress = ((100 * slotIndex) / slotsInEpoch).toFixed(1) + "%";
|
||||
const epochTimeRemaining = slotsToHumanString(
|
||||
slotsInEpoch - slotIndex,
|
||||
hourlyBlockTime
|
||||
hourlySlotTime
|
||||
);
|
||||
const { blockHeight, absoluteSlot } = epochInfo;
|
||||
|
||||
@@ -81,15 +72,17 @@ function StatsCardBody() {
|
||||
<Slot slot={absoluteSlot} />
|
||||
</td>
|
||||
</tr>
|
||||
{blockHeight !== undefined && (
|
||||
<tr>
|
||||
<td className="w-100">Block height</td>
|
||||
<td className="text-lg-right text-monospace">
|
||||
<Slot slot={blockHeight} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td className="w-100">Block height</td>
|
||||
<td className="text-lg-right text-monospace">
|
||||
<Slot slot={blockHeight} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="w-100">Block time</td>
|
||||
<td className="text-lg-right text-monospace">{averageBlockTime}</td>
|
||||
<td className="w-100">Slot time</td>
|
||||
<td className="text-lg-right text-monospace">{averageSlotTime}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="w-100">Epoch</td>
|
||||
@@ -106,3 +99,44 @@ function StatsCardBody() {
|
||||
</TableCardBody>
|
||||
);
|
||||
}
|
||||
|
||||
export function StatsNotReady({ error }: { error: boolean }) {
|
||||
const { setTimedOut, retry, active } = useStatsProvider();
|
||||
const { cluster } = useCluster();
|
||||
|
||||
React.useEffect(() => {
|
||||
let timedOut = 0;
|
||||
if (!error) {
|
||||
timedOut = setTimeout(setTimedOut, CLUSTER_STATS_TIMEOUT);
|
||||
}
|
||||
return () => {
|
||||
if (timedOut) {
|
||||
clearTimeout(timedOut);
|
||||
}
|
||||
};
|
||||
}, [setTimedOut, cluster, error]);
|
||||
|
||||
if (error || !active) {
|
||||
return (
|
||||
<div className="card-body text-center">
|
||||
There was a problem loading cluster stats.{" "}
|
||||
<button
|
||||
className="btn btn-white btn-sm"
|
||||
onClick={() => {
|
||||
retry();
|
||||
}}
|
||||
>
|
||||
<span className="fe fe-refresh-cw mr-2"></span>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card-body text-center">
|
||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||
Loading
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { SolanaBeachProvider } from "./solanaBeach";
|
||||
import { SolanaClusterStatsProvider } from "./solanaClusterStats";
|
||||
|
||||
type Props = { children: React.ReactNode };
|
||||
export function StatsProvider({ children }: Props) {
|
||||
return <SolanaBeachProvider>{children}</SolanaBeachProvider>;
|
||||
return <SolanaClusterStatsProvider>{children}</SolanaClusterStatsProvider>;
|
||||
}
|
||||
|
@@ -1,202 +0,0 @@
|
||||
import React from "react";
|
||||
import io from "socket.io-client";
|
||||
|
||||
import { pick, array, nullable, number, is, StructType } from "superstruct";
|
||||
import { useCluster, Cluster } from "providers/cluster";
|
||||
|
||||
const DashboardInfo = pick({
|
||||
avgBlockTime_1h: number(),
|
||||
avgBlockTime_1min: number(),
|
||||
epochInfo: pick({
|
||||
absoluteSlot: number(),
|
||||
blockHeight: number(),
|
||||
epoch: number(),
|
||||
slotIndex: number(),
|
||||
slotsInEpoch: number(),
|
||||
}),
|
||||
});
|
||||
|
||||
const RootInfo = pick({
|
||||
root: number(),
|
||||
});
|
||||
|
||||
export const PERF_UPDATE_SEC = 5;
|
||||
|
||||
const PerformanceInfo = pick({
|
||||
avgTPS: number(),
|
||||
perfHistory: pick({
|
||||
s: array(nullable(number())),
|
||||
m: array(nullable(number())),
|
||||
l: array(nullable(number())),
|
||||
}),
|
||||
totalTransactionCount: number(),
|
||||
});
|
||||
|
||||
type SetActive = React.Dispatch<React.SetStateAction<boolean>>;
|
||||
const SetActiveContext = React.createContext<
|
||||
{ setActive: SetActive } | undefined
|
||||
>(undefined);
|
||||
|
||||
type RootInfo = StructType<typeof RootInfo>;
|
||||
type RootState = { slot: number | undefined };
|
||||
const RootContext = React.createContext<RootState | undefined>(undefined);
|
||||
|
||||
type DashboardInfo = StructType<typeof DashboardInfo>;
|
||||
type DashboardState = { info: DashboardInfo | undefined };
|
||||
const DashboardContext = React.createContext<DashboardState | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
export type PerformanceInfo = {
|
||||
avgTps: number;
|
||||
historyMaxTps: number;
|
||||
perfHistory: {
|
||||
short: (number | null)[];
|
||||
medium: (number | null)[];
|
||||
long: (number | null)[];
|
||||
};
|
||||
transactionCount: number;
|
||||
};
|
||||
|
||||
type PerformanceState = { info: PerformanceInfo | undefined };
|
||||
const PerformanceContext = React.createContext<PerformanceState | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const MAINNET_URL = "https://api.solanabeach.io:8443/mainnet";
|
||||
const TESTNET_URL = "https://api.solanabeach.io:8443/tds";
|
||||
|
||||
type Props = { children: React.ReactNode };
|
||||
export function SolanaBeachProvider({ children }: Props) {
|
||||
const { cluster } = useCluster();
|
||||
const [active, setActive] = React.useState(false);
|
||||
const [root, setRoot] = React.useState<number>();
|
||||
const [dashboardInfo, setDashboardInfo] = React.useState<DashboardInfo>();
|
||||
const [performanceInfo, setPerformanceInfo] = React.useState<
|
||||
PerformanceInfo
|
||||
>();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!active) return;
|
||||
|
||||
let socket: SocketIOClient.Socket;
|
||||
if (cluster === Cluster.MainnetBeta) {
|
||||
socket = io(MAINNET_URL);
|
||||
} else if (cluster === Cluster.Testnet) {
|
||||
socket = io(TESTNET_URL);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.on("connect", () => {
|
||||
socket.emit("request_dashboardInfo");
|
||||
socket.emit("request_performanceInfo");
|
||||
});
|
||||
socket.on("error", (err: any) => {
|
||||
console.error(err);
|
||||
});
|
||||
socket.on("dashboardInfo", (data: any) => {
|
||||
if (is(data, DashboardInfo)) {
|
||||
setDashboardInfo(data);
|
||||
}
|
||||
});
|
||||
socket.on("performanceInfo", (data: any) => {
|
||||
if (is(data, PerformanceInfo)) {
|
||||
const trimSeries = (series: (number | null)[]) => {
|
||||
return series.slice(series.length - 51, series.length - 1);
|
||||
};
|
||||
|
||||
const seriesMax = (series: (number | null)[]) => {
|
||||
return series.reduce((max: number, next) => {
|
||||
if (next === null) return max;
|
||||
return Math.max(max, next);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const normalize = (series: Array<number | null>, seconds: number) => {
|
||||
return series.map((next) => {
|
||||
if (next === null) return next;
|
||||
return Math.round(next / seconds);
|
||||
});
|
||||
};
|
||||
|
||||
const short = normalize(trimSeries(data.perfHistory.s), 15);
|
||||
const medium = normalize(trimSeries(data.perfHistory.m), 60);
|
||||
const long = normalize(trimSeries(data.perfHistory.l), 180);
|
||||
const historyMaxTps = Math.max(
|
||||
seriesMax(short),
|
||||
seriesMax(medium),
|
||||
seriesMax(long)
|
||||
);
|
||||
|
||||
setPerformanceInfo({
|
||||
avgTps: data.avgTPS,
|
||||
historyMaxTps,
|
||||
perfHistory: { short, medium, long },
|
||||
transactionCount: data.totalTransactionCount,
|
||||
});
|
||||
}
|
||||
});
|
||||
socket.on("rootNotification", (data: any) => {
|
||||
if (is(data, RootInfo)) {
|
||||
setRoot(data.root);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [active, cluster]);
|
||||
|
||||
// Reset info whenever the cluster changes
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
setDashboardInfo(undefined);
|
||||
setPerformanceInfo(undefined);
|
||||
setRoot(undefined);
|
||||
};
|
||||
}, [cluster]);
|
||||
|
||||
return (
|
||||
<SetActiveContext.Provider value={{ setActive }}>
|
||||
<DashboardContext.Provider value={{ info: dashboardInfo }}>
|
||||
<PerformanceContext.Provider value={{ info: performanceInfo }}>
|
||||
<RootContext.Provider value={{ slot: root }}>
|
||||
{children}
|
||||
</RootContext.Provider>
|
||||
</PerformanceContext.Provider>
|
||||
</DashboardContext.Provider>
|
||||
</SetActiveContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSetActive() {
|
||||
const context = React.useContext(SetActiveContext);
|
||||
if (!context) {
|
||||
throw new Error(`useSetActive must be used within a StatsProvider`);
|
||||
}
|
||||
return context.setActive;
|
||||
}
|
||||
|
||||
export function useDashboardInfo() {
|
||||
const context = React.useContext(DashboardContext);
|
||||
if (!context) {
|
||||
throw new Error(`useDashboardInfo must be used within a StatsProvider`);
|
||||
}
|
||||
return context.info;
|
||||
}
|
||||
|
||||
export function usePerformanceInfo() {
|
||||
const context = React.useContext(PerformanceContext);
|
||||
if (!context) {
|
||||
throw new Error(`usePerformanceInfo must be used within a StatsProvider`);
|
||||
}
|
||||
return context.info;
|
||||
}
|
||||
|
||||
export function useRootSlot() {
|
||||
const context = React.useContext(RootContext);
|
||||
if (!context) {
|
||||
throw new Error(`useRootSlot must be used within a StatsProvider`);
|
||||
}
|
||||
return context.slot;
|
||||
}
|
261
explorer/src/providers/stats/solanaClusterStats.tsx
Normal file
261
explorer/src/providers/stats/solanaClusterStats.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
import React from "react";
|
||||
import { Connection } from "@solana/web3.js";
|
||||
import { useCluster, Cluster } from "providers/cluster";
|
||||
import {
|
||||
DashboardInfo,
|
||||
DashboardInfoActionType,
|
||||
dashboardInfoReducer,
|
||||
} from "./solanaDashboardInfo";
|
||||
import {
|
||||
PerformanceInfo,
|
||||
PerformanceInfoActionType,
|
||||
performanceInfoReducer,
|
||||
} from "./solanaPerformanceInfo";
|
||||
import { reportError } from "utils/sentry";
|
||||
|
||||
export const PERF_UPDATE_SEC = 5;
|
||||
export const SAMPLE_HISTORY_HOURS = 6;
|
||||
export const PERFORMANCE_SAMPLE_INTERVAL = 60000;
|
||||
export const TRANSACTION_COUNT_INTERVAL = 5000;
|
||||
export const EPOCH_INFO_INTERVAL = 2000;
|
||||
export const LOADING_TIMEOUT = 10000;
|
||||
|
||||
export enum ClusterStatsStatus {
|
||||
Loading,
|
||||
Ready,
|
||||
Error,
|
||||
}
|
||||
|
||||
const initialPerformanceInfo: PerformanceInfo = {
|
||||
status: ClusterStatsStatus.Loading,
|
||||
avgTps: 0,
|
||||
historyMaxTps: 0,
|
||||
perfHistory: {
|
||||
short: [],
|
||||
medium: [],
|
||||
long: [],
|
||||
},
|
||||
transactionCount: 0,
|
||||
};
|
||||
|
||||
const initialDashboardInfo: DashboardInfo = {
|
||||
status: ClusterStatsStatus.Loading,
|
||||
avgSlotTime_1h: 0,
|
||||
avgSlotTime_1min: 0,
|
||||
epochInfo: {
|
||||
absoluteSlot: 0,
|
||||
blockHeight: 0,
|
||||
epoch: 0,
|
||||
slotIndex: 0,
|
||||
slotsInEpoch: 0,
|
||||
},
|
||||
};
|
||||
|
||||
type SetActive = React.Dispatch<React.SetStateAction<boolean>>;
|
||||
const StatsProviderContext = React.createContext<
|
||||
| {
|
||||
setActive: SetActive;
|
||||
setTimedOut: Function;
|
||||
retry: Function;
|
||||
active: boolean;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
type DashboardState = { info: DashboardInfo };
|
||||
const DashboardContext = React.createContext<DashboardState | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
type PerformanceState = { info: PerformanceInfo };
|
||||
const PerformanceContext = React.createContext<PerformanceState | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
type Props = { children: React.ReactNode };
|
||||
export function SolanaClusterStatsProvider({ children }: Props) {
|
||||
const { cluster, url } = useCluster();
|
||||
const [active, setActive] = React.useState(false);
|
||||
const [dashboardInfo, dispatchDashboardInfo] = React.useReducer(
|
||||
dashboardInfoReducer,
|
||||
initialDashboardInfo
|
||||
);
|
||||
const [performanceInfo, dispatchPerformanceInfo] = React.useReducer(
|
||||
performanceInfoReducer,
|
||||
initialPerformanceInfo
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!active || !url) return;
|
||||
|
||||
const connection = new Connection(url);
|
||||
|
||||
const getPerformanceSamples = async () => {
|
||||
try {
|
||||
const samples = await connection.getRecentPerformanceSamples(
|
||||
60 * SAMPLE_HISTORY_HOURS
|
||||
);
|
||||
|
||||
if (samples.length < 1) {
|
||||
// no samples to work with (node has no history).
|
||||
return; // we will allow for a timeout instead of throwing an error
|
||||
}
|
||||
|
||||
dispatchPerformanceInfo({
|
||||
type: PerformanceInfoActionType.SetPerfSamples,
|
||||
data: samples,
|
||||
});
|
||||
|
||||
dispatchDashboardInfo({
|
||||
type: DashboardInfoActionType.SetPerfSamples,
|
||||
data: samples,
|
||||
});
|
||||
} catch (error) {
|
||||
if (cluster !== Cluster.Custom) {
|
||||
reportError(error, { url });
|
||||
}
|
||||
dispatchPerformanceInfo({
|
||||
type: PerformanceInfoActionType.SetError,
|
||||
data: error.toString(),
|
||||
});
|
||||
dispatchDashboardInfo({
|
||||
type: DashboardInfoActionType.SetError,
|
||||
data: error.toString(),
|
||||
});
|
||||
setActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getTransactionCount = async () => {
|
||||
try {
|
||||
const transactionCount = await connection.getTransactionCount();
|
||||
dispatchPerformanceInfo({
|
||||
type: PerformanceInfoActionType.SetTransactionCount,
|
||||
data: transactionCount,
|
||||
});
|
||||
} catch (error) {
|
||||
if (cluster !== Cluster.Custom) {
|
||||
reportError(error, { url });
|
||||
}
|
||||
dispatchPerformanceInfo({
|
||||
type: PerformanceInfoActionType.SetError,
|
||||
data: error.toString(),
|
||||
});
|
||||
setActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getEpochInfo = async () => {
|
||||
try {
|
||||
const epochInfo = await connection.getEpochInfo();
|
||||
dispatchDashboardInfo({
|
||||
type: DashboardInfoActionType.SetEpochInfo,
|
||||
data: epochInfo,
|
||||
});
|
||||
} catch (error) {
|
||||
if (cluster !== Cluster.Custom) {
|
||||
reportError(error, { url });
|
||||
}
|
||||
dispatchDashboardInfo({
|
||||
type: DashboardInfoActionType.SetError,
|
||||
data: error.toString(),
|
||||
});
|
||||
setActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
const performanceInterval = setInterval(
|
||||
getPerformanceSamples,
|
||||
PERFORMANCE_SAMPLE_INTERVAL
|
||||
);
|
||||
const transactionCountInterval = setInterval(
|
||||
getTransactionCount,
|
||||
TRANSACTION_COUNT_INTERVAL
|
||||
);
|
||||
const epochInfoInterval = setInterval(getEpochInfo, EPOCH_INFO_INTERVAL);
|
||||
|
||||
getPerformanceSamples();
|
||||
getTransactionCount();
|
||||
getEpochInfo();
|
||||
|
||||
return () => {
|
||||
clearInterval(performanceInterval);
|
||||
clearInterval(transactionCountInterval);
|
||||
clearInterval(epochInfoInterval);
|
||||
};
|
||||
}, [active, cluster, url]);
|
||||
|
||||
// Reset when cluster changes
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
resetData();
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
function resetData() {
|
||||
dispatchDashboardInfo({
|
||||
type: DashboardInfoActionType.Reset,
|
||||
data: initialDashboardInfo,
|
||||
});
|
||||
dispatchPerformanceInfo({
|
||||
type: PerformanceInfoActionType.Reset,
|
||||
data: initialPerformanceInfo,
|
||||
});
|
||||
}
|
||||
|
||||
const setTimedOut = React.useCallback(() => {
|
||||
dispatchDashboardInfo({
|
||||
type: DashboardInfoActionType.SetError,
|
||||
data: "Cluster stats timed out",
|
||||
});
|
||||
dispatchPerformanceInfo({
|
||||
type: PerformanceInfoActionType.SetError,
|
||||
data: "Cluster stats timed out",
|
||||
});
|
||||
if (cluster !== Cluster.Custom) {
|
||||
reportError(new Error("Cluster stats timed out"), { url });
|
||||
}
|
||||
setActive(false);
|
||||
}, [cluster, url]);
|
||||
|
||||
const retry = React.useCallback(() => {
|
||||
resetData();
|
||||
setActive(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StatsProviderContext.Provider
|
||||
value={{ setActive, setTimedOut, retry, active }}
|
||||
>
|
||||
<DashboardContext.Provider value={{ info: dashboardInfo }}>
|
||||
<PerformanceContext.Provider value={{ info: performanceInfo }}>
|
||||
{children}
|
||||
</PerformanceContext.Provider>
|
||||
</DashboardContext.Provider>
|
||||
</StatsProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useStatsProvider() {
|
||||
const context = React.useContext(StatsProviderContext);
|
||||
if (!context) {
|
||||
throw new Error(`useContext must be used within a StatsProvider`);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useDashboardInfo() {
|
||||
const context = React.useContext(DashboardContext);
|
||||
if (!context) {
|
||||
throw new Error(`useDashboardInfo must be used within a StatsProvider`);
|
||||
}
|
||||
return context.info;
|
||||
}
|
||||
|
||||
export function usePerformanceInfo() {
|
||||
const context = React.useContext(PerformanceContext);
|
||||
if (!context) {
|
||||
throw new Error(`usePerformanceInfo must be used within a StatsProvider`);
|
||||
}
|
||||
return context.info;
|
||||
}
|
95
explorer/src/providers/stats/solanaDashboardInfo.tsx
Normal file
95
explorer/src/providers/stats/solanaDashboardInfo.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { EpochInfo, PerfSample } from "@solana/web3.js";
|
||||
import { ClusterStatsStatus } from "./solanaClusterStats";
|
||||
|
||||
export type DashboardInfo = {
|
||||
status: ClusterStatsStatus;
|
||||
avgSlotTime_1h: number;
|
||||
avgSlotTime_1min: number;
|
||||
epochInfo: EpochInfo;
|
||||
};
|
||||
|
||||
export enum DashboardInfoActionType {
|
||||
SetPerfSamples,
|
||||
SetEpochInfo,
|
||||
SetError,
|
||||
Reset,
|
||||
}
|
||||
|
||||
export type DashboardInfoActionSetPerfSamples = {
|
||||
type: DashboardInfoActionType.SetPerfSamples;
|
||||
data: PerfSample[];
|
||||
};
|
||||
|
||||
export type DashboardInfoActionSetEpochInfo = {
|
||||
type: DashboardInfoActionType.SetEpochInfo;
|
||||
data: EpochInfo;
|
||||
};
|
||||
|
||||
export type DashboardInfoActionReset = {
|
||||
type: DashboardInfoActionType.Reset;
|
||||
data: DashboardInfo;
|
||||
};
|
||||
|
||||
export type DashboardInfoActionSetError = {
|
||||
type: DashboardInfoActionType.SetError;
|
||||
data: string;
|
||||
};
|
||||
|
||||
export type DashboardInfoAction =
|
||||
| DashboardInfoActionSetPerfSamples
|
||||
| DashboardInfoActionSetEpochInfo
|
||||
| DashboardInfoActionReset
|
||||
| DashboardInfoActionSetError;
|
||||
|
||||
export function dashboardInfoReducer(
|
||||
state: DashboardInfo,
|
||||
action: DashboardInfoAction
|
||||
) {
|
||||
const status =
|
||||
state.avgSlotTime_1h !== 0 && state.epochInfo.absoluteSlot !== 0
|
||||
? ClusterStatsStatus.Ready
|
||||
: ClusterStatsStatus.Loading;
|
||||
|
||||
switch (action.type) {
|
||||
case DashboardInfoActionType.SetPerfSamples:
|
||||
if (action.data.length < 1) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const samples = action.data
|
||||
.map((sample) => {
|
||||
return sample.samplePeriodSecs / sample.numSlots;
|
||||
})
|
||||
.slice(0, 60);
|
||||
|
||||
const samplesInHour = samples.length < 60 ? samples.length : 60;
|
||||
const avgSlotTime_1h =
|
||||
samples.reduce((sum: number, cur: number) => {
|
||||
return sum + cur;
|
||||
}, 0) / samplesInHour;
|
||||
|
||||
return {
|
||||
...state,
|
||||
avgSlotTime_1h,
|
||||
avgSlotTime_1min: samples[0],
|
||||
status,
|
||||
};
|
||||
case DashboardInfoActionType.SetEpochInfo:
|
||||
return {
|
||||
...state,
|
||||
epochInfo: action.data,
|
||||
status,
|
||||
};
|
||||
case DashboardInfoActionType.SetError:
|
||||
return {
|
||||
...state,
|
||||
status: ClusterStatsStatus.Error,
|
||||
};
|
||||
case DashboardInfoActionType.Reset:
|
||||
return {
|
||||
...action.data,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
126
explorer/src/providers/stats/solanaPerformanceInfo.tsx
Normal file
126
explorer/src/providers/stats/solanaPerformanceInfo.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { PerfSample } from "@solana/web3.js";
|
||||
import { ClusterStatsStatus } from "./solanaClusterStats";
|
||||
|
||||
export type PerformanceInfo = {
|
||||
status: ClusterStatsStatus;
|
||||
avgTps: number;
|
||||
historyMaxTps: number;
|
||||
perfHistory: {
|
||||
short: (number | null)[];
|
||||
medium: (number | null)[];
|
||||
long: (number | null)[];
|
||||
};
|
||||
transactionCount: number;
|
||||
};
|
||||
|
||||
export enum PerformanceInfoActionType {
|
||||
SetTransactionCount,
|
||||
SetPerfSamples,
|
||||
SetError,
|
||||
Reset,
|
||||
}
|
||||
|
||||
export type PerformanceInfoActionSetTransactionCount = {
|
||||
type: PerformanceInfoActionType.SetTransactionCount;
|
||||
data: number;
|
||||
};
|
||||
|
||||
export type PerformanceInfoActionSetPerfSamples = {
|
||||
type: PerformanceInfoActionType.SetPerfSamples;
|
||||
data: PerfSample[];
|
||||
};
|
||||
|
||||
export type PerformanceInfoActionSetError = {
|
||||
type: PerformanceInfoActionType.SetError;
|
||||
data: string;
|
||||
};
|
||||
|
||||
export type PerformanceInfoActionReset = {
|
||||
type: PerformanceInfoActionType.Reset;
|
||||
data: PerformanceInfo;
|
||||
};
|
||||
|
||||
export type PerformanceInfoAction =
|
||||
| PerformanceInfoActionSetTransactionCount
|
||||
| PerformanceInfoActionSetPerfSamples
|
||||
| PerformanceInfoActionSetError
|
||||
| PerformanceInfoActionReset;
|
||||
|
||||
export function performanceInfoReducer(
|
||||
state: PerformanceInfo,
|
||||
action: PerformanceInfoAction
|
||||
) {
|
||||
const status =
|
||||
state.avgTps !== 0 && state.transactionCount !== 0
|
||||
? ClusterStatsStatus.Ready
|
||||
: ClusterStatsStatus.Loading;
|
||||
|
||||
switch (action.type) {
|
||||
case PerformanceInfoActionType.SetPerfSamples:
|
||||
if (action.data.length < 1) {
|
||||
return state;
|
||||
}
|
||||
|
||||
let short = action.data.map((sample) => {
|
||||
return sample.numTransactions / sample.samplePeriodSecs;
|
||||
});
|
||||
|
||||
const avgTps = short[0];
|
||||
const medium = downsampleByFactor(short, 4);
|
||||
const long = downsampleByFactor(medium, 3);
|
||||
|
||||
const perfHistory = {
|
||||
short: round(short.slice(0, 30)).reverse(),
|
||||
medium: round(medium.slice(0, 30)).reverse(),
|
||||
long: round(long.slice(0, 30)).reverse(),
|
||||
};
|
||||
|
||||
const historyMaxTps = Math.max(
|
||||
Math.max(...perfHistory.short),
|
||||
Math.max(...perfHistory.medium),
|
||||
Math.max(...perfHistory.long)
|
||||
);
|
||||
|
||||
return {
|
||||
...state,
|
||||
historyMaxTps,
|
||||
avgTps,
|
||||
perfHistory,
|
||||
status,
|
||||
};
|
||||
case PerformanceInfoActionType.SetTransactionCount:
|
||||
return {
|
||||
...state,
|
||||
transactionCount: action.data,
|
||||
status,
|
||||
};
|
||||
case PerformanceInfoActionType.SetError:
|
||||
return {
|
||||
...state,
|
||||
status: ClusterStatsStatus.Error,
|
||||
};
|
||||
case PerformanceInfoActionType.Reset:
|
||||
return {
|
||||
...action.data,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function downsampleByFactor(series: number[], factor: number) {
|
||||
return series.reduce((result: number[], num: number, i: number) => {
|
||||
const downsampledIndex = Math.floor(i / factor);
|
||||
if (result.length < downsampledIndex + 1) {
|
||||
result.push(0);
|
||||
}
|
||||
const mean = result[downsampledIndex];
|
||||
const differential = (num - mean) / ((i % factor) + 1);
|
||||
result[downsampledIndex] = mean + differential;
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function round(series: number[]) {
|
||||
return series.map((n) => Math.round(n));
|
||||
}
|
Reference in New Issue
Block a user