Skip to content
Open
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 @@ -8,6 +8,7 @@ import { ClientLogo } from '@/components/Ethereum/ClientLogo';
import { SelectMenu } from '@/components/Forms/SelectMenu';
import type { FctNodeCpuUtilizationByProcess } from '@/api/types.gen';
import { type CpuMetric, type AnnotationType, type AnnotationEvent, type HighlightRange } from './types';
import { formatPercent } from './utils';
import { AnnotationSwimLanes } from './AnnotationSwimLanes';

function usToSeconds(us: number): number {
Expand Down Expand Up @@ -320,7 +321,7 @@ export function CpuUtilizationChart({
yAxis={{
name: '% of all cores',
min: 0,
formatter: (v: number) => `${v.toFixed(1)}%`,
formatter: (v: number) => formatPercent(v),
}}
height={inModal ? 500 : 350}
showLegend
Expand All @@ -345,7 +346,7 @@ export function CpuUtilizationChart({
html += `<div style="display:flex;align-items:center;gap:6px;margin:2px 0">`;
html += `${p.marker}`;
html += `<span style="flex:1">${p.seriesName}</span>`;
html += `<span style="font-weight:600">${Number(val).toFixed(1)}%</span>`;
html += `<span style="font-weight:600">${formatPercent(Number(val))}</span>`;
html += `</div>`;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DEFAULT_BEACON_SLOT_PHASES } from '@/utils/beacon';
import { ClientLogo } from '@/components/Ethereum/ClientLogo';
import type { FctNodeDiskIoByProcess } from '@/api/types.gen';
import { type AnnotationType, type AnnotationEvent, type HighlightRange } from './types';
import { getByteScale, formatScaled, seriesMax } from './utils';
import { AnnotationSwimLanes } from './AnnotationSwimLanes';

function usToSeconds(us: number): number {
Expand Down Expand Up @@ -76,9 +77,9 @@ export function DiskIoChart({
[]
);

const { series, clClient, elClient } = useMemo(() => {
const { series, clClient, elClient, displayUnit } = useMemo(() => {
if (data.length === 0) {
return { series: [] as SeriesData[], clClient: '', elClient: '' };
return { series: [] as SeriesData[], clClient: '', elClient: '', displayUnit: 'KB' };
}

const slotStartUs = Math.min(...data.map(d => d.wallclock_slot_start_date_time ?? 0).filter(v => v > 0));
Expand Down Expand Up @@ -201,7 +202,18 @@ export function DiskIoChart({
});
}

return { series: chartSeries, clClient: resolvedClClient, elClient: resolvedElClient };
// Scale data to appropriate unit so ECharts picks nice round ticks
const maxKB = seriesMax(chartSeries);
const { divisor, unit } = getByteScale(maxKB, 'KB');
if (divisor > 1) {
for (const s of chartSeries) {
for (const p of s.data as [number, number][]) {
p[1] /= divisor;
}
}
}

return { series: chartSeries, clClient: resolvedClClient, elClient: resolvedElClient, displayUnit: unit };
}, [data, selectedNode, CHART_CATEGORICAL_COLORS]);

const markLines = useMemo((): MarkLineConfig[] => {
Expand Down Expand Up @@ -273,9 +285,9 @@ export function DiskIoChart({
formatter: (v: number | string) => `${v}s`,
}}
yAxis={{
name: 'KB',
name: displayUnit,
min: 0,
formatter: (v: number) => `${v.toFixed(0)} KB`,
formatter: (v: number) => formatScaled(v, displayUnit),
}}
height={inModal ? 500 : 350}
showLegend
Expand All @@ -300,7 +312,7 @@ export function DiskIoChart({
html += `<div style="display:flex;align-items:center;gap:6px;margin:2px 0">`;
html += `${p.marker}`;
html += `<span style="flex:1">${p.seriesName}</span>`;
html += `<span style="font-weight:600">${Number(val).toFixed(1)} KB</span>`;
html += `<span style="font-weight:600">${formatScaled(Number(val), displayUnit)}</span>`;
html += `</div>`;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ClientLogo } from '@/components/Ethereum/ClientLogo';
import { SelectMenu } from '@/components/Forms/SelectMenu';
import type { FctNodeMemoryUsageByProcess } from '@/api/types.gen';
import { type AnnotationType, type AnnotationEvent, type HighlightRange } from './types';
import { getByteScale, formatScaled, seriesMax } from './utils';
import { AnnotationSwimLanes } from './AnnotationSwimLanes';

function usToSeconds(us: number): number {
Expand Down Expand Up @@ -110,9 +111,9 @@ export function MemoryUsageChart({
[]
);

const { series, clClient, elClient } = useMemo(() => {
const { series, clClient, elClient, displayUnit } = useMemo(() => {
if (data.length === 0) {
return { series: [] as SeriesData[], clClient: '', elClient: '' };
return { series: [] as SeriesData[], clClient: '', elClient: '', displayUnit: 'MB' };
}

const slotStartUs = Math.min(...data.map(d => d.wallclock_slot_start_date_time ?? 0).filter(v => v > 0));
Expand Down Expand Up @@ -202,7 +203,18 @@ export function MemoryUsageChart({
});
}

return { series: chartSeries, clClient: resolvedClClient, elClient: resolvedElClient };
// Scale data to appropriate unit so ECharts picks nice round ticks
const maxMB = seriesMax(chartSeries);
const { divisor, unit } = getByteScale(maxMB, 'MB');
if (divisor > 1) {
for (const s of chartSeries) {
for (const p of s.data as [number, number][]) {
p[1] /= divisor;
}
}
}

return { series: chartSeries, clClient: resolvedClClient, elClient: resolvedElClient, displayUnit: unit };
}, [data, selectedNode, metric, CHART_CATEGORICAL_COLORS]);

const markLines = useMemo((): MarkLineConfig[] => {
Expand Down Expand Up @@ -274,9 +286,9 @@ export function MemoryUsageChart({
formatter: (v: number | string) => `${v}s`,
}}
yAxis={{
name: 'MB',
name: displayUnit,
min: 0,
formatter: (v: number) => `${v.toFixed(0)} MB`,
formatter: (v: number) => formatScaled(v, displayUnit),
}}
height={inModal ? 500 : 350}
showLegend
Expand All @@ -301,7 +313,7 @@ export function MemoryUsageChart({
html += `<div style="display:flex;align-items:center;gap:6px;margin:2px 0">`;
html += `${p.marker}`;
html += `<span style="flex:1">${p.seriesName}</span>`;
html += `<span style="font-weight:600">${Number(val).toFixed(1)} MB</span>`;
html += `<span style="font-weight:600">${formatScaled(Number(val), displayUnit)}</span>`;
html += `</div>`;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getDataVizColors } from '@/utils';
import { DEFAULT_BEACON_SLOT_PHASES } from '@/utils/beacon';
import type { FctNodeNetworkIoByProcess } from '@/api/types.gen';
import { type AnnotationType, type AnnotationEvent, type HighlightRange } from './types';
import { getByteScale, formatScaled, seriesMax } from './utils';
import { AnnotationSwimLanes } from './AnnotationSwimLanes';

function usToSeconds(us: number): number {
Expand All @@ -19,19 +20,29 @@ function bytesToKB(bytes: number): number {
/** Port labels that are hidden by default (internal/API traffic) */
const HIDDEN_PORTS = new Set(['cl_beacon_api', 'el_json_rpc', 'el_engine_api', 'el_ws', 'cl_grpc']);

const PORT_LABELS: Record<string, string> = {
el_p2p_tcp: 'EL P2P',
cl_p2p_tcp: 'CL P2P',
cl_discovery: 'CL Discovery',
el_discovery: 'EL Discovery',
cl_beacon_api: 'CL Beacon API',
el_json_rpc: 'EL JSON-RPC',
el_engine_api: 'EL Engine API',
el_ws: 'EL WebSocket',
cl_grpc: 'CL gRPC',
unknown: 'Unknown',
/** Well-known abbreviations and expansions for port label segments */
const PORT_LABEL_TOKENS: Record<string, string> = {
cl: 'CL',
el: 'EL',
p2p: 'P2P',
tcp: 'TCP',
udp: 'UDP',
api: 'API',
rpc: 'RPC',
grpc: 'gRPC',
ws: 'WebSocket',
json: 'JSON',
io: 'I/O',
};

/** Convert a snake_case port label to a human-readable name */
function formatPortLabel(label: string): string {
return label
.split('_')
.map(part => PORT_LABEL_TOKENS[part] ?? part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
}

const BUCKET_SIZE = 0.25;
const ALL_BUCKETS = Array.from({ length: 49 }, (_, i) => i * BUCKET_SIZE);
const toBucket = (offsetSec: number): number => Math.round(offsetSec / BUCKET_SIZE) * BUCKET_SIZE;
Expand Down Expand Up @@ -87,8 +98,8 @@ export function NetworkIoChart({
[]
);

const series = useMemo(() => {
if (data.length === 0) return [] as SeriesData[];
const { series, displayUnit } = useMemo(() => {
if (data.length === 0) return { series: [] as SeriesData[], displayUnit: 'KB' };

const slotStartUs = Math.min(...data.map(d => d.wallclock_slot_start_date_time ?? 0).filter(v => v > 0));
const chartSeries: SeriesData[] = [];
Expand Down Expand Up @@ -123,7 +134,7 @@ export function NetworkIoChart({
t => [t, buckets.has(t) ? avg(buckets.get(t)!) : 0] as [number, number]
);

const label = PORT_LABELS[port] ?? port;
const label = formatPortLabel(port);
const color = CHART_CATEGORICAL_COLORS[i % CHART_CATEGORICAL_COLORS.length];

chartSeries.push({
Expand All @@ -138,7 +149,18 @@ export function NetworkIoChart({
});
}

return chartSeries;
// Scale data to appropriate unit so ECharts picks nice round ticks
const maxKB = seriesMax(chartSeries);
const { divisor, unit } = getByteScale(maxKB, 'KB');
if (divisor > 1) {
for (const s of chartSeries) {
for (const p of s.data as [number, number][]) {
p[1] /= divisor;
}
}
}

return { series: chartSeries, displayUnit: unit };
}, [data, selectedNode, CHART_CATEGORICAL_COLORS]);

const markLines = useMemo((): MarkLineConfig[] => {
Expand Down Expand Up @@ -202,9 +224,9 @@ export function NetworkIoChart({
formatter: (v: number | string) => `${v}s`,
}}
yAxis={{
name: 'KB',
name: displayUnit,
min: 0,
formatter: (v: number) => `${v.toFixed(0)} KB`,
formatter: (v: number) => formatScaled(v, displayUnit),
}}
height={inModal ? 500 : 350}
showLegend
Expand All @@ -229,7 +251,7 @@ export function NetworkIoChart({
html += `<div style="display:flex;align-items:center;gap:6px;margin:2px 0">`;
html += `${p.marker}`;
html += `<span style="flex:1">${p.seriesName}</span>`;
html += `<span style="font-weight:600">${Number(val).toFixed(1)} KB</span>`;
html += `<span style="font-weight:600">${formatScaled(Number(val), displayUnit)}</span>`;
html += `</div>`;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function NodeResourcesPanel({
const referenceNodesOnly = search.refNodes ?? false;
const selectedNode = search.node ?? null;
const metric: CpuMetric = search.metric ?? 'mean';
const memMetric: MemoryMetric = search.memMetric ?? 'vm_rss';
const memMetric: MemoryMetric = search.memMetric ?? 'rss_anon';

const setReferenceNodesOnly = useCallback(
(value: boolean) => {
Expand Down Expand Up @@ -130,7 +130,7 @@ export function NodeResourcesPanel({
navigate({
to: '/ethereum/slots/$slot',
params: { slot: String(slot) },
search: { ...search, memMetric: value === 'vm_rss' ? undefined : value },
search: { ...search, memMetric: value === 'rss_anon' ? undefined : value },
replace: true,
resetScroll: false,
});
Expand Down
65 changes: 65 additions & 0 deletions src/pages/ethereum/slots/components/NodeResources/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const BYTE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'] as const;

interface ByteScale {
divisor: number;
unit: string;
}

/**
* Determine the appropriate display unit for byte-based values.
* Returns a divisor to apply to values so they render in a human-friendly unit.
*
* @param maxValue - The largest value in the dataset (in baseUnit)
* @param baseUnit - The unit the raw values are already expressed in
*/
export function getByteScale(maxValue: number, baseUnit: 'KB' | 'MB'): ByteScale {
const baseIndex = BYTE_UNITS.indexOf(baseUnit);
let divisor = 1;
let unitIndex = baseIndex;

while (maxValue / divisor >= 1024 && unitIndex < BYTE_UNITS.length - 1) {
divisor *= 1024;
unitIndex++;
}

return { divisor, unit: BYTE_UNITS[unitIndex] };
}

/**
* Format a numeric value with adaptive precision to avoid duplicate y-axis labels.
* Picks decimal places based on value magnitude.
*/
export function formatScaled(value: number, unit: string): string {
if (value === 0) return `0 ${unit}`;
const abs = Math.abs(value);
if (abs >= 100) return `${Math.round(value)} ${unit}`;
if (abs >= 1) return `${value.toFixed(1)} ${unit}`;
return `${value.toFixed(2)} ${unit}`;
}

/**
* Format a percentage with adaptive precision to avoid duplicate y-axis labels.
*/
export function formatPercent(value: number): string {
if (value === 0) return '0%';
const abs = Math.abs(value);
if (abs >= 10) return `${Math.round(value)}%`;
if (abs >= 1) return `${value.toFixed(1)}%`;
if (abs >= 0.1) return `${value.toFixed(2)}%`;
return `${value.toFixed(3)}%`;
}

/**
* Find the max y-value across all series data points.
* Accepts the broad SeriesData.data union type.
*/
export function seriesMax(series: { data: unknown[] }[]): number {
let max = 0;
for (const s of series) {
for (const point of s.data) {
const y = Array.isArray(point) ? (point[1] as number) : (point as number);
if (typeof y === 'number' && isFinite(y) && y > max) max = y;
}
}
return max;
}