diff --git a/explorer/package-lock.json b/explorer/package-lock.json
index 1557d1a8a8..88065f1975 100644
--- a/explorer/package-lock.json
+++ b/explorer/package-lock.json
@@ -3892,11 +3892,6 @@
}
}
},
- "after": {
- "version": "0.8.2",
- "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
- "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
- },
"aggregate-error": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz",
@@ -4111,11 +4106,6 @@
"es-abstract": "^1.17.0-next.1"
}
},
- "arraybuffer.slice": {
- "version": "0.0.7",
- "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
- "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
- },
"arrify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
@@ -4901,11 +4891,6 @@
"resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
"integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ=="
},
- "backo2": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
- "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
- },
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@@ -4974,11 +4959,6 @@
"safe-buffer": "^5.0.1"
}
},
- "base64-arraybuffer": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
- "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI="
- },
"base64-js": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
@@ -5016,11 +4996,6 @@
"file-uri-to-path": "1.0.0"
}
},
- "blob": {
- "version": "0.0.5",
- "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
- "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
- },
"block-stream": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
@@ -5908,21 +5883,11 @@
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs="
},
- "component-bind": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
- "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E="
- },
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
- "component-inherit": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
- "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM="
- },
"compose-function": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/compose-function/-/compose-function-3.0.3.tgz",
@@ -6978,59 +6943,6 @@
"once": "^1.4.0"
}
},
- "engine.io-client": {
- "version": "3.4.4",
- "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.4.tgz",
- "integrity": "sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==",
- "requires": {
- "component-emitter": "~1.3.0",
- "component-inherit": "0.0.3",
- "debug": "~3.1.0",
- "engine.io-parser": "~2.2.0",
- "has-cors": "1.1.0",
- "indexof": "0.0.1",
- "parseqs": "0.0.6",
- "parseuri": "0.0.6",
- "ws": "~6.1.0",
- "xmlhttprequest-ssl": "~1.5.4",
- "yeast": "0.1.2"
- },
- "dependencies": {
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "requires": {
- "ms": "2.0.0"
- }
- },
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
- },
- "ws": {
- "version": "6.1.4",
- "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz",
- "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==",
- "requires": {
- "async-limiter": "~1.0.0"
- }
- }
- }
- },
- "engine.io-parser": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz",
- "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==",
- "requires": {
- "after": "0.8.2",
- "arraybuffer.slice": "~0.0.7",
- "base64-arraybuffer": "0.1.4",
- "blob": "0.0.5",
- "has-binary2": "~1.0.2"
- }
- },
"enhanced-resolve": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz",
@@ -8616,26 +8528,6 @@
}
}
},
- "has-binary2": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
- "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
- "requires": {
- "isarray": "2.0.1"
- },
- "dependencies": {
- "isarray": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
- "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
- }
- }
- },
- "has-cors": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
- "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
- },
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
@@ -9044,11 +8936,6 @@
"resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
"integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc="
},
- "indexof": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
- "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
- },
"infer-owner": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
@@ -11880,16 +11767,6 @@
"resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
"integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA=="
},
- "parseqs": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
- "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w=="
- },
- "parseuri": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
- "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow=="
- },
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -14977,69 +14854,6 @@
"kind-of": "^3.2.0"
}
},
- "socket.io-client": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.1.tgz",
- "integrity": "sha512-YXmXn3pA8abPOY//JtYxou95Ihvzmg8U6kQyolArkIyLd0pgVhrfor/iMsox8cn07WCOOvvuJ6XKegzIucPutQ==",
- "requires": {
- "backo2": "1.0.2",
- "component-bind": "1.0.0",
- "component-emitter": "~1.3.0",
- "debug": "~3.1.0",
- "engine.io-client": "~3.4.0",
- "has-binary2": "~1.0.2",
- "indexof": "0.0.1",
- "parseqs": "0.0.6",
- "parseuri": "0.0.6",
- "socket.io-parser": "~3.3.0",
- "to-array": "0.1.4"
- },
- "dependencies": {
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "requires": {
- "ms": "2.0.0"
- }
- },
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
- }
- }
- },
- "socket.io-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz",
- "integrity": "sha512-1QLvVAe8dTz+mKmZ07Swxt+LAo4Y1ff50rlyoEx00TQmDFVQYPfcqGvIDJLGaBdhdNCecXtyKpD+EgKGcmmbuQ==",
- "requires": {
- "component-emitter": "~1.3.0",
- "debug": "~3.1.0",
- "isarray": "2.0.1"
- },
- "dependencies": {
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "requires": {
- "ms": "2.0.0"
- }
- },
- "isarray": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
- "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
- },
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
- }
- }
- },
"sockjs": {
"version": "0.3.20",
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz",
@@ -15962,11 +15776,6 @@
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
"integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE="
},
- "to-array": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
- "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
- },
"to-arraybuffer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
@@ -17189,11 +16998,6 @@
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
},
- "xmlhttprequest-ssl": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
- "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4="
- },
"xregexp": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz",
@@ -17342,11 +17146,6 @@
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
}
}
- },
- "yeast": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
- "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
}
}
}
diff --git a/explorer/package.json b/explorer/package.json
index b50a51b033..3ddbc4dbc0 100644
--- a/explorer/package.json
+++ b/explorer/package.json
@@ -38,7 +38,6 @@
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.3",
"react-select": "^3.1.0",
- "socket.io-client": "^2.3.1",
"solana-sdk-wasm": "file:wasm/pkg",
"superstruct": "github:solana-labs/superstruct",
"typescript": "^3.9.7",
diff --git a/explorer/src/components/TpsCard.tsx b/explorer/src/components/TpsCard.tsx
index 9fdbbedd18..81a6cef318 100644
--- a/explorer/src/components/TpsCard.tsx
+++ b/explorer/src/components/TpsCard.tsx
@@ -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 (
-
-
- Stats are not available for this cluster
-
-
- );
- }
-
- if (!performanceInfo) {
- return (
-
-
- Loading
-
+
);
}
@@ -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",
},
};
diff --git a/explorer/src/pages/ClusterStatsPage.tsx b/explorer/src/pages/ClusterStatsPage.tsx
index 7cb8c701a0..21c7937d7d 100644
--- a/explorer/src/pages/ClusterStatsPage.tsx
+++ b/explorer/src/pages/ClusterStatsPage.tsx
@@ -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 (
@@ -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 (
-
-
- Stats are not available for this cluster
-
-
- );
+ if (
+ performanceInfo.status !== ClusterStatsStatus.Ready ||
+ dashboardInfo.status !== ClusterStatsStatus.Ready
+ ) {
+ const error =
+ performanceInfo.status === ClusterStatsStatus.Error ||
+ dashboardInfo.status === ClusterStatsStatus.Error;
+ return
;
}
- if (!dashboardInfo || !performanceInfo) {
- return (
-
-
- Loading
-
- );
- }
-
- 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() {
+ {blockHeight !== undefined && (
+
+ Block height |
+
+
+ |
+
+ )}
- Block height |
-
-
- |
-
-
- Block time |
- {averageBlockTime} |
+ Slot time |
+ {averageSlotTime} |
Epoch |
@@ -106,3 +99,44 @@ function StatsCardBody() {
);
}
+
+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 (
+
+ There was a problem loading cluster stats.{" "}
+
+
+ );
+ }
+
+ return (
+
+
+ Loading
+
+ );
+}
diff --git a/explorer/src/providers/stats/index.tsx b/explorer/src/providers/stats/index.tsx
index 60a482d5fc..56e20c9980 100644
--- a/explorer/src/providers/stats/index.tsx
+++ b/explorer/src/providers/stats/index.tsx
@@ -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 {children};
+ return {children};
}
diff --git a/explorer/src/providers/stats/solanaBeach.tsx b/explorer/src/providers/stats/solanaBeach.tsx
deleted file mode 100644
index d3893290a2..0000000000
--- a/explorer/src/providers/stats/solanaBeach.tsx
+++ /dev/null
@@ -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>;
-const SetActiveContext = React.createContext<
- { setActive: SetActive } | undefined
->(undefined);
-
-type RootInfo = StructType;
-type RootState = { slot: number | undefined };
-const RootContext = React.createContext(undefined);
-
-type DashboardInfo = StructType;
-type DashboardState = { info: DashboardInfo | undefined };
-const DashboardContext = React.createContext(
- 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(
- 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();
- const [dashboardInfo, setDashboardInfo] = React.useState();
- 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, 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 (
-
-
-
-
- {children}
-
-
-
-
- );
-}
-
-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;
-}
diff --git a/explorer/src/providers/stats/solanaClusterStats.tsx b/explorer/src/providers/stats/solanaClusterStats.tsx
new file mode 100644
index 0000000000..52bd9e7844
--- /dev/null
+++ b/explorer/src/providers/stats/solanaClusterStats.tsx
@@ -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>;
+const StatsProviderContext = React.createContext<
+ | {
+ setActive: SetActive;
+ setTimedOut: Function;
+ retry: Function;
+ active: boolean;
+ }
+ | undefined
+>(undefined);
+
+type DashboardState = { info: DashboardInfo };
+const DashboardContext = React.createContext(
+ undefined
+);
+
+type PerformanceState = { info: PerformanceInfo };
+const PerformanceContext = React.createContext(
+ 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 (
+
+
+
+ {children}
+
+
+
+ );
+}
+
+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;
+}
diff --git a/explorer/src/providers/stats/solanaDashboardInfo.tsx b/explorer/src/providers/stats/solanaDashboardInfo.tsx
new file mode 100644
index 0000000000..9509979909
--- /dev/null
+++ b/explorer/src/providers/stats/solanaDashboardInfo.tsx
@@ -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;
+ }
+}
diff --git a/explorer/src/providers/stats/solanaPerformanceInfo.tsx b/explorer/src/providers/stats/solanaPerformanceInfo.tsx
new file mode 100644
index 0000000000..a2d73111da
--- /dev/null
+++ b/explorer/src/providers/stats/solanaPerformanceInfo.tsx
@@ -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));
+}