perf(beszel): deep memoization pass for live chart rendering
- Set withDots={false} to eliminate thousands of hidden SVG Dot nodes
- Disable Recharts animation (isAnimationActive: false) during data updates
- Merge ChartPanel + BeszelAreaChart into BeszelChartPanel to fix
React.memo being defeated by children prop creating new refs
- Memoize yAxisProps inside BeszelAreaChart instead of rebuilding inline
- Hoist chartStyle, panelStyle, CPU_Y_AXIS_DOMAIN to module constants
- Memoize subscription input/enabled in useLiveStats
- Replace O(n) find() with Map lookup in useDockerChartData hot path
This commit is contained in:
3 files changed
+88
-107
No files matched your search
@@ -12,8 +12,8 @@ import classes from "./component.module.css";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { containerColors } from "../beszel/_shared/colors";
|
||||
import {
|
||||
BeszelAreaChart,
|
||||
ChartPanel,
|
||||
BeszelChartPanel,
|
||||
CPU_Y_AXIS_DOMAIN,
|
||||
useContainerNames,
|
||||
useDockerChartData,
|
||||
useSystemChartData,
|
||||
@@ -229,105 +229,69 @@ export default function BeszelSystemStatsWidget({
|
||||
{activeStats && (
|
||||
<SimpleGrid cols={cols} spacing="md">
|
||||
{options.showCpu && cpuData.length > 0 && (
|
||||
<ChartPanel title={t("chart.cpu.title")} subtitle={t("chart.cpu.subtitle")}>
|
||||
<BeszelAreaChart
|
||||
h={CHART_HEIGHT}
|
||||
data={cpuData}
|
||||
series={cpuSeries}
|
||||
yAxisFormatter={chartAxisFormatters.percent}
|
||||
yAxisDomain={[0, "auto"]}
|
||||
tooltipProps={tooltipPercent}
|
||||
/>
|
||||
</ChartPanel>
|
||||
<BeszelChartPanel
|
||||
title={t("chart.cpu.title")}
|
||||
subtitle={t("chart.cpu.subtitle")}
|
||||
chartProps={{ h: CHART_HEIGHT, data: cpuData, series: cpuSeries, yAxisFormatter: chartAxisFormatters.percent, yAxisDomain: CPU_Y_AXIS_DOMAIN, tooltipProps: tooltipPercent }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{options.showMemory && memoryData.length > 0 && (
|
||||
<ChartPanel title={t("chart.memory.title")} subtitle={t("chart.memory.subtitle")}>
|
||||
<BeszelAreaChart
|
||||
h={CHART_HEIGHT}
|
||||
data={memoryData}
|
||||
type="stacked"
|
||||
series={memorySeries}
|
||||
yAxisFormatter={chartAxisFormatters.gb}
|
||||
tooltipProps={tooltipGB}
|
||||
/>
|
||||
</ChartPanel>
|
||||
<BeszelChartPanel
|
||||
title={t("chart.memory.title")}
|
||||
subtitle={t("chart.memory.subtitle")}
|
||||
chartProps={{ h: CHART_HEIGHT, data: memoryData, type: "stacked", series: memorySeries, yAxisFormatter: chartAxisFormatters.gb, tooltipProps: tooltipGB }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{options.showDisk && diskData.length > 0 && (
|
||||
<ChartPanel title={t("chart.disk.title")} subtitle={t("chart.disk.subtitle")}>
|
||||
<BeszelAreaChart
|
||||
h={CHART_HEIGHT}
|
||||
data={diskData}
|
||||
series={diskSeries}
|
||||
yAxisFormatter={chartAxisFormatters.gb}
|
||||
tooltipProps={tooltipGB}
|
||||
/>
|
||||
</ChartPanel>
|
||||
<BeszelChartPanel
|
||||
title={t("chart.disk.title")}
|
||||
subtitle={t("chart.disk.subtitle")}
|
||||
chartProps={{ h: CHART_HEIGHT, data: diskData, series: diskSeries, yAxisFormatter: chartAxisFormatters.gb, tooltipProps: tooltipGB }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{options.showDiskIO && diskIOData.length > 0 && (
|
||||
<ChartPanel title={t("chart.diskIO.title")} subtitle={t("chart.diskIO.subtitle")}>
|
||||
<BeszelAreaChart
|
||||
h={CHART_HEIGHT}
|
||||
data={diskIOData}
|
||||
series={diskIOSeries}
|
||||
yAxisFormatter={chartAxisFormatters.rate}
|
||||
tooltipProps={tooltipRate}
|
||||
/>
|
||||
</ChartPanel>
|
||||
<BeszelChartPanel
|
||||
title={t("chart.diskIO.title")}
|
||||
subtitle={t("chart.diskIO.subtitle")}
|
||||
chartProps={{ h: CHART_HEIGHT, data: diskIOData, series: diskIOSeries, yAxisFormatter: chartAxisFormatters.rate, tooltipProps: tooltipRate }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{options.showNetwork && networkData.length > 0 && (
|
||||
<ChartPanel title={t("chart.network.title")} subtitle={t("chart.network.subtitle")}>
|
||||
<BeszelAreaChart
|
||||
h={CHART_HEIGHT}
|
||||
data={networkData}
|
||||
series={networkSeries}
|
||||
yAxisFormatter={chartAxisFormatters.rate}
|
||||
tooltipProps={tooltipRate}
|
||||
/>
|
||||
</ChartPanel>
|
||||
<BeszelChartPanel
|
||||
title={t("chart.network.title")}
|
||||
subtitle={t("chart.network.subtitle")}
|
||||
chartProps={{ h: CHART_HEIGHT, data: networkData, series: networkSeries, yAxisFormatter: chartAxisFormatters.rate, tooltipProps: tooltipRate }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showDocker && containerSeries.length > 0 && (
|
||||
<>
|
||||
{options.showDockerCpu && dockerCpuData.length > 0 && (
|
||||
<ChartPanel title={t("chart.dockerCpu.title")} subtitle={t("chart.dockerCpu.subtitle")}>
|
||||
<BeszelAreaChart
|
||||
h={CHART_HEIGHT}
|
||||
data={dockerCpuData}
|
||||
type="stacked"
|
||||
series={containerSeries}
|
||||
yAxisFormatter={chartAxisFormatters.percent}
|
||||
tooltipProps={tooltipPercentTotal}
|
||||
/>
|
||||
</ChartPanel>
|
||||
<BeszelChartPanel
|
||||
title={t("chart.dockerCpu.title")}
|
||||
subtitle={t("chart.dockerCpu.subtitle")}
|
||||
chartProps={{ h: CHART_HEIGHT, data: dockerCpuData, type: "stacked", series: containerSeries, yAxisFormatter: chartAxisFormatters.percent, tooltipProps: tooltipPercentTotal }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{options.showDockerMemory && dockerMemData.length > 0 && (
|
||||
<ChartPanel title={t("chart.dockerMemory.title")} subtitle={t("chart.dockerMemory.subtitle")}>
|
||||
<BeszelAreaChart
|
||||
h={CHART_HEIGHT}
|
||||
data={dockerMemData}
|
||||
type="stacked"
|
||||
series={containerSeries}
|
||||
yAxisFormatter={chartAxisFormatters.bytes}
|
||||
tooltipProps={tooltipBytesTotal}
|
||||
/>
|
||||
</ChartPanel>
|
||||
<BeszelChartPanel
|
||||
title={t("chart.dockerMemory.title")}
|
||||
subtitle={t("chart.dockerMemory.subtitle")}
|
||||
chartProps={{ h: CHART_HEIGHT, data: dockerMemData, type: "stacked", series: containerSeries, yAxisFormatter: chartAxisFormatters.bytes, tooltipProps: tooltipBytesTotal }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{options.showDockerNetwork && dockerNetData.length > 0 && (
|
||||
<ChartPanel title={t("chart.dockerNetwork.title")} subtitle={t("chart.dockerNetwork.subtitle")}>
|
||||
<BeszelAreaChart
|
||||
h={CHART_HEIGHT}
|
||||
data={dockerNetData}
|
||||
series={containerSeries}
|
||||
yAxisFormatter={chartAxisFormatters.rate}
|
||||
tooltipProps={tooltipRate}
|
||||
/>
|
||||
</ChartPanel>
|
||||
<BeszelChartPanel
|
||||
title={t("chart.dockerNetwork.title")}
|
||||
subtitle={t("chart.dockerNetwork.subtitle")}
|
||||
chartProps={{ h: CHART_HEIGHT, data: dockerNetData, series: containerSeries, yAxisFormatter: chartAxisFormatters.rate, tooltipProps: tooltipRate }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -19,17 +19,19 @@ function prepareRecords<T>(records: T[], live: boolean) {
|
||||
}
|
||||
|
||||
const yAxisBase = { tickMargin: 0, tick: { fontSize: 10 } } as const;
|
||||
const activeDot = { r: 1, strokeWidth: 3 } as const;
|
||||
const hiddenDot = { r: 0, strokeWidth: 0 } as const;
|
||||
const chartStyle = { minWidth: 0, minHeight: 0 } as const;
|
||||
const panelStyle = { minWidth: 0, overflow: "hidden" } as const;
|
||||
const noAnimation = { isAnimationActive: false } as const;
|
||||
export const CPU_Y_AXIS_DOMAIN: [number, string] = [0, "auto"];
|
||||
|
||||
interface ChartPanelProps {
|
||||
interface BeszelChartPanelProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
children: React.ReactNode;
|
||||
chartProps: BeszelAreaChartProps;
|
||||
}
|
||||
|
||||
export const ChartPanel = memo(({ title, subtitle, children }: ChartPanelProps) => (
|
||||
<Stack gap={4} style={{ minWidth: 0, overflow: "hidden" }}>
|
||||
export const BeszelChartPanel = memo(({ title, subtitle, chartProps }: BeszelChartPanelProps) => (
|
||||
<Stack gap={4} style={panelStyle}>
|
||||
<Group gap="xs">
|
||||
<Text size="sm" fw={600}>
|
||||
{title}
|
||||
@@ -40,7 +42,7 @@ export const ChartPanel = memo(({ title, subtitle, children }: ChartPanelProps)
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
{children}
|
||||
<BeszelAreaChart {...chartProps} />
|
||||
</Stack>
|
||||
));
|
||||
|
||||
@@ -49,35 +51,45 @@ type BeszelAreaChartProps = Omit<AreaChartProps, "dataKey" | "curveType" | "with
|
||||
yAxisDomain?: [number, string];
|
||||
};
|
||||
|
||||
export const BeszelAreaChart = memo(
|
||||
({ yAxisFormatter, yAxisDomain, yAxisProps, style, type = "default", ...props }: BeszelAreaChartProps) => (
|
||||
const BeszelAreaChart = memo(({
|
||||
yAxisFormatter,
|
||||
yAxisDomain,
|
||||
yAxisProps: yAxisPropsOverride,
|
||||
type = "default",
|
||||
...props
|
||||
}: BeszelAreaChartProps) => {
|
||||
const mergedYAxis = useMemo(
|
||||
() => ({
|
||||
...yAxisBase,
|
||||
width: 32,
|
||||
tickMargin: 2,
|
||||
...(yAxisDomain ? { domain: yAxisDomain } : {}),
|
||||
tickFormatter: yAxisFormatter,
|
||||
...yAxisPropsOverride,
|
||||
}),
|
||||
[yAxisFormatter, yAxisDomain, yAxisPropsOverride],
|
||||
);
|
||||
|
||||
return (
|
||||
<AreaChart
|
||||
dataKey="time"
|
||||
curveType="monotone"
|
||||
withGradient={false}
|
||||
connectNulls
|
||||
withDots={true}
|
||||
withDots={false}
|
||||
type={type}
|
||||
strokeWidth={1}
|
||||
fillOpacity={0.2}
|
||||
activeDotProps={activeDot}
|
||||
dotProps={hiddenDot}
|
||||
areaChartProps={noAnimation}
|
||||
withXAxis
|
||||
withYAxis
|
||||
w="100%"
|
||||
style={{ minWidth: 0, minHeight: 0, ...style }}
|
||||
yAxisProps={{
|
||||
...yAxisBase,
|
||||
width: 32,
|
||||
tickMargin: 2,
|
||||
...(yAxisDomain ? { domain: yAxisDomain } : {}),
|
||||
tickFormatter: yAxisFormatter,
|
||||
...yAxisProps,
|
||||
}}
|
||||
style={chartStyle}
|
||||
yAxisProps={mergedYAxis}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
export const useSystemChartData = (
|
||||
systemStats: BeszelSystemStatsRecord[] | undefined,
|
||||
@@ -135,8 +147,9 @@ export const useDockerChartData = (
|
||||
const { fmt, ordered } = prepareRecords(containerStats, live);
|
||||
return ordered.map((record) => {
|
||||
const point: Record<string, unknown> = { time: fmt(record.created) };
|
||||
const byName = new Map(record.stats.map((c) => [c.n, c]));
|
||||
for (const name of containerNames) {
|
||||
point[name] = extract(record.stats.find((c) => c.n === name));
|
||||
point[name] = extract(byName.get(name));
|
||||
}
|
||||
return point;
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { BeszelContainerStatsRecord, BeszelSystemStatsRecord } from "@homarr/integrations/types";
|
||||
@@ -41,10 +41,14 @@ export const useLiveStats = (integrationIds: string[], systemId: string, enabled
|
||||
setError(err instanceof Error ? err : new Error(String(err)));
|
||||
}, []);
|
||||
|
||||
clientApi.widget.beszel.subscribeSystemStats.useSubscription(
|
||||
{ integrationIds, systemId },
|
||||
{ enabled: enabled && systemId !== "", onData, onError },
|
||||
);
|
||||
const subscriptionInput = useMemo(() => ({ integrationIds, systemId }), [integrationKey, systemId]);
|
||||
const subscriptionEnabled = enabled && systemId !== "";
|
||||
|
||||
clientApi.widget.beszel.subscribeSystemStats.useSubscription(subscriptionInput, {
|
||||
enabled: subscriptionEnabled,
|
||||
onData,
|
||||
onError,
|
||||
});
|
||||
|
||||
return { data, error };
|
||||
};
|
||||
Reference in new issue
Block a user