Skip to content

Commit

Permalink
Use validators.app info for mainnet ping chart (#414)
Browse files Browse the repository at this point in the history
# Changes to Note

- Mainnet ping chart is now provided as the median confirmation time for
transactions as reported by
[validators.app](https://www.validators.app/ping-thing?locale=en&network=mainnet)

Not a change, but worth calling out- TPS chart timing has never been 1-1
with ping information because TPS and Ping signals have different
sampling rates
  • Loading branch information
ngundotra authored Nov 20, 2024
1 parent d3ef00b commit 8a6f977
Show file tree
Hide file tree
Showing 4 changed files with 416 additions and 13 deletions.
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

0 comments on commit 8a6f977

Please sign in to comment.