Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Use validators.app info for mainnet ping chart #414

Merged
merged 3 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions app/api/ping/[network]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { fetch } from 'cross-fetch';
import { NextResponse } from 'next/server';

type Params = {
params: {
network: 'mainnet';
};
};

export type ValidatorsAppPingStats = {
interval: number;
max: number;
median: number;
min: number;
network: string;
num_of_records: number;
time_from: string;
average_slot_latency: number;
tps: number;
};

const PING_INTERVALS: number[] = [1, 3, 12];

export async function GET(_request: Request, { params: { network } }: Params) {
const responses = await Promise.all(
PING_INTERVALS.map(interval =>
fetch(`https://www.validators.app/api/v1/ping-thing-stats/${network}.json?interval=${interval}`, {
cache: 'no-store',
headers: {
'Cache-Control': 'no-store, max-age=0',
Token: process.env.PING_API_KEY || '',
},
})
)
);
const data: { [interval: number]: ValidatorsAppPingStats[] } = {};
await Promise.all(
responses.map(async (response, index) => {
const interval = PING_INTERVALS[index];
data[interval] = await response.json();
})
);

return NextResponse.json(data, {
headers: {
'Cache-Control': 'public, max-age=30',
},
});
}
226 changes: 214 additions & 12 deletions app/components/LiveTransactionStatsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ import { Bar } from 'react-chartjs-2';
import CountUp from 'react-countup';
import { RefreshCw } from 'react-feather';

import { useCluster } from '../providers/cluster';
import {
useValidatorsAppPingStats,
ValidatorsAppPingStatsInfo,
ValidatorsAppPingStatsRecord,
} from '../providers/stats/ValidatorsAppStatsProvider';
import { Cluster } from '../utils/cluster';

Chart.register(BarElement, CategoryScale, LinearScale, Tooltip);

type Series = 'short' | 'medium' | 'long';
Expand All @@ -34,24 +42,18 @@ const SERIES_INFO = {

export function LiveTransactionStatsCard() {
const [series, setSeries] = React.useState<Series>('short');
const { cluster } = useCluster();
return (
<div className="card">
<div className="card-header">
<h4 className="card-header-title">Live Transaction Stats</h4>
</div>
<TpsCardBody series={series} setSeries={setSeries} />
<div className="alert alert-warning m-2" role="alert">
Note: We are aware of an issue with ping statistic reporting. Ping statistics may not reflect actual
network performance. Please see{' '}
<a
href="https://www.validators.app/ping-thing?locale=en&network=mainnet"
className="text-white text-decoration-underline"
>
validators.app
</a>{' '}
for more information.
</div>
<PingStatsCardBody series={series} setSeries={setSeries} />
{cluster === Cluster.MainnetBeta ? (
<ValidatorsAppPingStatsCardBody series={series} setSeries={setSeries} />
) : (
<PingStatsCardBody series={series} setSeries={setSeries} />
)}
</div>
);
}
Expand All @@ -66,6 +68,29 @@ function TpsCardBody({ series, setSeries }: { series: Series; setSeries: SetSeri
return <TpsBarChart performanceInfo={performanceInfo} series={series} setSeries={setSeries} />;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function ValidatorsAppTpsCardBody({ series, setSeries }: { series: Series; setSeries: SetSeries }) {
const performanceInfo = usePerformanceInfo();
const statsInfo = useValidatorsAppPingStats();

if (performanceInfo.status !== ClusterStatsStatus.Ready || statsInfo.status !== PingStatus.Ready) {
return (
<StatsNotReady
error={performanceInfo.status === ClusterStatsStatus.Error || statsInfo.status === PingStatus.Error}
/>
);
}

return (
<ValidatorsAppTpsBarChart
statsInfo={statsInfo}
performanceInfo={performanceInfo}
series={series}
setSeries={setSeries}
/>
);
}

const TPS_CHART_OPTIONS = (historyMaxTps: number): ChartOptions<'bar'> => {
return {
animation: false,
Expand Down Expand Up @@ -157,6 +182,84 @@ const TPS_CHART_OPTIONS = (historyMaxTps: number): ChartOptions<'bar'> => {
};
};

type ValidatorsAppTpsBarChartProps = {
performanceInfo: PerformanceInfo;
statsInfo: ValidatorsAppPingStatsInfo;
series: Series;
setSeries: SetSeries;
};
function ValidatorsAppTpsBarChart({ statsInfo, performanceInfo, series, setSeries }: ValidatorsAppTpsBarChartProps) {
const seriesData = statsInfo[series] || [];
const avgTps = Math.round(
(statsInfo[SERIES[2]] || [])?.map(val => val.tps).reduce((a, b) => a + b, 0) / seriesData.length
);
const historyMaxTps = (statsInfo[SERIES[2]] || [])?.map(val => val.tps).reduce((a, b) => Math.max(a, b), 0);
const averageTps = Math.round(avgTps).toLocaleString('en-US');
const transactionCount = <AnimatedTransactionCount info={performanceInfo} />;
const chartOptions = React.useMemo<ChartOptions<'bar'>>(() => TPS_CHART_OPTIONS(historyMaxTps), [historyMaxTps]);

const now = new Date();
const chartData: ChartData<'bar'> = {
datasets: [
{
backgroundColor: '#00D192',
borderWidth: 0,
data: seriesData.map(val => (val.tps >= historyMaxTps ? avgTps : val.tps || 0)),
hoverBackgroundColor: '#00D192',
},
],
labels: seriesData.map(val => {
const minAgo = Math.round((now.getTime() - val.timestamp.getTime()) / 60000);
return `${minAgo}min ago`;
}),
};

return (
<>
<TableCardBody>
<tr>
<td className="w-100">Transaction count</td>
<td className="text-lg-end font-monospace">{transactionCount} </td>
</tr>
<tr>
<td className="w-100">Transactions per second (TPS)</td>
<td className="text-lg-end font-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 ms-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>
</>
);
}

type TpsBarChartProps = {
performanceInfo: PerformanceInfo;
series: Series;
Expand Down Expand Up @@ -287,6 +390,16 @@ function PingStatsCardBody({ series, setSeries }: { series: Series; setSeries: S
return <PingBarChart pingInfo={pingInfo} series={series} setSeries={setSeries} />;
}

function ValidatorsAppPingStatsCardBody({ series, setSeries }: { series: Series; setSeries: SetSeries }) {
const pingInfo = useValidatorsAppPingStats();

if (pingInfo.status !== PingStatus.Ready) {
return <PingStatsNotReady error={pingInfo.status === PingStatus.Error} retry={pingInfo.retry} />;
}

return <ValidatorsAppPingBarChart pingInfo={pingInfo} series={series} setSeries={setSeries} />;
}

type StatsNotReadyProps = { error: boolean; retry?: () => void };
function PingStatsNotReady({ error, retry }: StatsNotReadyProps) {
if (error) {
Expand Down Expand Up @@ -498,3 +611,92 @@ function PingBarChart({
</div>
);
}

function ValidatorsAppPingBarChart({
pingInfo,
series,
setSeries,
}: {
pingInfo: ValidatorsAppPingStatsInfo;
series: Series;
setSeries: SetSeries;
}) {
const seriesData = pingInfo[series] || [];
const maxMedian = seriesData.reduce((a, b) => {
return Math.max(a, b.median);
}, 0);
const backgroundColor = (val: ValidatorsAppPingStatsRecord) => {
if (val.submitted === 0) {
return '#08a274';
}

return '#00D192';
};
const now = new Date();
const chartData: ChartData<'bar'> = {
datasets: [
{
backgroundColor: seriesData.map(backgroundColor),
borderWidth: 0,
data: seriesData.map(val => {
if (val.submitted === 0) {
return maxMedian * 0.5;
}
return val.median || 0;
}),
hoverBackgroundColor: seriesData.map(backgroundColor),
minBarLength: 2,
},
],
labels: seriesData.map(val => {
const minutesAgo = Math.round((now.getTime() - val.timestamp.getTime()) / (60 * 1000));
if (val.submitted === 0) {
return `
<div class="label">
<p class="mb-0">Ping statistics unavailable</p>
${minutesAgo} min ago
</div>
`;
}

return `
<div class="value">${val.median} ms</div>
<div class="label">
<p class="mb-0">${val.submitted} confirmed</p>
${`<p class="mb-0">${val.average_slot_latency.toLocaleString(undefined)} Average Slot Latency</p>`}
${minutesAgo}min ago
</div>
`;
}),
};

return (
<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">Average Ping Time</span>

<div className="font-size-sm">
{SERIES.map(key => (
<button
key={key}
onClick={() => setSeries(key)}
className={classNames('btn btn-sm btn-white ms-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={PING_CHART_OPTIONS} height={80} />
</div>
</div>
</div>
</div>
);
}
Loading
Loading