Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface ClientVersionData {
meta_execution_implementation?: string;
meta_execution_version?: string;
observation_count?: number;
median_duration_ms?: number;
avg_duration_ms?: number;
min_duration_ms?: number;
max_duration_ms?: number;
avg_returned_count?: number;
Expand All @@ -47,23 +47,21 @@ export interface ClientVersionBreakdownProps {
hideObservations?: boolean;
/** If true, hides the range column */
hideRange?: boolean;
/** Custom label for the duration column (defaults to "Median") */
durationLabel?: string;
/** Custom label for the blob count column (defaults to "Avg Blobs") */
blobCountLabel?: string;
}

interface AggregatedRow {
client: string;
version: string;
medianDuration: number;
avgDuration: number;
minDuration: number;
maxDuration: number;
observations: number;
avgBlobCount?: number;
}

type SortField = 'client' | 'version' | 'medianDuration' | 'maxDuration' | 'observations' | 'avgBlobCount';
type SortField = 'client' | 'version' | 'avgDuration' | 'observations' | 'avgBlobCount';
type SortDirection = 'asc' | 'desc';

/**
Expand All @@ -78,11 +76,10 @@ export function ClientVersionBreakdown({
compactClient = false,
hideObservations = false,
hideRange = false,
durationLabel = 'Median',
blobCountLabel = 'Avg Blobs',
}: ClientVersionBreakdownProps): JSX.Element {
// Default sort by medianDuration (ascending = fastest first) when observations hidden, otherwise by observations
const [sortField, setSortField] = useState<SortField>(hideObservations ? 'medianDuration' : 'observations');
// Default sort by avgDuration (ascending = fastest first) when observations hidden, otherwise by observations
const [sortField, setSortField] = useState<SortField>(hideObservations ? 'avgDuration' : 'observations');
const [sortDirection, setSortDirection] = useState<SortDirection>(hideObservations ? 'asc' : 'desc');

// Aggregate data by client + version
Expand All @@ -92,7 +89,7 @@ export function ClientVersionBreakdown({
{
client: string;
version: string;
totalWeightedMedian: number;
totalWeightedAvg: number;
minDuration: number;
maxDuration: number;
totalObservations: number;
Expand All @@ -110,7 +107,7 @@ export function ClientVersionBreakdown({

const existing = map.get(key);
if (existing) {
existing.totalWeightedMedian += (row.median_duration_ms ?? 0) * obs;
existing.totalWeightedAvg += (row.avg_duration_ms ?? 0) * obs;
// For min/max, take the actual min/max across all rows
if (minDur > 0 && (existing.minDuration === 0 || minDur < existing.minDuration)) {
existing.minDuration = minDur;
Expand All @@ -124,7 +121,7 @@ export function ClientVersionBreakdown({
map.set(key, {
client,
version,
totalWeightedMedian: (row.median_duration_ms ?? 0) * obs,
totalWeightedAvg: (row.avg_duration_ms ?? 0) * obs,
minDuration: minDur,
maxDuration: maxDur,
totalObservations: obs,
Expand All @@ -140,7 +137,7 @@ export function ClientVersionBreakdown({
result.push({
client: entry.client,
version: entry.version,
medianDuration: entry.totalWeightedMedian / entry.totalObservations,
avgDuration: entry.totalWeightedAvg / entry.totalObservations,
minDuration: entry.minDuration,
maxDuration: entry.maxDuration,
observations: entry.totalObservations,
Expand All @@ -152,9 +149,9 @@ export function ClientVersionBreakdown({
return result;
}, [data]);

// Calculate max median for color scaling
const maxMedianDuration = useMemo(() => {
return Math.max(...aggregatedData.map(r => r.medianDuration), 0);
// Calculate max avg duration for color scaling
const maxAvgDuration = useMemo(() => {
return Math.max(...aggregatedData.map(r => r.avgDuration), 0);
}, [aggregatedData]);

// Sort data
Expand All @@ -168,11 +165,8 @@ export function ClientVersionBreakdown({
case 'version':
comparison = a.version.localeCompare(b.version);
break;
case 'medianDuration':
comparison = a.medianDuration - b.medianDuration;
break;
case 'maxDuration':
comparison = a.maxDuration - b.maxDuration;
case 'avgDuration':
comparison = a.avgDuration - b.avgDuration;
break;
case 'observations':
comparison = a.observations - b.observations;
Expand Down Expand Up @@ -251,16 +245,11 @@ export function ClientVersionBreakdown({
Version
<SortIcon field="version" />
</th>
<th className={clsx(headerClass, 'text-right')} onClick={() => handleSort('medianDuration')}>
{durationLabel}
<SortIcon field="medianDuration" />
<th className={clsx(headerClass, 'text-right')} onClick={() => handleSort('avgDuration')}>
Avg (ms)
<SortIcon field="avgDuration" />
</th>
{!hideRange && (
<th className={clsx(headerClass, 'text-right')} onClick={() => handleSort('maxDuration')}>
Range
<SortIcon field="maxDuration" />
</th>
)}
{!hideRange && <th className={clsx(headerClass, 'text-right')}>Range</th>}
{showBlobCount && (
<th className={clsx(headerClass, 'text-right')} onClick={() => handleSort('avgBlobCount')}>
{blobCountLabel}
Expand Down Expand Up @@ -288,14 +277,11 @@ export function ClientVersionBreakdown({
<span className="font-mono text-sm text-muted">{row.version}</span>
</td>
<td className="px-3 py-3 text-right">
{row.medianDuration > 0 ? (
{row.avgDuration > 0 ? (
<span
className={clsx(
'text-sm font-medium',
getDurationColorClass(row.medianDuration, maxMedianDuration)
)}
className={clsx('text-sm font-medium', getDurationColorClass(row.avgDuration, maxAvgDuration))}
>
{row.medianDuration.toFixed(0)} ms
{row.avgDuration.toFixed(1)}
</span>
) : (
<span className="text-sm text-muted">-</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,47 +66,54 @@ export function GetBlobsTab({ data, isLoading }: GetBlobsTabProps): JSX.Element
return <GetBlobsTabSkeleton />;
}

const { getBlobsByElClient, getBlobsDurationHistogram, getBlobsHourly, getBlobsDaily } = data;
const { getBlobsByElClient, getBlobsDurationHistogram } = data;

// Filter to SUCCESS status only for duration-based charts
const successBlobsByElClient = getBlobsByElClient.filter(r => r.status?.toUpperCase() === 'SUCCESS');

// Determine which aggregated data source to use based on time range
// Always use hourly data since all time ranges are < 24 hours
const useHourlyData = true;

// Calculate summary stats from pre-aggregated hourly/daily data
// Calculate summary stats from per-slot/per-client data for accuracy
const aggregatedStats = (() => {
const sourceData = useHourlyData ? getBlobsHourly : getBlobsDaily;

if (sourceData.length === 0) {
if (getBlobsByElClient.length === 0) {
return { totalObservations: 0, successRate: '0', avgDuration: '0' };
}

// Sum up counts from all rows
// Sum up counts from all rows, grouped by status
let successCount = 0;
let partialCount = 0;
let emptyCount = 0;
let errorCount = 0;
let unsupportedCount = 0;
let totalWeightedDuration = 0;

sourceData.forEach(row => {
const success = row.success_count ?? 0;
successCount += success;
partialCount += row.partial_count ?? 0;
emptyCount += row.empty_count ?? 0;
errorCount += row.error_count ?? 0;
unsupportedCount += row.unsupported_count ?? 0;
// Weight duration averages by success count (durations are SUCCESS only from backend)
totalWeightedDuration += (row.avg_duration_ms ?? 0) * success;
getBlobsByElClient.forEach(row => {
const obs = row.observation_count ?? 0;
const status = row.status?.toUpperCase() ?? '';

switch (status) {
case 'SUCCESS':
successCount += obs;
// Duration stats only for SUCCESS
totalWeightedDuration += (row.avg_duration_ms ?? 0) * obs;
break;
case 'PARTIAL':
partialCount += obs;
break;
case 'EMPTY':
emptyCount += obs;
break;
case 'ERROR':
errorCount += obs;
break;
case 'UNSUPPORTED':
unsupportedCount += obs;
break;
}
});

const totalStatusCount = successCount + partialCount + emptyCount + errorCount + unsupportedCount;
const successRate = totalStatusCount > 0 ? ((successCount / totalStatusCount) * 100).toFixed(1) : '0';
const avgDuration = successCount > 0 ? (totalWeightedDuration / successCount).toFixed(0) : '0';

// Use successCount for observations to match the table which shows SUCCESS only
return { totalObservations: successCount, successRate, avgDuration };
})();

Expand Down Expand Up @@ -146,15 +153,27 @@ export function GetBlobsTab({ data, isLoading }: GetBlobsTabProps): JSX.Element
const histogramLabels = histogramBuckets.map(bucket => `${bucket}ms`);
const histogramData = histogramBuckets.map(bucket => histogramMap.get(bucket) ?? 0);

// Calculate status breakdown from pre-aggregated hourly data
const statusTotals = getBlobsHourly.reduce(
(acc, item) => ({
success: acc.success + (item.success_count ?? 0),
partial: acc.partial + (item.partial_count ?? 0),
empty: acc.empty + (item.empty_count ?? 0),
error: acc.error + (item.error_count ?? 0),
unsupported: acc.unsupported + (item.unsupported_count ?? 0),
}),
// Calculate status breakdown from per-slot data
const statusTotals = getBlobsByElClient.reduce(
(acc, item) => {
const obs = item.observation_count ?? 0;
const status = item.status?.toUpperCase() ?? '';

switch (status) {
case 'SUCCESS':
return { ...acc, success: acc.success + obs };
case 'PARTIAL':
return { ...acc, partial: acc.partial + obs };
case 'EMPTY':
return { ...acc, empty: acc.empty + obs };
case 'ERROR':
return { ...acc, error: acc.error + obs };
case 'UNSUPPORTED':
return { ...acc, unsupported: acc.unsupported + obs };
default:
return acc;
}
},
{ success: 0, partial: 0, empty: 0, error: 0, unsupported: 0 }
);

Expand Down Expand Up @@ -309,7 +328,7 @@ export function GetBlobsTab({ data, isLoading }: GetBlobsTabProps): JSX.Element
<ClientVersionBreakdown
data={successBlobsByElClient}
title="EL Client Duration"
description="Median engine_getBlobs duration (ms) by execution client and version"
description="engine_getBlobs duration (ms) by execution client and version"
showBlobCount
hideObservations
/>
Expand Down
Loading
Loading