Skip to content

Commit e9a1c77

Browse files
authored
feat(timings): add 90d, 6mo, and 1y time ranges (#442)
Long ranges fetch the lightweight daily winrate aggregation (fct_engine_new_payload_winrate_daily) instead of the hourly and per-slot tables, which are too heavy at this scale. The winrate chart renders one normalized stacked bar per day, while hourly-backed sections show an explicit unavailability note.
1 parent e960b1a commit e9a1c77

6 files changed

Lines changed: 201 additions & 25 deletions

File tree

src/pages/ethereum/execution/timings/IndexPage.types.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { z } from 'zod';
33
/**
44
* Time range options for engine timing data
55
*/
6-
export type TimeRange = '1hour' | '6hours' | '24hours' | '7days' | '31days';
6+
export type TimeRange = '1hour' | '6hours' | '24hours' | '7days' | '31days' | '90days' | '180days' | '365days';
77

88
/**
99
* Time range configuration with labels and duration in seconds
@@ -14,6 +14,9 @@ export const TIME_RANGE_CONFIG: Record<TimeRange, { label: string; seconds: numb
1414
'24hours': { label: 'Last 24h', seconds: 86400 },
1515
'7days': { label: 'Last 7d', seconds: 604800 },
1616
'31days': { label: 'Last 31d', seconds: 2678400 },
17+
'90days': { label: 'Last 90d', seconds: 7776000 },
18+
'180days': { label: 'Last 6mo', seconds: 15552000 },
19+
'365days': { label: 'Last 1y', seconds: 31536000 },
1720
};
1821

1922
/**
@@ -22,6 +25,12 @@ export const TIME_RANGE_CONFIG: Record<TimeRange, { label: string; seconds: numb
2225
*/
2326
export const PER_SLOT_CHART_RANGES: TimeRange[] = ['1hour', '6hours'];
2427

28+
/**
29+
* Time ranges backed by hourly aggregated data (per-slot or hourly charts)
30+
* Longer ranges only fetch the daily winrate aggregation and hide hourly-based sections
31+
*/
32+
export const HOURLY_CHART_RANGES: TimeRange[] = ['1hour', '6hours', '24hours', '7days', '31days'];
33+
2534
/**
2635
* Tab identifiers for the timings page
2736
*/
@@ -36,7 +45,7 @@ export const timingsSearchSchema = z.object({
3645
tab: z.enum(['overview', 'newPayload', 'getBlobs', 'clients']).optional(),
3746

3847
// Time range for data queries
39-
range: z.enum(['1hour', '6hours', '24hours', '7days', '31days']).optional(),
48+
range: z.enum(['1hour', '6hours', '24hours', '7days', '31days', '90days', '180days', '365days']).optional(),
4049

4150
// Filter to reference nodes only (ethPandaOps controlled fleet)
4251
refNodes: z.boolean().optional(),

src/pages/ethereum/execution/timings/components/GetBlobsTab/GetBlobsTab.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import { LoadingContainer } from '@/components/Layout/LoadingContainer';
1111
import { useThemeColors } from '@/hooks/useThemeColors';
1212
import { formatSlot, getExecutionClientColor } from '@/utils';
1313
import type { EngineTimingsData } from '../../hooks/useEngineTimingsData';
14-
import { PER_SLOT_CHART_RANGES, type TimeRange } from '../../IndexPage.types';
14+
import { PER_SLOT_CHART_RANGES, HOURLY_CHART_RANGES, type TimeRange } from '../../IndexPage.types';
1515
import { ClientVersionBreakdown } from '../ClientVersionBreakdown';
16+
import { RangeUnavailableNote } from '../RangeUnavailableNote';
1617

1718
export interface GetBlobsTabProps {
1819
data: EngineTimingsData;
@@ -75,6 +76,16 @@ export function GetBlobsTab({ data, timeRange, isLoading }: GetBlobsTabProps): J
7576
return <GetBlobsTabSkeleton />;
7677
}
7778

79+
// getBlobs data is only aggregated hourly or finer, which is too heavy for long ranges (90d+)
80+
if (!HOURLY_CHART_RANGES.includes(timeRange)) {
81+
return (
82+
<div className="space-y-6">
83+
<EIP7870SpecsBanner />
84+
<RangeUnavailableNote />
85+
</div>
86+
);
87+
}
88+
7889
const { getBlobsByElClient, getBlobsByElClientHourly, getBlobsDurationHistogram } = data;
7990

8091
// Check if we should show per-slot charts (only for short time ranges)

src/pages/ethereum/execution/timings/components/NewPayloadTab/NewPayloadTab.tsx

Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@ import { ScatterAndLineChart } from '@/components/Charts/ScatterAndLine';
1010
import { useThemeColors } from '@/hooks/useThemeColors';
1111
import { formatSlot, getExecutionClientColor } from '@/utils';
1212
import { ClientLogo } from '@/components/Ethereum/ClientLogo';
13-
import type { FctEngineNewPayloadWinrateHourly, IntEngineNewPayloadFastestExecutionByNodeClass } from '@/api/types.gen';
13+
import type {
14+
FctEngineNewPayloadWinrateHourly,
15+
FctEngineNewPayloadWinrateDaily,
16+
IntEngineNewPayloadFastestExecutionByNodeClass,
17+
} from '@/api/types.gen';
1418
import type { EngineTimingsData } from '../../hooks/useEngineTimingsData';
15-
import { PER_SLOT_CHART_RANGES, type TimeRange } from '../../IndexPage.types';
19+
import { PER_SLOT_CHART_RANGES, HOURLY_CHART_RANGES, type TimeRange } from '../../IndexPage.types';
1620
import { ClientVersionBreakdown } from '../ClientVersionBreakdown';
21+
import { RangeUnavailableNote } from '../RangeUnavailableNote';
1722

1823
export interface NewPayloadTabProps {
1924
data: EngineTimingsData;
@@ -30,12 +35,16 @@ export function NewPayloadTab({ data, timeRange }: NewPayloadTabProps): JSX.Elem
3035
newPayloadByElClient,
3136
newPayloadByElClientHourly,
3237
winrateHourly,
38+
winrateDaily,
3339
winratePerSlot,
3440
} = data;
3541

3642
// Check if we should show per-slot charts (only for short time ranges)
3743
const showPerSlotCharts = PER_SLOT_CHART_RANGES.includes(timeRange);
3844

45+
// Long ranges (90d+) only fetch daily winrate data - hourly-based sections are unavailable
46+
const showHourlyCharts = HOURLY_CHART_RANGES.includes(timeRange);
47+
3948
// Filter to VALID status only for duration-based charts
4049
const validPayloadByElClient = newPayloadByElClient.filter(r => r.status?.toUpperCase() === 'VALID');
4150

@@ -454,10 +463,12 @@ export function NewPayloadTab({ data, timeRange }: NewPayloadTabProps): JSX.Elem
454463
{/* Execution Winrate — fastest client per slot */}
455464
<WinrateSection
456465
hourlyRecords={winrateHourly}
466+
dailyRecords={winrateDaily}
457467
perSlotRecords={computedWinratePerSlot}
458468
allClients={[...new Set([...hourlyClientList, ...elClientList])]}
459469
timeRange={timeRange}
460470
showPerSlot={showPerSlotCharts}
471+
showDaily={!showHourlyCharts}
461472
/>
462473

463474
{/* Client Version Breakdown — VALID status only */}
@@ -467,14 +478,18 @@ export function NewPayloadTab({ data, timeRange }: NewPayloadTabProps): JSX.Elem
467478
anchorId="client-duration"
468479
modalSize="full"
469480
>
470-
{() => (
471-
<ClientVersionBreakdown
472-
data={validPayloadByElClient}
473-
hourlyData={newPayloadByElClientHourly}
474-
hideRange
475-
noCard
476-
/>
477-
)}
481+
{() =>
482+
showHourlyCharts ? (
483+
<ClientVersionBreakdown
484+
data={validPayloadByElClient}
485+
hourlyData={newPayloadByElClientHourly}
486+
hideRange
487+
noCard
488+
/>
489+
) : (
490+
<RangeUnavailableNote />
491+
)
492+
}
478493
</PopoutCard>
479494

480495
{/* Per-slot charts — short time ranges only */}
@@ -570,7 +585,7 @@ export function NewPayloadTab({ data, timeRange }: NewPayloadTabProps): JSX.Elem
570585
)}
571586

572587
{/* Hourly charts — longer time ranges */}
573-
{!showPerSlotCharts && (
588+
{!showPerSlotCharts && showHourlyCharts && (
574589
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
575590
{hourlyTrendSeries.length > 0 && hourlyTrendSeries.some(s => s.data.length > 0) && (
576591
<PopoutCard
@@ -826,16 +841,20 @@ function buildStandings(totalsByImpl: Map<string, number>, allClients: string[])
826841
/** Winrate chart + standings for engine_newPayload fastest client per slot */
827842
function WinrateSection({
828843
hourlyRecords,
844+
dailyRecords,
829845
perSlotRecords,
830846
allClients,
831847
timeRange,
832848
showPerSlot,
849+
showDaily,
833850
}: {
834851
hourlyRecords: FctEngineNewPayloadWinrateHourly[];
852+
dailyRecords: FctEngineNewPayloadWinrateDaily[];
835853
perSlotRecords: IntEngineNewPayloadFastestExecutionByNodeClass[];
836854
allClients: string[];
837855
timeRange: TimeRange;
838856
showPerSlot: boolean;
857+
showDaily: boolean;
839858
}): JSX.Element | null {
840859
const { chartConfig, standings, tooltipLabels } = useMemo(() => {
841860
if (showPerSlot) {
@@ -928,7 +947,77 @@ function WinrateSection({
928947
};
929948
}
930949

931-
// Hourly mode (24h / 7d)
950+
// Daily mode (90d+): one bar per day, win_count normalized by that day's total
951+
// Ties award full credit to each tied client, so totals may exceed slot counts
952+
if (showDaily) {
953+
if (!dailyRecords.length && !allClients.length)
954+
return { chartConfig: null, standings: [], tooltipLabels: [] as string[] };
955+
956+
const byDayAndImpl = new Map<string, Map<string, number>>();
957+
const allImpls = new Set<string>();
958+
959+
for (const r of dailyRecords) {
960+
const day = r.day_start_date ?? '';
961+
const impl = normalizeClient(r.meta_execution_implementation ?? '');
962+
const count = r.win_count ?? 0;
963+
if (!impl || !day) continue;
964+
965+
allImpls.add(impl);
966+
if (!byDayAndImpl.has(day)) byDayAndImpl.set(day, new Map());
967+
const dayMap = byDayAndImpl.get(day)!;
968+
dayMap.set(impl, (dayMap.get(impl) ?? 0) + count);
969+
}
970+
971+
// YYYY-MM-DD strings sort chronologically
972+
const sortedDays = [...byDayAndImpl.keys()].sort();
973+
if (!sortedDays.length) return { chartConfig: null, standings: [] };
974+
975+
const totalsByDay = new Map<string, number>();
976+
for (const day of sortedDays) {
977+
const implMap = byDayAndImpl.get(day)!;
978+
let total = 0;
979+
for (const count of implMap.values()) total += count;
980+
totalsByDay.set(day, total);
981+
}
982+
983+
const labels = sortedDays.map(day => {
984+
const date = new Date(day + 'T00:00:00Z');
985+
return date.toLocaleDateString([], { month: 'short', day: 'numeric', timeZone: 'UTC' });
986+
});
987+
988+
const tooltipLabels = sortedDays.map(day => {
989+
const date = new Date(day + 'T00:00:00Z');
990+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' });
991+
});
992+
993+
const sortedImpls = [...allImpls].sort();
994+
const series: SeriesData[] = sortedImpls.map((impl, i) => ({
995+
name: impl,
996+
data: sortedDays.map(day => {
997+
const count = byDayAndImpl.get(day)?.get(impl) ?? 0;
998+
const total = totalsByDay.get(day) ?? 0;
999+
return total > 0 ? Number(((count / total) * 100).toFixed(2)) : null;
1000+
}),
1001+
color: getExecutionClientColor(impl, i),
1002+
seriesType: 'bar' as const,
1003+
stack: 'winrate',
1004+
}));
1005+
1006+
const totalsByImpl = new Map<string, number>();
1007+
for (const r of dailyRecords) {
1008+
const impl = normalizeClient(r.meta_execution_implementation ?? '');
1009+
if (!impl) continue;
1010+
totalsByImpl.set(impl, (totalsByImpl.get(impl) ?? 0) + (r.win_count ?? 0));
1011+
}
1012+
1013+
return {
1014+
chartConfig: { labels, series },
1015+
standings: buildStandings(totalsByImpl, allClients),
1016+
tooltipLabels,
1017+
};
1018+
}
1019+
1020+
// Hourly mode (24h / 7d / 31d)
9321021
if (!hourlyRecords.length && !allClients.length)
9331022
return { chartConfig: null, standings: [], tooltipLabels: [] as string[] };
9341023

@@ -1014,7 +1103,7 @@ function WinrateSection({
10141103
standings: buildStandings(totalsByImpl, allClients),
10151104
tooltipLabels,
10161105
};
1017-
}, [hourlyRecords, perSlotRecords, allClients, timeRange, showPerSlot]);
1106+
}, [hourlyRecords, dailyRecords, perSlotRecords, allClients, timeRange, showPerSlot, showDaily]);
10181107

10191108
// Custom tooltip formatter that shows full timestamps instead of abbreviated axis labels
10201109
const winrateTooltipFormatter = useMemo(() => {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { JSX } from 'react';
2+
3+
/**
4+
* Empty-state note for sections whose backing data is only aggregated
5+
* at hourly or finer granularity and is too heavy to fetch for long ranges
6+
*/
7+
export function RangeUnavailableNote(): JSX.Element {
8+
return (
9+
<div className="flex h-32 items-center justify-center rounded-sm border border-dashed border-border bg-surface/30 text-sm text-muted">
10+
Not available for ranges beyond 31 days
11+
</div>
12+
);
13+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { RangeUnavailableNote } from './RangeUnavailableNote';

0 commit comments

Comments
 (0)