Add live transaction stats card with history chart (#11813)
This commit is contained in:
65
explorer/package-lock.json
generated
65
explorer/package-lock.json
generated
@@ -3181,6 +3181,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.12.tgz",
|
||||||
"integrity": "sha512-aN5IAC8QNtSUdQzxu7lGBgYAOuU1tmRU4c9dIq5OKGf/SBVjXo+ffM2wEjudAWbgpOhy60nLoAGH1xm8fpCKFQ=="
|
"integrity": "sha512-aN5IAC8QNtSUdQzxu7lGBgYAOuU1tmRU4c9dIq5OKGf/SBVjXo+ffM2wEjudAWbgpOhy60nLoAGH1xm8fpCKFQ=="
|
||||||
},
|
},
|
||||||
|
"@types/chart.js": {
|
||||||
|
"version": "2.9.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.23.tgz",
|
||||||
|
"integrity": "sha512-4QQNE/b+digosu3mnj4E7aNQGKnlpzXa9JvQYPtexpO7v9gnDeqwc1DxF8vLJWLDCNoO6hH0EgO8K/7PtJl8wg==",
|
||||||
|
"requires": {
|
||||||
|
"moment": "^2.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/classnames": {
|
||||||
|
"version": "2.2.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.10.tgz",
|
||||||
|
"integrity": "sha512-1UzDldn9GfYYEsWWnn/P4wkTlkZDH7lDb0wBMGbtIQc9zXEQq7FlKBdZUn6OBqD8sKZZ2RQO2mAjGpXiDGoRmQ=="
|
||||||
|
},
|
||||||
"@types/color-name": {
|
"@types/color-name": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
||||||
@@ -5447,6 +5460,32 @@
|
|||||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
|
||||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
||||||
},
|
},
|
||||||
|
"chart.js": {
|
||||||
|
"version": "2.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.3.tgz",
|
||||||
|
"integrity": "sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==",
|
||||||
|
"requires": {
|
||||||
|
"chartjs-color": "^2.1.0",
|
||||||
|
"moment": "^2.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chartjs-color": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==",
|
||||||
|
"requires": {
|
||||||
|
"chartjs-color-string": "^0.6.0",
|
||||||
|
"color-convert": "^1.9.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chartjs-color-string": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==",
|
||||||
|
"requires": {
|
||||||
|
"color-name": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"check-error": {
|
"check-error": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
|
||||||
@@ -5629,6 +5668,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"classnames": {
|
||||||
|
"version": "2.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
|
||||||
|
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
|
||||||
|
},
|
||||||
"clean-css": {
|
"clean-css": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
|
||||||
@@ -10886,6 +10930,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"moment": {
|
||||||
|
"version": "2.27.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz",
|
||||||
|
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ=="
|
||||||
|
},
|
||||||
"move-concurrently": {
|
"move-concurrently": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||||
@@ -13180,6 +13229,22 @@
|
|||||||
"semver": "^5.6.0"
|
"semver": "^5.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-chartjs-2": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-1MjWEkUn8LLFf6GVyYUOrruJTW3yVU5hlEJOwGj3MiokuC+jH/BahjWVGAMonbe9UYbEIUbd2Rn36iVlC0Hb7w==",
|
||||||
|
"requires": {
|
||||||
|
"lodash": "^4.17.19",
|
||||||
|
"prop-types": "^15.7.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": {
|
||||||
|
"version": "4.17.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||||
|
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-countup": {
|
"react-countup": {
|
||||||
"version": "4.3.3",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-countup/-/react-countup-4.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-countup/-/react-countup-4.3.3.tgz",
|
||||||
|
@@ -12,6 +12,8 @@
|
|||||||
"@types/bn.js": "^4.11.6",
|
"@types/bn.js": "^4.11.6",
|
||||||
"@types/bs58": "^4.0.1",
|
"@types/bs58": "^4.0.1",
|
||||||
"@types/chai": "^4.2.12",
|
"@types/chai": "^4.2.12",
|
||||||
|
"@types/chart.js": "^2.9.23",
|
||||||
|
"@types/classnames": "^2.2.10",
|
||||||
"@types/jest": "^26.0.10",
|
"@types/jest": "^26.0.10",
|
||||||
"@types/node": "^14.6.0",
|
"@types/node": "^14.6.0",
|
||||||
"@types/react": "^16.9.46",
|
"@types/react": "^16.9.46",
|
||||||
@@ -23,11 +25,14 @@
|
|||||||
"bootstrap": "^4.5.2",
|
"bootstrap": "^4.5.2",
|
||||||
"bs58": "^4.0.1",
|
"bs58": "^4.0.1",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
|
"chart.js": "^2.9.3",
|
||||||
|
"classnames": "2.2.6",
|
||||||
"humanize-duration-ts": "^2.1.1",
|
"humanize-duration-ts": "^2.1.1",
|
||||||
"node-sass": "^4.14.1",
|
"node-sass": "^4.14.1",
|
||||||
"prettier": "^2.0.5",
|
"prettier": "^2.0.5",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-app-rewired": "^2.1.6",
|
"react-app-rewired": "^2.1.6",
|
||||||
|
"react-chartjs-2": "^2.10.0",
|
||||||
"react-countup": "^4.3.3",
|
"react-countup": "^4.3.3",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
|
271
explorer/src/components/TpsCard.tsx
Normal file
271
explorer/src/components/TpsCard.tsx
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Bar } from "react-chartjs-2";
|
||||||
|
import CountUp from "react-countup";
|
||||||
|
import {
|
||||||
|
usePerformanceInfo,
|
||||||
|
PERF_UPDATE_SEC,
|
||||||
|
PerformanceInfo,
|
||||||
|
} from "providers/stats/solanaBeach";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { TableCardBody } from "components/common/TableCardBody";
|
||||||
|
import { useCluster, Cluster } from "providers/cluster";
|
||||||
|
import { ChartOptions, ChartTooltipModel } from "chart.js";
|
||||||
|
|
||||||
|
export function TpsCard() {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h4 className="card-header-title">Live Transaction Stats</h4>
|
||||||
|
</div>
|
||||||
|
<TpsCardBody />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TpsCardBody() {
|
||||||
|
const performanceInfo = usePerformanceInfo();
|
||||||
|
const { cluster } = useCluster();
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return (
|
||||||
|
<div className="card-body text-center">
|
||||||
|
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||||
|
Loading
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <TpsBarChart performanceInfo={performanceInfo} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Series = "short" | "medium" | "long";
|
||||||
|
const SERIES: Series[] = ["short", "medium", "long"];
|
||||||
|
const SERIES_INFO = {
|
||||||
|
short: {
|
||||||
|
label: (index: number) => Math.floor(index / 4),
|
||||||
|
interval: "30m",
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
label: (index: number) => index,
|
||||||
|
interval: "2h",
|
||||||
|
},
|
||||||
|
long: {
|
||||||
|
label: (index: number) => 3 * index,
|
||||||
|
interval: "6h",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const CUSTOM_TOOLTIP = function (this: any, tooltipModel: ChartTooltipModel) {
|
||||||
|
// Tooltip Element
|
||||||
|
let tooltipEl = document.getElementById("chartjs-tooltip");
|
||||||
|
|
||||||
|
// Create element on first render
|
||||||
|
if (!tooltipEl) {
|
||||||
|
tooltipEl = document.createElement("div");
|
||||||
|
tooltipEl.id = "chartjs-tooltip";
|
||||||
|
tooltipEl.innerHTML = `<div class="content"></div>`;
|
||||||
|
document.body.appendChild(tooltipEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide if no tooltip
|
||||||
|
if (tooltipModel.opacity === 0) {
|
||||||
|
tooltipEl.style.opacity = "0";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Text
|
||||||
|
if (tooltipModel.body) {
|
||||||
|
const { label, value } = tooltipModel.dataPoints[0];
|
||||||
|
const tooltipContent = tooltipEl.querySelector("div");
|
||||||
|
if (tooltipContent) {
|
||||||
|
let innerHtml = `<div class="value">${value} TPS</div>`;
|
||||||
|
innerHtml += `<div class="label">${label}</div>`;
|
||||||
|
tooltipContent.innerHTML = innerHtml;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable tooltip and set position
|
||||||
|
const canvas: Element = this._chart.canvas;
|
||||||
|
const position = canvas.getBoundingClientRect();
|
||||||
|
tooltipEl.style.opacity = "1";
|
||||||
|
tooltipEl.style.left =
|
||||||
|
position.left + window.pageXOffset + tooltipModel.caretX + "px";
|
||||||
|
tooltipEl.style.top =
|
||||||
|
position.top + window.pageYOffset + tooltipModel.caretY + "px";
|
||||||
|
};
|
||||||
|
|
||||||
|
const CHART_OPTIONS = (historyMaxTps: number): ChartOptions => {
|
||||||
|
return {
|
||||||
|
tooltips: {
|
||||||
|
intersect: false, // Show tooltip when cursor in between bars
|
||||||
|
enabled: false, // Hide default tooltip
|
||||||
|
custom: CUSTOM_TOOLTIP,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
stepSize: 100,
|
||||||
|
fontSize: 10,
|
||||||
|
fontColor: "#EEE",
|
||||||
|
beginAtZero: true,
|
||||||
|
display: true,
|
||||||
|
suggestedMax: historyMaxTps,
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
duration: 0, // general animation time
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
animationDuration: 0, // duration of animations when hovering an item
|
||||||
|
},
|
||||||
|
responsiveAnimationDuration: 0, // animation duration after a resize
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type TpsBarChartProps = { performanceInfo: PerformanceInfo };
|
||||||
|
function TpsBarChart({ performanceInfo }: TpsBarChartProps) {
|
||||||
|
const { perfHistory, avgTps, historyMaxTps } = performanceInfo;
|
||||||
|
const [series, setSeries] = React.useState<Series>("short");
|
||||||
|
const averageTps = Math.round(avgTps).toLocaleString("en-US");
|
||||||
|
const transactionCount = <AnimatedTransactionCount info={performanceInfo} />;
|
||||||
|
const seriesData = perfHistory[series];
|
||||||
|
const chartOptions = React.useMemo(() => CHART_OPTIONS(historyMaxTps), [
|
||||||
|
historyMaxTps,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const seriesLength = seriesData.length;
|
||||||
|
const chartData: Chart.ChartData = {
|
||||||
|
labels: seriesData.map((val, i) => {
|
||||||
|
return `${SERIES_INFO[series].label(seriesLength - i)}min ago`;
|
||||||
|
}),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
backgroundColor: "#00D192",
|
||||||
|
hoverBackgroundColor: "#00D192",
|
||||||
|
borderWidth: 0,
|
||||||
|
data: seriesData.map((val) => val || 0),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TableCardBody>
|
||||||
|
<tr>
|
||||||
|
<td className="w-100">Transaction count</td>
|
||||||
|
<td className="text-lg-right text-monospace">{transactionCount} </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="w-100">Transactions per second (TPS)</td>
|
||||||
|
<td className="text-lg-right text-monospace">{averageTps} </td>
|
||||||
|
</tr>
|
||||||
|
</TableCardBody>
|
||||||
|
|
||||||
|
<hr className="my-0" />
|
||||||
|
|
||||||
|
<div className="card-body py-3">
|
||||||
|
<div className="align-box-row align-items-start justify-content-between">
|
||||||
|
<div className="d-flex justify-content-between w-100">
|
||||||
|
<span className="mb-0 font-size-sm">TPS history</span>
|
||||||
|
|
||||||
|
<div className="font-size-sm">
|
||||||
|
{SERIES.map((key) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
onClick={() => setSeries(key)}
|
||||||
|
className={classNames("btn btn-sm btn-white ml-2", {
|
||||||
|
active: series === key,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{SERIES_INFO[key].interval}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="perf-history"
|
||||||
|
className="mt-3 d-flex justify-content-end flex-row w-100"
|
||||||
|
>
|
||||||
|
<div className="w-100">
|
||||||
|
<Bar data={chartData} options={chartOptions} height={80} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnimatedTransactionCount({ info }: { info: PerformanceInfo }) {
|
||||||
|
const txCountRef = React.useRef(0);
|
||||||
|
const countUpRef = React.useRef({ start: 0, period: 0, lastUpdate: 0 });
|
||||||
|
const countUp = countUpRef.current;
|
||||||
|
|
||||||
|
const { transactionCount: txCount, avgTps } = info;
|
||||||
|
|
||||||
|
// Track last tx count to reset count up options
|
||||||
|
if (txCount !== txCountRef.current) {
|
||||||
|
if (countUp.lastUpdate > 0) {
|
||||||
|
// Since we overshoot below, calculate the elapsed value
|
||||||
|
// and start from there.
|
||||||
|
const elapsed = Date.now() - countUp.lastUpdate;
|
||||||
|
const elapsedPeriods = elapsed / (PERF_UPDATE_SEC * 1000);
|
||||||
|
countUp.start = countUp.start + elapsedPeriods * countUp.period;
|
||||||
|
countUp.period = txCount - countUp.start;
|
||||||
|
} else {
|
||||||
|
// Since this is the first tx count value, estimate the previous
|
||||||
|
// tx count in order to have a starting point for our animation
|
||||||
|
countUp.period = PERF_UPDATE_SEC * avgTps;
|
||||||
|
countUp.start = txCount - countUp.period;
|
||||||
|
}
|
||||||
|
countUp.lastUpdate = Date.now();
|
||||||
|
txCountRef.current = txCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overshoot the target tx count in case the next update is delayed
|
||||||
|
const COUNT_PERIODS = 3;
|
||||||
|
const countUpEnd = countUp.start + COUNT_PERIODS * countUp.period;
|
||||||
|
return (
|
||||||
|
<CountUp
|
||||||
|
start={countUp.start}
|
||||||
|
end={countUpEnd}
|
||||||
|
duration={PERF_UPDATE_SEC * COUNT_PERIODS}
|
||||||
|
delay={0}
|
||||||
|
useEasing={false}
|
||||||
|
preserveValue={true}
|
||||||
|
separator=","
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,16 +1,14 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import CountUp from "react-countup";
|
|
||||||
|
|
||||||
import { TableCardBody } from "components/common/TableCardBody";
|
import { TableCardBody } from "components/common/TableCardBody";
|
||||||
import {
|
import {
|
||||||
useDashboardInfo,
|
useDashboardInfo,
|
||||||
usePerformanceInfo,
|
usePerformanceInfo,
|
||||||
PERF_UPDATE_SEC,
|
|
||||||
useSetActive,
|
useSetActive,
|
||||||
PerformanceInfo,
|
|
||||||
} from "providers/stats/solanaBeach";
|
} from "providers/stats/solanaBeach";
|
||||||
import { slotsToHumanString } from "utils";
|
import { slotsToHumanString } from "utils";
|
||||||
import { useCluster, Cluster } from "providers/cluster";
|
import { useCluster, Cluster } from "providers/cluster";
|
||||||
|
import { TpsCard } from "components/TpsCard";
|
||||||
|
|
||||||
export function ClusterStatsPage() {
|
export function ClusterStatsPage() {
|
||||||
return (
|
return (
|
||||||
@@ -25,6 +23,7 @@ export function ClusterStatsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<StatsCardBody />
|
<StatsCardBody />
|
||||||
</div>
|
</div>
|
||||||
|
<TpsCard />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -71,8 +70,6 @@ function StatsCardBody() {
|
|||||||
slotsInEpoch - slotIndex,
|
slotsInEpoch - slotIndex,
|
||||||
hourlyBlockTime
|
hourlyBlockTime
|
||||||
);
|
);
|
||||||
const averageTps = Math.round(performanceInfo.avgTPS);
|
|
||||||
const transactionCount = <AnimatedTransactionCount info={performanceInfo} />;
|
|
||||||
const blockHeight = epochInfo.blockHeight.toLocaleString("en-US");
|
const blockHeight = epochInfo.blockHeight.toLocaleString("en-US");
|
||||||
const currentSlot = epochInfo.absoluteSlot.toLocaleString("en-US");
|
const currentSlot = epochInfo.absoluteSlot.toLocaleString("en-US");
|
||||||
|
|
||||||
@@ -102,56 +99,6 @@ function StatsCardBody() {
|
|||||||
<td className="w-100">Epoch time remaining</td>
|
<td className="w-100">Epoch time remaining</td>
|
||||||
<td className="text-lg-right text-monospace">{epochTimeRemaining} </td>
|
<td className="text-lg-right text-monospace">{epochTimeRemaining} </td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td className="w-100">Transaction count</td>
|
|
||||||
<td className="text-lg-right text-monospace">{transactionCount} </td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="w-100">Transactions per second</td>
|
|
||||||
<td className="text-lg-right text-monospace">{averageTps} </td>
|
|
||||||
</tr>
|
|
||||||
</TableCardBody>
|
</TableCardBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AnimatedTransactionCount({ info }: { info: PerformanceInfo }) {
|
|
||||||
const txCountRef = React.useRef(0);
|
|
||||||
const countUpRef = React.useRef({ start: 0, period: 0, lastUpdate: 0 });
|
|
||||||
const countUp = countUpRef.current;
|
|
||||||
|
|
||||||
const { totalTransactionCount: txCount, avgTPS } = info;
|
|
||||||
|
|
||||||
// Track last tx count to reset count up options
|
|
||||||
if (txCount !== txCountRef.current) {
|
|
||||||
if (countUp.lastUpdate > 0) {
|
|
||||||
// Since we overshoot below, calculate the elapsed value
|
|
||||||
// and start from there.
|
|
||||||
const elapsed = Date.now() - countUp.lastUpdate;
|
|
||||||
const elapsedPeriods = elapsed / (PERF_UPDATE_SEC * 1000);
|
|
||||||
countUp.start = countUp.start + elapsedPeriods * countUp.period;
|
|
||||||
countUp.period = txCount - countUp.start;
|
|
||||||
} else {
|
|
||||||
// Since this is the first tx count value, estimate the previous
|
|
||||||
// tx count in order to have a starting point for our animation
|
|
||||||
countUp.period = PERF_UPDATE_SEC * avgTPS;
|
|
||||||
countUp.start = txCount - countUp.period;
|
|
||||||
}
|
|
||||||
countUp.lastUpdate = Date.now();
|
|
||||||
txCountRef.current = txCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overshoot the target tx count in case the next update is delayed
|
|
||||||
const COUNT_PERIODS = 3;
|
|
||||||
const countUpEnd = countUp.start + COUNT_PERIODS * countUp.period;
|
|
||||||
return (
|
|
||||||
<CountUp
|
|
||||||
start={countUp.start}
|
|
||||||
end={countUpEnd}
|
|
||||||
duration={PERF_UPDATE_SEC * COUNT_PERIODS}
|
|
||||||
delay={0}
|
|
||||||
useEasing={false}
|
|
||||||
preserveValue={true}
|
|
||||||
separator=","
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import io from "socket.io-client";
|
import io from "socket.io-client";
|
||||||
|
|
||||||
import { pick, number, is, StructType } from "superstruct";
|
import { pick, array, nullable, number, is, StructType } from "superstruct";
|
||||||
import { useCluster, Cluster } from "providers/cluster";
|
import { useCluster, Cluster } from "providers/cluster";
|
||||||
|
|
||||||
const DashboardInfo = pick({
|
const DashboardInfo = pick({
|
||||||
@@ -24,6 +24,11 @@ export const PERF_UPDATE_SEC = 5;
|
|||||||
|
|
||||||
const PerformanceInfo = pick({
|
const PerformanceInfo = pick({
|
||||||
avgTPS: number(),
|
avgTPS: number(),
|
||||||
|
perfHistory: pick({
|
||||||
|
s: array(nullable(number())),
|
||||||
|
m: array(nullable(number())),
|
||||||
|
l: array(nullable(number())),
|
||||||
|
}),
|
||||||
totalTransactionCount: number(),
|
totalTransactionCount: number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -42,7 +47,17 @@ const DashboardContext = React.createContext<DashboardState | undefined>(
|
|||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
export type PerformanceInfo = StructType<typeof PerformanceInfo>;
|
export type PerformanceInfo = {
|
||||||
|
avgTps: number;
|
||||||
|
historyMaxTps: number;
|
||||||
|
perfHistory: {
|
||||||
|
short: (number | null)[];
|
||||||
|
medium: (number | null)[];
|
||||||
|
long: (number | null)[];
|
||||||
|
};
|
||||||
|
transactionCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
type PerformanceState = { info: PerformanceInfo | undefined };
|
type PerformanceState = { info: PerformanceInfo | undefined };
|
||||||
const PerformanceContext = React.createContext<PerformanceState | undefined>(
|
const PerformanceContext = React.createContext<PerformanceState | undefined>(
|
||||||
undefined
|
undefined
|
||||||
@@ -87,7 +102,39 @@ export function SolanaBeachProvider({ children }: Props) {
|
|||||||
});
|
});
|
||||||
socket.on("performanceInfo", (data: any) => {
|
socket.on("performanceInfo", (data: any) => {
|
||||||
if (is(data, PerformanceInfo)) {
|
if (is(data, PerformanceInfo)) {
|
||||||
setPerformanceInfo(data);
|
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) => {
|
socket.on("rootNotification", (data: any) => {
|
||||||
|
@@ -198,3 +198,49 @@ h4.slot-pill {
|
|||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#chartjs-tooltip {
|
||||||
|
opacity: 1;
|
||||||
|
position: absolute;
|
||||||
|
-webkit-transition: all .1s ease;
|
||||||
|
transition: all .1s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
-webkit-transform: translate(-50%, -105%);
|
||||||
|
transform: translate(-50%, -105%);
|
||||||
|
|
||||||
|
div.content {
|
||||||
|
font-size: 12px;
|
||||||
|
color: white;
|
||||||
|
margin: 0px;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.label {
|
||||||
|
padding: 10px;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.value {
|
||||||
|
padding: 10px;
|
||||||
|
background: black;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
left: 50%;
|
||||||
|
top: -1px;
|
||||||
|
border-right: 10px solid transparent;
|
||||||
|
border-top: 10px solid #111;
|
||||||
|
border-left: 10px solid transparent;
|
||||||
|
-webkit-transform: translate(-50%, 0);
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
content:'';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user