diff --git a/explorer/src/pages/ClusterStatsPage.tsx b/explorer/src/pages/ClusterStatsPage.tsx
index 4f4a70013e..b110afe2de 100644
--- a/explorer/src/pages/ClusterStatsPage.tsx
+++ b/explorer/src/pages/ClusterStatsPage.tsx
@@ -10,6 +10,7 @@ import {
import { slotsToHumanString } from "utils";
import { useCluster } from "providers/cluster";
import { TpsCard } from "components/TpsCard";
+import { displayTimestamp } from "utils/date";
const CLUSTER_STATS_TIMEOUT = 10000;
@@ -52,7 +53,12 @@ function StatsCardBody() {
return ;
}
- const { avgSlotTime_1h, avgSlotTime_1min, epochInfo } = dashboardInfo;
+ const {
+ avgSlotTime_1h,
+ avgSlotTime_1min,
+ epochInfo,
+ blockTime,
+ } = dashboardInfo;
const hourlySlotTime = Math.round(1000 * avgSlotTime_1h);
const averageSlotTime = Math.round(1000 * avgSlotTime_1min);
const { slotIndex, slotsInEpoch } = epochInfo;
@@ -80,6 +86,14 @@ function StatsCardBody() {
)}
+ {blockTime && (
+
+ Block time |
+
+ {displayTimestamp(blockTime)}
+ |
+
+ )}
Slot time (1min average) |
{averageSlotTime}ms |
diff --git a/explorer/src/providers/stats/solanaClusterStats.tsx b/explorer/src/providers/stats/solanaClusterStats.tsx
index 726136e5d3..8672d0f22f 100644
--- a/explorer/src/providers/stats/solanaClusterStats.tsx
+++ b/explorer/src/providers/stats/solanaClusterStats.tsx
@@ -18,6 +18,7 @@ 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 BLOCK_TIME_INTERVAL = 5000;
export const LOADING_TIMEOUT = 10000;
export enum ClusterStatsStatus {
@@ -89,6 +90,7 @@ export function SolanaClusterStatsProvider({ children }: Props) {
if (!active || !url) return;
const connection = new Connection(url);
+ let lastSlot: number | null = null;
const getPerformanceSamples = async () => {
try {
@@ -148,6 +150,7 @@ export function SolanaClusterStatsProvider({ children }: Props) {
const getEpochInfo = async () => {
try {
const epochInfo = await connection.getEpochInfo();
+ lastSlot = epochInfo.absoluteSlot;
dispatchDashboardInfo({
type: DashboardInfoActionType.SetEpochInfo,
data: epochInfo,
@@ -164,6 +167,25 @@ export function SolanaClusterStatsProvider({ children }: Props) {
}
};
+ const getBlockTime = async () => {
+ if (lastSlot) {
+ try {
+ const blockTime = await connection.getBlockTime(lastSlot);
+ if (blockTime !== null) {
+ dispatchDashboardInfo({
+ type: DashboardInfoActionType.SetLastBlockTime,
+ data: {
+ slot: lastSlot,
+ blockTime: blockTime * 1000,
+ },
+ });
+ }
+ } catch (error) {
+ // let this fail gracefully
+ }
+ }
+ };
+
const performanceInterval = setInterval(
getPerformanceSamples,
PERFORMANCE_SAMPLE_INTERVAL
@@ -173,15 +195,20 @@ export function SolanaClusterStatsProvider({ children }: Props) {
TRANSACTION_COUNT_INTERVAL
);
const epochInfoInterval = setInterval(getEpochInfo, EPOCH_INFO_INTERVAL);
+ const blockTimeInterval = setInterval(getBlockTime, BLOCK_TIME_INTERVAL);
getPerformanceSamples();
getTransactionCount();
- getEpochInfo();
+ (async () => {
+ await getEpochInfo();
+ await getBlockTime();
+ })();
return () => {
clearInterval(performanceInterval);
clearInterval(transactionCountInterval);
clearInterval(epochInfoInterval);
+ clearInterval(blockTimeInterval);
};
}, [active, cluster, url]);
diff --git a/explorer/src/providers/stats/solanaDashboardInfo.tsx b/explorer/src/providers/stats/solanaDashboardInfo.tsx
index 67afb0e648..9e02a8c008 100644
--- a/explorer/src/providers/stats/solanaDashboardInfo.tsx
+++ b/explorer/src/providers/stats/solanaDashboardInfo.tsx
@@ -6,11 +6,19 @@ export type DashboardInfo = {
avgSlotTime_1h: number;
avgSlotTime_1min: number;
epochInfo: EpochInfo;
+ blockTime?: number;
+ lastBlockTime?: BlockTimeInfo;
+};
+
+export type BlockTimeInfo = {
+ blockTime: number;
+ slot: number;
};
export enum DashboardInfoActionType {
SetPerfSamples,
SetEpochInfo,
+ SetLastBlockTime,
SetError,
Reset,
}
@@ -35,17 +43,32 @@ export type DashboardInfoActionSetError = {
data: string;
};
+export type DashboardInfoActionSetLastBlockTime = {
+ type: DashboardInfoActionType.SetLastBlockTime;
+ data: BlockTimeInfo;
+};
+
export type DashboardInfoAction =
| DashboardInfoActionSetPerfSamples
| DashboardInfoActionSetEpochInfo
| DashboardInfoActionReset
- | DashboardInfoActionSetError;
+ | DashboardInfoActionSetError
+ | DashboardInfoActionSetLastBlockTime;
export function dashboardInfoReducer(
state: DashboardInfo,
action: DashboardInfoAction
) {
switch (action.type) {
+ case DashboardInfoActionType.SetLastBlockTime: {
+ const blockTime = state.blockTime || action.data.blockTime;
+ return {
+ ...state,
+ lastBlockTime: action.data,
+ blockTime,
+ };
+ }
+
case DashboardInfoActionType.SetPerfSamples: {
if (action.data.length < 1) {
return state;
@@ -85,10 +108,25 @@ export function dashboardInfoReducer(
? ClusterStatsStatus.Ready
: ClusterStatsStatus.Loading;
+ let blockTime = state.blockTime;
+
+ // interpolate blocktime based on last known blocktime and average slot time
+ if (
+ state.lastBlockTime &&
+ state.avgSlotTime_1h !== 0 &&
+ action.data.absoluteSlot >= state.lastBlockTime.slot
+ ) {
+ blockTime =
+ state.lastBlockTime.blockTime +
+ (action.data.absoluteSlot - state.lastBlockTime.slot) *
+ Math.floor(state.avgSlotTime_1h * 1000);
+ }
+
return {
...state,
epochInfo: action.data,
status,
+ blockTime,
};
}