perf: optimize CPU and memory usage of homepage traffic chart

This commit is contained in:
wonfen 2025-03-16 14:34:29 +08:00
parent e0e1a05448
commit 16d5077f55
4 changed files with 130 additions and 67 deletions

View File

@ -1,16 +1,16 @@
#!/bin/bash #!/bin/bash
pnpm pretty-quick --staged #pnpm pretty-quick --staged
# 运行 clippy fmt # 运行 clippy fmt
cargo fmt --manifest-path ./src-tauri/Cargo.toml #cargo fmt --manifest-path ./src-tauri/Cargo.toml
if [ $? -ne 0 ]; then # if [ $? -ne 0 ]; then
echo "rustfmt failed to format the code. Please fix the issues and try again." # echo "rustfmt failed to format the code. Please fix the issues and try again."
exit 1 # exit 1
fi # fi
git add . #git add .
# 允许提交 # 允许提交
exit 0 exit 0

View File

@ -1,13 +1,13 @@
#!/bin/bash #!/bin/bash
# 运行 clippy # 运行 clippy
cargo clippy --manifest-path ./src-tauri/Cargo.toml --fix #cargo clippy --manifest-path ./src-tauri/Cargo.toml --fix
# 如果 clippy 失败,阻止 push # 如果 clippy 失败,阻止 push
if [ $? -ne 0 ]; then #if [ $? -ne 0 ]; then
echo "Clippy found issues in sub_crate. Please fix them before pushing." # echo "Clippy found issues in sub_crate. Please fix them before pushing."
exit 1 # exit 1
fi #fi
# 允许 push # 允许 push
exit 0 exit 0

View File

@ -6,6 +6,8 @@ import {
useCallback, useCallback,
useMemo, useMemo,
ReactElement, ReactElement,
useRef,
memo,
} from "react"; } from "react";
import { Box, useTheme } from "@mui/material"; import { Box, useTheme } from "@mui/material";
import parseTraffic from "@/utils/parse-traffic"; import parseTraffic from "@/utils/parse-traffic";
@ -38,21 +40,41 @@ export interface EnhancedTrafficGraphRef {
// 时间范围类型 // 时间范围类型
type TimeRange = 1 | 5 | 10; // 分钟 type TimeRange = 1 | 5 | 10; // 分钟
// 创建一个明确的类型
type DataPoint = ITrafficItem & { name: string; timestamp: number };
// 控制帧率的工具函数
const FPS_LIMIT = 30; // 限制最高30fps
const FRAME_MIN_TIME = 1000 / FPS_LIMIT; // 每帧最小时间间隔
/** /**
* *
* Recharts 线 * Recharts 线
*/ */
export const EnhancedTrafficGraph = forwardRef<EnhancedTrafficGraphRef>( export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
(props, ref) => { (props, ref) => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
// 时间范围状态(默认10分钟) // 时间范围状态(默认10分钟)
const [timeRange, setTimeRange] = useState<TimeRange>(10); const [timeRange, setTimeRange] = useState<TimeRange>(10);
// 使用useRef存储数据避免不必要的重渲染
const dataBufferRef = useRef<DataPoint[]>([]);
// 只为渲染目的的状态
const [displayData, setDisplayData] = useState<DataPoint[]>([]);
// 帧率控制
const lastUpdateTimeRef = useRef<number>(0);
const pendingUpdateRef = useRef<boolean>(false);
const rafIdRef = useRef<number | null>(null);
// 根据时间范围计算保留的数据点数量 // 根据时间范围计算保留的数据点数量
const getMaxPointsByTimeRange = useCallback( const getMaxPointsByTimeRange = useCallback(
(minutes: TimeRange): number => minutes * 60, // 每分钟60个点(每秒1个点) (minutes: TimeRange): number => {
// 使用更低的采样率来减少点的数量每2秒一个点而不是每秒一个点
return minutes * 30; // 每分钟30个点(每2秒1个点)
},
[], [],
); );
@ -65,21 +87,6 @@ export const EnhancedTrafficGraph = forwardRef<EnhancedTrafficGraphRef>(
// 图表样式line 或 area // 图表样式line 或 area
const [chartStyle, setChartStyle] = useState<"line" | "area">("area"); const [chartStyle, setChartStyle] = useState<"line" | "area">("area");
// 创建一个明确的类型
type DataPoint = ITrafficItem & { name: string; timestamp: number };
// 完整数据缓冲区 - 保存10分钟的数据
const [dataBuffer, setDataBuffer] = useState<DataPoint[]>([]);
// 当前显示的数据点 - 根据选定的时间范围从缓冲区过滤
const dataPoints = useMemo(() => {
if (dataBuffer.length === 0) return [];
// 根据当前时间范围计算需要显示的点数
const pointsToShow = getMaxPointsByTimeRange(timeRange);
// 从缓冲区中获取最新的数据点
return dataBuffer.slice(-pointsToShow);
}, [dataBuffer, timeRange, getMaxPointsByTimeRange]);
// 颜色配置 // 颜色配置
const colors = useMemo( const colors = useMemo(
() => ({ () => ({
@ -108,7 +115,7 @@ export const EnhancedTrafficGraph = forwardRef<EnhancedTrafficGraphRef>(
const now = Date.now(); const now = Date.now();
const tenMinutesAgo = now - 10 * 60 * 1000; const tenMinutesAgo = now - 10 * 60 * 1000;
// 创建600个点作为初始缓冲区 // 创建初始缓冲区,降低点的密度
const initialBuffer: DataPoint[] = Array.from( const initialBuffer: DataPoint[] = Array.from(
{ length: MAX_BUFFER_SIZE }, { length: MAX_BUFFER_SIZE },
(_, index) => { (_, index) => {
@ -131,39 +138,91 @@ export const EnhancedTrafficGraph = forwardRef<EnhancedTrafficGraphRef>(
}, },
); );
setDataBuffer(initialBuffer); dataBufferRef.current = initialBuffer;
setDisplayData(initialBuffer);
// 清理函数,取消任何未完成的动画帧
return () => {
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current);
rafIdRef.current = null;
}
};
}, [MAX_BUFFER_SIZE]); }, [MAX_BUFFER_SIZE]);
// 处理数据更新并控制帧率的函数
const updateDisplayData = useCallback(() => {
if (pendingUpdateRef.current) {
pendingUpdateRef.current = false;
// 根据当前时间范围计算需要显示的点数
const pointsToShow = getMaxPointsByTimeRange(timeRange);
// 从缓冲区中获取最新的数据点
const newDisplayData = dataBufferRef.current.slice(-pointsToShow);
setDisplayData(newDisplayData);
}
rafIdRef.current = null;
}, [timeRange, getMaxPointsByTimeRange]);
// 节流更新函数
const throttledUpdateData = useCallback(() => {
pendingUpdateRef.current = true;
const now = performance.now();
const timeSinceLastUpdate = now - lastUpdateTimeRef.current;
if (rafIdRef.current === null) {
if (timeSinceLastUpdate >= FRAME_MIN_TIME) {
// 如果距离上次更新已经超过最小帧时间,立即更新
lastUpdateTimeRef.current = now;
rafIdRef.current = requestAnimationFrame(updateDisplayData);
} else {
// 否则,在适当的时间进行更新
const timeToWait = FRAME_MIN_TIME - timeSinceLastUpdate;
setTimeout(() => {
lastUpdateTimeRef.current = performance.now();
rafIdRef.current = requestAnimationFrame(updateDisplayData);
}, timeToWait);
}
}
}, [updateDisplayData]);
// 监听时间范围变化,更新显示数据
useEffect(() => {
throttledUpdateData();
}, [timeRange, throttledUpdateData]);
// 添加数据点方法 // 添加数据点方法
const appendData = useCallback((data: ITrafficItem) => { const appendData = useCallback((data: ITrafficItem) => {
// 安全处理数据 // 安全处理数据
const safeData = { const safeData = {
up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0, up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0,
down: down: typeof data.down === "number" && !isNaN(data.down) ? data.down : 0,
typeof data.down === "number" && !isNaN(data.down) ? data.down : 0,
}; };
setDataBuffer((prev) => { // 使用提供的时间戳或当前时间
// 使用提供的时间戳或当前时间 const timestamp = data.timestamp || Date.now();
const timestamp = data.timestamp || Date.now(); const date = new Date(timestamp);
const date = new Date(timestamp);
// 带时间标签的新数据点 // 带时间标签的新数据点
const newPoint: DataPoint = { const newPoint: DataPoint = {
...safeData, ...safeData,
name: date.toLocaleTimeString("en-US", { name: date.toLocaleTimeString("en-US", {
hour12: false, hour12: false,
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
second: "2-digit", second: "2-digit",
}), }),
timestamp: timestamp, timestamp: timestamp,
}; };
// 更新缓冲区,保持最大长度 // 直接更新ref不触发重渲染
return [...prev.slice(1), newPoint]; dataBufferRef.current = [...dataBufferRef.current.slice(1), newPoint];
});
}, []); // 使用节流更新显示数据
throttledUpdateData();
}, [throttledUpdateData]);
// 切换图表样式 // 切换图表样式
const toggleStyle = useCallback(() => { const toggleStyle = useCallback(() => {
@ -181,16 +240,16 @@ export const EnhancedTrafficGraph = forwardRef<EnhancedTrafficGraphRef>(
); );
// 格式化工具提示内容 // 格式化工具提示内容
const formatTooltip = (value: number) => { const formatTooltip = useCallback((value: number) => {
const [num, unit] = parseTraffic(value); const [num, unit] = parseTraffic(value);
return [`${num} ${unit}/s`, ""]; return [`${num} ${unit}/s`, ""];
}; }, []);
// Y轴刻度格式化 // Y轴刻度格式化
const formatYAxis = (value: number) => { const formatYAxis = useCallback((value: number) => {
const [num, unit] = parseTraffic(value); const [num, unit] = parseTraffic(value);
return `${num}${unit}`; return `${num}${unit}`;
}; }, []);
// 格式化X轴标签 // 格式化X轴标签
const formatXLabel = useCallback((value: string) => { const formatXLabel = useCallback((value: string) => {
@ -206,7 +265,7 @@ export const EnhancedTrafficGraph = forwardRef<EnhancedTrafficGraphRef>(
}, [timeRange, t]); }, [timeRange, t]);
// 渲染图表内的标签 // 渲染图表内的标签
const renderInnerLabels = () => ( const renderInnerLabels = useCallback(() => (
<> <>
{/* 上传标签 - 右上角 */} {/* 上传标签 - 右上角 */}
<text <text
@ -232,19 +291,19 @@ export const EnhancedTrafficGraph = forwardRef<EnhancedTrafficGraphRef>(
{t("Download")} {t("Download")}
</text> </text>
</> </>
); ), [colors.up, colors.down, t]);
// 共享图表配置 // 共享图表配置
const commonProps = { const commonProps = useMemo(() => ({
data: dataPoints, data: displayData,
margin: { top: 10, right: 20, left: 0, bottom: 0 }, margin: { top: 10, right: 20, left: 0, bottom: 0 },
}; }), [displayData]);
// 曲线类型 - 使用平滑曲线 // 曲线类型 - 使用平滑曲线
const curveType = "basis"; const curveType = "basis";
// 共享图表子组件 // 共享图表子组件
const commonChildren = ( const commonChildren = useMemo(() => (
<> <>
<CartesianGrid <CartesianGrid
strokeDasharray="3 3" strokeDasharray="3 3"
@ -303,16 +362,17 @@ export const EnhancedTrafficGraph = forwardRef<EnhancedTrafficGraphRef>(
</text> </text>
</g> </g>
</> </>
); ), [colors, formatXLabel, formatYAxis, formatTooltip, timeRange, theme.palette.text.secondary, handleTimeRangeClick, getTimeRangeText, t]);
// 渲染图表 - 线图或面积图 // 渲染图表 - 线图或面积图
const renderChart = () => { const renderChart = useCallback(() => {
// 共享的线条/区域配置 // 共享的线条/区域配置
const commonLineProps = { const commonLineProps = {
dot: false, dot: false,
strokeWidth: 2, strokeWidth: 2,
connectNulls: false, connectNulls: false,
activeDot: { r: 4, strokeWidth: 1 }, activeDot: { r: 4, strokeWidth: 1 },
isAnimationActive: false, // 禁用动画以减少CPU使用
}; };
return chartStyle === "line" ? ( return chartStyle === "line" ? (
@ -358,7 +418,7 @@ export const EnhancedTrafficGraph = forwardRef<EnhancedTrafficGraphRef>(
{renderInnerLabels()} {renderInnerLabels()}
</AreaChart> </AreaChart>
); );
}; }, [chartStyle, commonProps, commonChildren, renderInnerLabels, colors, t]);
return ( return (
<Box <Box
@ -379,4 +439,7 @@ export const EnhancedTrafficGraph = forwardRef<EnhancedTrafficGraphRef>(
</Box> </Box>
); );
}, },
); ));
// 添加显示名称以便调试
EnhancedTrafficGraph.displayName = "EnhancedTrafficGraph";

View File

@ -10,7 +10,7 @@ export const useCurrentProxy = () => {
"getProxies", "getProxies",
getProxies, getProxies,
{ {
refreshInterval: 3000, refreshInterval: 2000,
revalidateOnFocus: false, revalidateOnFocus: false,
revalidateOnReconnect: true, revalidateOnReconnect: true,
}, },