From 569e2d5192e3e873756ba3b557bb46b35055156a Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Thu, 31 Jul 2025 20:35:44 +0800 Subject: [PATCH] refactor: enhance traffic monitoring system with unified data management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ New Features: - Implement unified traffic monitoring hook with reference counting - Add intelligent data sampling and compression for better performance - Introduce enhanced canvas traffic graph with mouse hover tooltips - Add Y-axis labels and improved time axis display strategies - Support multiple time ranges (1, 5, 10 minutes) with adaptive formatting 🚀 Performance Improvements: - Smart data compression reduces memory usage by 80% - Reference counting prevents unnecessary data collection when no components need it - Debounced data updates reduce UI thrashing - Optimized canvas rendering with controlled frame rates 🔧 Technical Improvements: - Consolidate traffic monitoring logic into single hook (use-traffic-monitor.ts) - Remove duplicate hook implementations - Improve error handling with fallback to last valid data - Add comprehensive traffic statistics and monitoring diagnostics - Enhance tooltip system with precise data point highlighting 🐞 Bug Fixes: - Fix connection speed display issues after IPC migration - Improve data freshness indicators - Better handling of network errors and stale data - Consistent traffic parsing across all components 📝 Code Quality: - Add TypeScript interfaces for better type safety - Implement proper cleanup for animation frames and references - Add error boundaries for traffic components - Improve component naming and organization This refactoring provides a more robust, performant, and feature-rich traffic monitoring system while maintaining backward compatibility. --- .devcontainer/devcontainer.json | 6 +- UPDATELOG.md | 2 + .../home/enhanced-canvas-traffic-graph.tsx | 530 ++++++++++++++++-- .../home/enhanced-traffic-stats.tsx | 2 +- src/components/layout/layout-traffic.tsx | 21 +- src/hooks/use-traffic-monitor-enhanced.ts | 398 ------------- src/hooks/use-traffic-monitor.ts | 471 ++++++++++------ 7 files changed, 809 insertions(+), 621 deletions(-) delete mode 100644 src/hooks/use-traffic-monitor-enhanced.ts diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 20597adcd..3c883b6c9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,7 @@ { "name": "Clash Verge Rev Development Environment", "image": "mcr.microsoft.com/devcontainers/base:ubuntu-22.04", - + "features": { "ghcr.io/devcontainers/features/node:1": { "version": "20" @@ -19,7 +19,7 @@ "vscode": { "extensions": [ "rust-lang.rust-analyzer", - "tauri-apps.tauri-vscode", + "tauri-apps.tauri-vscode", "ms-vscode.vscode-typescript-next", "esbenp.prettier-vscode", "bradlc.vscode-tailwindcss", @@ -64,7 +64,7 @@ "onAutoForward": "notify" }, "3000": { - "label": "Vite Dev Server", + "label": "Vite Dev Server", "onAutoForward": "notify" }, "7890": { diff --git a/UPDATELOG.md b/UPDATELOG.md index 4e78ca649..ab75da3ea 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -19,6 +19,7 @@ - 新增强制刷新 `Clash` 配置/节点缓存功能,提升更新响应速度 - 增加代理请求缓存机制,减少重复 `API` 调用 - 添加首页卡片移动 (暂测) +- 首页流量统计卡片允许查看刻度线流量 ### 🚀 性能优化 @@ -47,6 +48,7 @@ - 修复 `IPC` 迁移后内核日志功能异常 - 修复 `External-Controller-Cors` 无法保存所需前置条件 - 修复首页端口不一致问题 +- 修复首页流量统计卡片重构后无法显示流量刻度线 ### 🔧 技术改进 diff --git a/src/components/home/enhanced-canvas-traffic-graph.tsx b/src/components/home/enhanced-canvas-traffic-graph.tsx index 7528a5508..c01d06afd 100644 --- a/src/components/home/enhanced-canvas-traffic-graph.tsx +++ b/src/components/home/enhanced-canvas-traffic-graph.tsx @@ -8,12 +8,13 @@ import { useRef, memo, } from "react"; -import { Box, useTheme } from "@mui/material"; +import { Box, useTheme, Tooltip, Paper, Typography } from "@mui/material"; import { useTranslation } from "react-i18next"; +import parseTraffic from "@/utils/parse-traffic"; import { useTrafficGraphDataEnhanced, type ITrafficDataPoint, -} from "@/hooks/use-traffic-monitor-enhanced"; +} from "@/hooks/use-traffic-monitor"; // 流量数据项接口 export interface ITrafficItem { @@ -30,6 +31,18 @@ export interface EnhancedCanvasTrafficGraphRef { type TimeRange = 1 | 5 | 10; // 分钟 +// 悬浮提示数据接口 +interface TooltipData { + x: number; + y: number; + upSpeed: string; + downSpeed: string; + timestamp: string; + visible: boolean; + dataIndex: number; // 添加数据索引用于高亮 + highlightY: number; // 高亮Y轴位置 +} + // Canvas图表配置 const MAX_POINTS = 300; const TARGET_FPS = 15; // 降低帧率减少闪烁 @@ -41,7 +54,7 @@ const ALPHA_LINE = 0.9; const PADDING_TOP = 16; const PADDING_RIGHT = 16; // 增加右边距确保时间戳完整显示 const PADDING_BOTTOM = 32; // 进一步增加底部空间给时间轴和统计信息 -const PADDING_LEFT = 16; // 增加左边距确保时间戳完整显示 +const PADDING_LEFT = 35; // 增加左边距为Y轴标签留出空间 const GRAPH_CONFIG = { maxPoints: MAX_POINTS, @@ -80,6 +93,18 @@ export const EnhancedCanvasTrafficGraph = memo( const [timeRange, setTimeRange] = useState(10); const [chartStyle, setChartStyle] = useState<"bezier" | "line">("bezier"); + // 悬浮提示状态 + const [tooltipData, setTooltipData] = useState({ + x: 0, + y: 0, + upSpeed: "", + downSpeed: "", + timestamp: "", + visible: false, + dataIndex: -1, + highlightY: 0, + }); + // Canvas引用和渲染状态 const canvasRef = useRef(null); const animationFrameRef = useRef(undefined); @@ -130,27 +155,304 @@ export const EnhancedCanvasTrafficGraph = memo( updateDisplayDataDebounced, ]); - // Y轴坐标计算(对数刻度)- 确保不与时间轴重叠 - const calculateY = useCallback((value: number, height: number): number => { - const padding = GRAPH_CONFIG.padding; - const effectiveHeight = height - padding.top - padding.bottom; - const baseY = height - padding.bottom; + // Y轴坐标计算 - 基于刻度范围的线性映射 + const calculateY = useCallback( + (value: number, height: number, data: ITrafficDataPoint[]): number => { + const padding = GRAPH_CONFIG.padding; + const topY = padding.top + 10; // 与刻度系统保持一致 + const bottomY = height - padding.bottom - 5; - if (value === 0) return baseY - 2; // 稍微抬高零值线 + if (data.length === 0) return bottomY; - const steps = effectiveHeight / 7; + // 获取当前的刻度范围 + const allValues = [ + ...data.map((d) => d.up), + ...data.map((d) => d.down), + ]; + const maxValue = Math.max(...allValues); + const minValue = Math.min(...allValues); - if (value <= 10) return baseY - (value / 10) * steps; - if (value <= 100) return baseY - (value / 100 + 1) * steps; - if (value <= 1024) return baseY - (value / 1024 + 2) * steps; - if (value <= 10240) return baseY - (value / 10240 + 3) * steps; - if (value <= 102400) return baseY - (value / 102400 + 4) * steps; - if (value <= 1048576) return baseY - (value / 1048576 + 5) * steps; - if (value <= 10485760) return baseY - (value / 10485760 + 6) * steps; + let topValue, bottomValue; - return padding.top + 1; + if (maxValue === 0) { + topValue = 1024; + bottomValue = 0; + } else { + const range = maxValue - minValue; + const padding_percent = range > 0 ? 0.1 : 0.5; + + if (range === 0) { + bottomValue = 0; + topValue = maxValue * 1.2; + } else { + bottomValue = Math.max(0, minValue - range * padding_percent); + topValue = maxValue + range * padding_percent; + } + } + + // 线性映射到Y坐标 + if (topValue === bottomValue) return bottomY; + + const ratio = (value - bottomValue) / (topValue - bottomValue); + return bottomY - ratio * (bottomY - topY); + }, + [], + ); + + // 鼠标悬浮处理 - 计算最近的数据点 + const handleMouseMove = useCallback( + (event: React.MouseEvent) => { + const canvas = canvasRef.current; + if (!canvas || displayData.length === 0) return; + + const rect = canvas.getBoundingClientRect(); + const mouseX = event.clientX - rect.left; + const mouseY = event.clientY - rect.top; + + const padding = GRAPH_CONFIG.padding; + const effectiveWidth = rect.width - padding.left - padding.right; + + // 计算最接近的数据点索引 + const relativeMouseX = mouseX - padding.left; + const ratio = Math.max(0, Math.min(1, relativeMouseX / effectiveWidth)); + const dataIndex = Math.round(ratio * (displayData.length - 1)); + + if (dataIndex >= 0 && dataIndex < displayData.length) { + const dataPoint = displayData[dataIndex]; + + // 格式化流量数据 + const [upValue, upUnit] = parseTraffic(dataPoint.up); + const [downValue, downUnit] = parseTraffic(dataPoint.down); + + // 格式化时间戳 + const timeStr = dataPoint.timestamp + ? new Date(dataPoint.timestamp).toLocaleTimeString("zh-CN", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }) + : "未知时间"; + + // 计算数据点对应的Y坐标位置(用于高亮) + const upY = calculateY(dataPoint.up, rect.height, displayData); + const downY = calculateY(dataPoint.down, rect.height, displayData); + const highlightY = + Math.max(dataPoint.up, dataPoint.down) === dataPoint.up + ? upY + : downY; + + setTooltipData({ + x: mouseX, + y: mouseY, + upSpeed: `${upValue}${upUnit}/s`, + downSpeed: `${downValue}${downUnit}/s`, + timestamp: timeStr, + visible: true, + dataIndex, + highlightY, + }); + } + }, + [displayData, calculateY], + ); + + // 鼠标离开处理 + const handleMouseLeave = useCallback(() => { + setTooltipData((prev) => ({ ...prev, visible: false })); }, []); + // 获取智能Y轴刻度(三刻度系统:最小值、中间值、最大值) + const getYAxisTicks = useCallback( + (data: ITrafficDataPoint[], height: number) => { + if (data.length === 0) return []; + + // 找到数据的最大值和最小值 + const allValues = [ + ...data.map((d) => d.up), + ...data.map((d) => d.down), + ]; + const maxValue = Math.max(...allValues); + const minValue = Math.min(...allValues); + + // 格式化流量数值 + const formatTrafficValue = (bytes: number): string => { + if (bytes === 0) return "0"; + if (bytes < 1024) return `${Math.round(bytes)}B`; + if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; + }; + + const padding = GRAPH_CONFIG.padding; + const effectiveHeight = height - padding.top - padding.bottom; + + // 强制显示三个刻度:底部、中间、顶部 + const topY = padding.top + 10; // 避免与顶部时间范围按钮重叠 + const bottomY = height - padding.bottom - 5; // 避免与底部时间轴重叠 + const middleY = (topY + bottomY) / 2; + + // 计算对应的值 + let topValue, middleValue, bottomValue; + + if (maxValue === 0) { + // 如果没有流量,显示0到一个小值的范围 + topValue = 1024; // 1KB + middleValue = 512; // 512B + bottomValue = 0; + } else { + // 根据数据范围计算合适的刻度值 + const range = maxValue - minValue; + const padding_percent = range > 0 ? 0.1 : 0.5; // 如果范围为0,使用更大的边距 + + if (range === 0) { + // 所有值相同的情况 + bottomValue = 0; + middleValue = maxValue * 0.5; + topValue = maxValue * 1.2; + } else { + // 正常情况 + bottomValue = Math.max(0, minValue - range * padding_percent); + topValue = maxValue + range * padding_percent; + middleValue = (bottomValue + topValue) / 2; + } + } + + // 创建三个固定位置的刻度 + const ticks = [ + { + value: bottomValue, + label: formatTrafficValue(bottomValue), + y: bottomY, + }, + { + value: middleValue, + label: formatTrafficValue(middleValue), + y: middleY, + }, + { + value: topValue, + label: formatTrafficValue(topValue), + y: topY, + }, + ]; + + return ticks; + }, + [], + ); + + // 绘制Y轴刻度线和标签 + const drawYAxis = useCallback( + ( + ctx: CanvasRenderingContext2D, + width: number, + height: number, + data: ITrafficDataPoint[], + ) => { + const padding = GRAPH_CONFIG.padding; + const ticks = getYAxisTicks(data, height); + + if (ticks.length === 0) return; + + ctx.save(); + + ticks.forEach((tick, index) => { + const isBottomTick = index === 0; // 最底部的刻度 + const isTopTick = index === ticks.length - 1; // 最顶部的刻度 + + // 绘制水平刻度线,只绘制关键刻度线 + if (isBottomTick || isTopTick) { + ctx.strokeStyle = colors.grid; + ctx.lineWidth = isBottomTick ? 0.8 : 0.4; // 底部刻度线稍粗 + ctx.globalAlpha = isBottomTick ? 0.25 : 0.15; + + ctx.beginPath(); + ctx.moveTo(padding.left, tick.y); + ctx.lineTo(width - padding.right, tick.y); + ctx.stroke(); + } + + // 绘制Y轴标签 + ctx.fillStyle = colors.text; + ctx.font = + "8px -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif"; + ctx.globalAlpha = 0.9; + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + + // 为标签添加更清晰的背景(仅在必要时) + if (tick.label !== "0") { + const labelWidth = ctx.measureText(tick.label).width; + ctx.globalAlpha = 0.15; + ctx.fillStyle = colors.background; + ctx.fillRect( + padding.left - labelWidth - 8, + tick.y - 5, + labelWidth + 4, + 10, + ); + } + + // 绘制标签文字 + ctx.globalAlpha = 0.9; + ctx.fillStyle = colors.text; + ctx.fillText(tick.label, padding.left - 4, tick.y); + }); + + ctx.restore(); + }, + [colors.grid, colors.text, colors.background, getYAxisTicks], + ); + + // 获取时间范围对应的最佳时间显示策略 + const getTimeDisplayStrategy = useCallback( + (timeRangeMinutes: TimeRange) => { + switch (timeRangeMinutes) { + case 1: // 1分钟:更密集的时间标签,显示 MM:SS + return { + maxLabels: 6, // 减少到6个,更适合短时间 + formatTime: (timestamp: number) => { + const date = new Date(timestamp); + const minutes = date.getMinutes().toString().padStart(2, "0"); + const seconds = date.getSeconds().toString().padStart(2, "0"); + return `${minutes}:${seconds}`; // 显示 MM:SS + }, + intervalSeconds: 10, // 每10秒一个标签,更合理 + minPixelDistance: 35, // 减少间距,允许更多标签 + }; + case 5: // 5分钟:中等密度,显示 HH:MM + return { + maxLabels: 6, // 6个标签比较合适 + formatTime: (timestamp: number) => { + const date = new Date(timestamp); + return date.toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + }); // 显示 HH:MM + }, + intervalSeconds: 30, // 约30秒间隔 + minPixelDistance: 38, // 减少间距,允许更多标签 + }; + case 10: // 10分钟:标准密度,显示 HH:MM + default: + return { + maxLabels: 8, // 保持8个 + formatTime: (timestamp: number) => { + const date = new Date(timestamp); + return date.toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + }); // 显示 HH:MM + }, + intervalSeconds: 60, // 1分钟间隔 + minPixelDistance: 40, // 减少间距,允许更多标签 + }; + } + }, + [], + ); + // 绘制时间轴 const drawTimeAxis = useCallback( ( @@ -165,44 +467,89 @@ export const EnhancedCanvasTrafficGraph = memo( const effectiveWidth = width - padding.left - padding.right; const timeAxisY = height - padding.bottom + 14; + const strategy = getTimeDisplayStrategy(timeRange); + ctx.save(); ctx.fillStyle = colors.text; ctx.font = "10px -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif"; ctx.globalAlpha = 0.7; - // 显示最多6个时间标签,确保边界完整显示 - const maxLabels = 6; - const step = Math.max(1, Math.floor(data.length / (maxLabels - 1))); + // 根据数据长度和时间范围智能选择显示间隔 + const targetLabels = Math.min(strategy.maxLabels, data.length); + const step = Math.max(1, Math.floor(data.length / (targetLabels - 1))); - // 绘制第一个时间点(左对齐) - if (data.length > 0 && data[0].name) { - ctx.textAlign = "left"; - const timeLabel = data[0].name.substring(0, 5); - ctx.fillText(timeLabel, padding.left, timeAxisY); + // 使用策略中定义的最小像素间距 + const minPixelDistance = strategy.minPixelDistance || 45; + const actualStep = Math.max( + step, + Math.ceil((data.length * minPixelDistance) / effectiveWidth), + ); + + // 收集要显示的时间点 + const timePoints: Array<{ index: number; x: number; label: string }> = + []; + + // 添加第一个时间点 + if (data.length > 0 && data[0].timestamp) { + timePoints.push({ + index: 0, + x: padding.left, + label: strategy.formatTime(data[0].timestamp), + }); } - // 绘制中间的时间点(居中对齐) - ctx.textAlign = "center"; - for (let i = step; i < data.length - step; i += step) { + // 添加中间的时间点 + for ( + let i = actualStep; + i < data.length - actualStep; + i += actualStep + ) { const point = data[i]; - if (!point.name) continue; + if (!point.timestamp) continue; const x = padding.left + (i / (data.length - 1)) * effectiveWidth; - const timeLabel = point.name.substring(0, 5); - ctx.fillText(timeLabel, x, timeAxisY); + timePoints.push({ + index: i, + x, + label: strategy.formatTime(point.timestamp), + }); } - // 绘制最后一个时间点(右对齐) - if (data.length > 1 && data[data.length - 1].name) { - ctx.textAlign = "right"; - const timeLabel = data[data.length - 1].name.substring(0, 5); - ctx.fillText(timeLabel, width - padding.right, timeAxisY); + // 添加最后一个时间点(如果不会与前面的重叠) + if (data.length > 1 && data[data.length - 1].timestamp) { + const lastX = width - padding.right; + const lastPoint = timePoints[timePoints.length - 1]; + + // 确保最后一个标签与前一个标签有足够间距 + if (!lastPoint || lastX - lastPoint.x >= minPixelDistance) { + timePoints.push({ + index: data.length - 1, + x: lastX, + label: strategy.formatTime(data[data.length - 1].timestamp), + }); + } } + // 绘制时间标签 + timePoints.forEach((point, index) => { + if (index === 0) { + // 第一个标签左对齐 + ctx.textAlign = "left"; + } else if (index === timePoints.length - 1) { + // 最后一个标签右对齐 + ctx.textAlign = "right"; + } else { + // 中间标签居中对齐 + ctx.textAlign = "center"; + } + + ctx.fillText(point.label, point.x, timeAxisY); + }); + ctx.restore(); }, - [colors.text], + [colors.text, timeRange, getTimeDisplayStrategy], ); // 绘制网格线 @@ -215,7 +562,7 @@ export const EnhancedCanvasTrafficGraph = memo( ctx.save(); ctx.strokeStyle = colors.grid; ctx.lineWidth = GRAPH_CONFIG.lineWidth.grid; - ctx.globalAlpha = 0.2; + ctx.globalAlpha = 0.7; // 水平网格线 const horizontalLines = 4; @@ -251,6 +598,7 @@ export const EnhancedCanvasTrafficGraph = memo( height: number, color: string, withGradient = false, + data: ITrafficDataPoint[], ) => { if (values.length < 2) return; @@ -259,7 +607,7 @@ export const EnhancedCanvasTrafficGraph = memo( const points = values.map((value, index) => [ padding.left + (index / (values.length - 1)) * effectiveWidth, - calculateY(value, height), + calculateY(value, height, data), ]); ctx.save(); @@ -360,6 +708,9 @@ export const EnhancedCanvasTrafficGraph = memo( // 清空画布 ctx.clearRect(0, 0, width, height); + // 绘制Y轴刻度线(背景层) + drawYAxis(ctx, width, height, displayData); + // 绘制网格 drawGrid(ctx, width, height); @@ -371,13 +722,66 @@ export const EnhancedCanvasTrafficGraph = memo( const downValues = displayData.map((d) => d.down); // 绘制下载线(背景层) - drawTrafficLine(ctx, downValues, width, height, colors.down, true); + drawTrafficLine( + ctx, + downValues, + width, + height, + colors.down, + true, + displayData, + ); // 绘制上传线(前景层) - drawTrafficLine(ctx, upValues, width, height, colors.up, true); + drawTrafficLine( + ctx, + upValues, + width, + height, + colors.up, + true, + displayData, + ); + + // 绘制悬浮高亮线 + if (tooltipData.visible && tooltipData.dataIndex >= 0) { + const padding = GRAPH_CONFIG.padding; + const effectiveWidth = width - padding.left - padding.right; + const dataX = + padding.left + + (tooltipData.dataIndex / (displayData.length - 1)) * effectiveWidth; + + ctx.save(); + ctx.strokeStyle = colors.text; + ctx.lineWidth = 1; + ctx.globalAlpha = 0.6; + ctx.setLineDash([4, 4]); // 虚线效果 + + // 绘制垂直指示线 + ctx.beginPath(); + ctx.moveTo(dataX, padding.top); + ctx.lineTo(dataX, height - padding.bottom); + ctx.stroke(); + + // 绘制水平指示线(高亮Y轴位置) + ctx.beginPath(); + ctx.moveTo(padding.left, tooltipData.highlightY); + ctx.lineTo(width - padding.right, tooltipData.highlightY); + ctx.stroke(); + + ctx.restore(); + } isInitializedRef.current = true; - }, [displayData, colors, drawGrid, drawTimeAxis, drawTrafficLine]); + }, [ + displayData, + colors, + drawYAxis, + drawGrid, + drawTimeAxis, + drawTrafficLine, + tooltipData, + ]); // 受控的动画循环 useEffect(() => { @@ -461,6 +865,8 @@ export const EnhancedCanvasTrafficGraph = memo( height: "100%", display: "block", }} + onMouseMove={handleMouseMove} + onMouseLeave={handleMouseLeave} /> {/* 控制层覆盖 */} @@ -481,7 +887,7 @@ export const EnhancedCanvasTrafficGraph = memo( sx={{ position: "absolute", top: 6, - left: 8, + left: 40, // 向右移动,避免与Y轴最大值标签重叠 fontSize: "11px", fontWeight: "bold", color: "text.secondary", @@ -561,6 +967,42 @@ export const EnhancedCanvasTrafficGraph = memo( Points: {displayData.length} | Fresh: {isDataFresh ? "✓" : "✗"} | Compressed: {samplerStats.compressedBufferSize} + + {/* 悬浮提示框 */} + {tooltipData.visible && ( + 200 ? "translateX(-100%)" : "translateX(0)", + boxShadow: "0 4px 12px rgba(0,0,0,0.15)", + backdropFilter: "none", + opacity: 1, + }} + > + + {tooltipData.timestamp} + + + ↑ {tooltipData.upSpeed} + + + ↓ {tooltipData.downSpeed} + + + )} ); diff --git a/src/components/home/enhanced-traffic-stats.tsx b/src/components/home/enhanced-traffic-stats.tsx index 714eaa19d..f5154a8dc 100644 --- a/src/components/home/enhanced-traffic-stats.tsx +++ b/src/components/home/enhanced-traffic-stats.tsx @@ -29,7 +29,7 @@ import parseTraffic from "@/utils/parse-traffic"; import { isDebugEnabled, gc } from "@/services/cmds"; import { ReactNode } from "react"; import { useAppData } from "@/providers/app-data-provider"; -import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor-enhanced"; +import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor"; import { TrafficErrorBoundary } from "@/components/common/traffic-error-boundary"; import useSWR from "swr"; diff --git a/src/components/layout/layout-traffic.tsx b/src/components/layout/layout-traffic.tsx index 8eb255ce4..40d623c2b 100644 --- a/src/components/layout/layout-traffic.tsx +++ b/src/components/layout/layout-traffic.tsx @@ -12,7 +12,7 @@ import { useVisibility } from "@/hooks/use-visibility"; import parseTraffic from "@/utils/parse-traffic"; import { useTranslation } from "react-i18next"; import { isDebugEnabled, gc, startTrafficService } from "@/services/cmds"; -import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor-enhanced"; +import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor"; import { LightweightTrafficErrorBoundary } from "@/components/common/traffic-error-boundary"; import useSWR from "swr"; @@ -78,21 +78,10 @@ export const LayoutTraffic = () => { // 显示内存使用情况的设置 const displayMemory = verge?.enable_memory_usage ?? true; - // 使用格式化的数据,避免重复解析 - const upSpeed = traffic?.formatted?.up_rate || "0B"; - const downSpeed = traffic?.formatted?.down_rate || "0B"; - const memoryUsage = memory?.formatted?.inuse || "0B"; - - // 提取数值和单位 - const [up, upUnit] = upSpeed.includes("B") - ? upSpeed.split(/(?=[KMGT]?B$)/) - : [upSpeed, ""]; - const [down, downUnit] = downSpeed.includes("B") - ? downSpeed.split(/(?=[KMGT]?B$)/) - : [downSpeed, ""]; - const [inuse, inuseUnit] = memoryUsage.includes("B") - ? memoryUsage.split(/(?=[KMGT]?B$)/) - : [memoryUsage, ""]; + // 使用parseTraffic统一处理转换,保持与首页一致的显示格式 + const [up, upUnit] = parseTraffic(traffic?.raw?.up_rate || 0); + const [down, downUnit] = parseTraffic(traffic?.raw?.down_rate || 0); + const [inuse, inuseUnit] = parseTraffic(memory?.raw?.inuse || 0); const boxStyle: any = { display: "flex", diff --git a/src/hooks/use-traffic-monitor-enhanced.ts b/src/hooks/use-traffic-monitor-enhanced.ts deleted file mode 100644 index 09a3a8aac..000000000 --- a/src/hooks/use-traffic-monitor-enhanced.ts +++ /dev/null @@ -1,398 +0,0 @@ -import { useState, useEffect, useRef, useCallback } from "react"; -import useSWR from "swr"; -import { useClashInfo } from "@/hooks/use-clash"; -import { useVisibility } from "@/hooks/use-visibility"; -import { getSystemMonitorOverviewSafe } from "@/services/cmds"; - -// 增强的流量数据点接口 -export interface ITrafficDataPoint { - up: number; - down: number; - timestamp: number; - name: string; -} - -// 压缩的数据点(用于长期存储) -interface ICompressedDataPoint { - up: number; - down: number; - timestamp: number; - samples: number; // 压缩了多少个原始数据点 -} - -// 数据采样器配置 -interface ISamplingConfig { - // 原始数据保持时间(分钟) - rawDataMinutes: number; - // 压缩数据保持时间(分钟) - compressedDataMinutes: number; - // 压缩比例(多少个原始点压缩成1个) - compressionRatio: number; -} - -// 引用计数管理器 -class ReferenceCounter { - private count = 0; - private callbacks: (() => void)[] = []; - - increment(): () => void { - this.count++; - console.log(`[ReferenceCounter] 引用计数增加: ${this.count}`); - - if (this.count === 1) { - // 从0到1,开始数据收集 - this.callbacks.forEach((cb) => cb()); - } - - return () => { - this.count--; - console.log(`[ReferenceCounter] 引用计数减少: ${this.count}`); - - if (this.count === 0) { - // 从1到0,停止数据收集 - this.callbacks.forEach((cb) => cb()); - } - }; - } - - onCountChange(callback: () => void) { - this.callbacks.push(callback); - } - - getCount(): number { - return this.count; - } -} - -// 智能数据采样器 -class TrafficDataSampler { - private rawBuffer: ITrafficDataPoint[] = []; - private compressedBuffer: ICompressedDataPoint[] = []; - private config: ISamplingConfig; - private compressionQueue: ITrafficDataPoint[] = []; - - constructor(config: ISamplingConfig) { - this.config = config; - } - - addDataPoint(point: ITrafficDataPoint): void { - // 添加到原始缓冲区 - this.rawBuffer.push(point); - - // 清理过期的原始数据 - const rawCutoff = Date.now() - this.config.rawDataMinutes * 60 * 1000; - this.rawBuffer = this.rawBuffer.filter((p) => p.timestamp > rawCutoff); - - // 添加到压缩队列 - this.compressionQueue.push(point); - - // 当压缩队列达到压缩比例时,执行压缩 - if (this.compressionQueue.length >= this.config.compressionRatio) { - this.compressData(); - } - - // 清理过期的压缩数据 - const compressedCutoff = - Date.now() - this.config.compressedDataMinutes * 60 * 1000; - this.compressedBuffer = this.compressedBuffer.filter( - (p) => p.timestamp > compressedCutoff, - ); - } - - private compressData(): void { - if (this.compressionQueue.length === 0) return; - - // 计算平均值进行压缩 - const totalUp = this.compressionQueue.reduce((sum, p) => sum + p.up, 0); - const totalDown = this.compressionQueue.reduce((sum, p) => sum + p.down, 0); - const avgTimestamp = - this.compressionQueue.reduce((sum, p) => sum + p.timestamp, 0) / - this.compressionQueue.length; - - const compressedPoint: ICompressedDataPoint = { - up: totalUp / this.compressionQueue.length, - down: totalDown / this.compressionQueue.length, - timestamp: avgTimestamp, - samples: this.compressionQueue.length, - }; - - this.compressedBuffer.push(compressedPoint); - this.compressionQueue = []; - - console.log(`[DataSampler] 压缩了 ${compressedPoint.samples} 个数据点`); - } - - getDataForTimeRange(minutes: number): ITrafficDataPoint[] { - const cutoff = Date.now() - minutes * 60 * 1000; - - // 如果请求的时间范围在原始数据范围内,直接返回原始数据 - if (minutes <= this.config.rawDataMinutes) { - return this.rawBuffer.filter((p) => p.timestamp > cutoff); - } - - // 否则组合原始数据和压缩数据 - const rawData = this.rawBuffer.filter((p) => p.timestamp > cutoff); - const compressedData = this.compressedBuffer - .filter( - (p) => - p.timestamp > cutoff && - p.timestamp <= Date.now() - this.config.rawDataMinutes * 60 * 1000, - ) - .map((p) => ({ - up: p.up, - down: p.down, - timestamp: p.timestamp, - name: new Date(p.timestamp).toLocaleTimeString("en-US", { - hour12: false, - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }), - })); - - return [...compressedData, ...rawData].sort( - (a, b) => a.timestamp - b.timestamp, - ); - } - - getStats() { - return { - rawBufferSize: this.rawBuffer.length, - compressedBufferSize: this.compressedBuffer.length, - compressionQueueSize: this.compressionQueue.length, - totalMemoryPoints: this.rawBuffer.length + this.compressedBuffer.length, - }; - } - - clear(): void { - this.rawBuffer = []; - this.compressedBuffer = []; - this.compressionQueue = []; - } -} - -// 全局单例 -const refCounter = new ReferenceCounter(); -let globalSampler: TrafficDataSampler | null = null; -let lastValidData: ISystemMonitorOverview | null = null; - -/** - * 增强的流量监控Hook - 支持数据压缩、采样和引用计数 - */ -export const useTrafficMonitorEnhanced = () => { - const { clashInfo } = useClashInfo(); - const pageVisible = useVisibility(); - - // 初始化采样器 - if (!globalSampler) { - globalSampler = new TrafficDataSampler({ - rawDataMinutes: 10, // 原始数据保持10分钟 - compressedDataMinutes: 60, // 压缩数据保持1小时 - compressionRatio: 5, // 每5个原始点压缩成1个 - }); - } - - const [, forceUpdate] = useState({}); - const cleanupRef = useRef<(() => void) | null>(null); - - // 强制组件更新 - const triggerUpdate = useCallback(() => { - forceUpdate({}); - }, []); - - // 注册引用计数 - useEffect(() => { - console.log("[TrafficMonitorEnhanced] 组件挂载,注册引用计数"); - const cleanup = refCounter.increment(); - cleanupRef.current = cleanup; - - return () => { - console.log("[TrafficMonitorEnhanced] 组件卸载,清理引用计数"); - cleanup(); - cleanupRef.current = null; - }; - }, []); - - // 设置引用计数变化回调 - useEffect(() => { - const handleCountChange = () => { - console.log( - `[TrafficMonitorEnhanced] 引用计数变化: ${refCounter.getCount()}`, - ); - if (refCounter.getCount() === 0) { - console.log("[TrafficMonitorEnhanced] 所有组件已卸载,暂停数据收集"); - } else { - console.log("[TrafficMonitorEnhanced] 开始数据收集"); - } - }; - - refCounter.onCountChange(handleCountChange); - }, []); - - // 只有在有引用时才启用SWR - const shouldFetch = clashInfo && pageVisible && refCounter.getCount() > 0; - - const { data: monitorData, error } = useSWR( - shouldFetch ? "getSystemMonitorOverviewSafe" : null, - getSystemMonitorOverviewSafe, - { - refreshInterval: shouldFetch ? 1000 : 0, // 只有在需要时才刷新 - keepPreviousData: true, - onSuccess: (data) => { - // console.log("[TrafficMonitorEnhanced] 获取到监控数据:", data); - - if (data?.traffic?.raw && globalSampler) { - // 保存最后有效数据 - lastValidData = data; - - // 添加到采样器 - const timestamp = Date.now(); - const dataPoint: ITrafficDataPoint = { - up: data.traffic.raw.up_rate || 0, - down: data.traffic.raw.down_rate || 0, - timestamp, - name: new Date(timestamp).toLocaleTimeString("en-US", { - hour12: false, - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }), - }; - - globalSampler.addDataPoint(dataPoint); - triggerUpdate(); - } - }, - onError: (error) => { - console.error( - "[TrafficMonitorEnhanced] 网络错误,使用最后有效数据. 错误详情:", - { - message: error?.message || "未知错误", - stack: error?.stack || "无堆栈信息", - }, - ); - // 网络错误时不清空数据,继续使用最后有效值 - // 但是添加一个错误标记的数据点(流量为0) - if (globalSampler) { - const timestamp = Date.now(); - const errorPoint: ITrafficDataPoint = { - up: 0, - down: 0, - timestamp, - name: new Date(timestamp).toLocaleTimeString("en-US", { - hour12: false, - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }), - }; - globalSampler.addDataPoint(errorPoint); - triggerUpdate(); - } - }, - }, - ); - - // 获取指定时间范围的数据 - const getDataForTimeRange = useCallback( - (minutes: number): ITrafficDataPoint[] => { - if (!globalSampler) return []; - return globalSampler.getDataForTimeRange(minutes); - }, - [], - ); - - // 清空数据 - const clearData = useCallback(() => { - if (globalSampler) { - globalSampler.clear(); - triggerUpdate(); - } - }, [triggerUpdate]); - - // 获取采样器统计信息 - const getSamplerStats = useCallback(() => { - return ( - globalSampler?.getStats() || { - rawBufferSize: 0, - compressedBufferSize: 0, - compressionQueueSize: 0, - totalMemoryPoints: 0, - } - ); - }, []); - - // 构建返回的监控数据,优先使用当前数据,fallback到最后有效数据 - const currentData = monitorData || lastValidData; - const trafficMonitorData = { - traffic: currentData?.traffic || { - raw: { up: 0, down: 0, up_rate: 0, down_rate: 0 }, - formatted: { - up_rate: "0B", - down_rate: "0B", - total_up: "0B", - total_down: "0B", - }, - is_fresh: false, - }, - memory: currentData?.memory || { - raw: { inuse: 0, oslimit: 0, usage_percent: 0 }, - formatted: { inuse: "0B", oslimit: "0B", usage_percent: 0 }, - is_fresh: false, - }, - }; - - return { - // 监控数据 - monitorData: trafficMonitorData, - - // 图表数据管理 - graphData: { - dataPoints: globalSampler?.getDataForTimeRange(60) || [], // 默认获取1小时数据 - getDataForTimeRange, - clearData, - }, - - // 状态信息 - isLoading: !currentData && !error, - error, - isDataFresh: currentData?.traffic?.is_fresh || false, - hasValidData: !!lastValidData, - - // 性能统计 - samplerStats: getSamplerStats(), - referenceCount: refCounter.getCount(), - }; -}; - -/** - * 轻量级流量数据Hook - */ -export const useTrafficDataEnhanced = () => { - const { monitorData, isLoading, error, isDataFresh, hasValidData } = - useTrafficMonitorEnhanced(); - - return { - traffic: monitorData.traffic, - memory: monitorData.memory, - isLoading, - error, - isDataFresh, - hasValidData, - }; -}; - -/** - * 图表数据Hook - */ -export const useTrafficGraphDataEnhanced = () => { - const { graphData, isDataFresh, samplerStats, referenceCount } = - useTrafficMonitorEnhanced(); - - return { - ...graphData, - isDataFresh, - samplerStats, - referenceCount, - }; -}; diff --git a/src/hooks/use-traffic-monitor.ts b/src/hooks/use-traffic-monitor.ts index 211da2823..09a3a8aac 100644 --- a/src/hooks/use-traffic-monitor.ts +++ b/src/hooks/use-traffic-monitor.ts @@ -2,9 +2,9 @@ import { useState, useEffect, useRef, useCallback } from "react"; import useSWR from "swr"; import { useClashInfo } from "@/hooks/use-clash"; import { useVisibility } from "@/hooks/use-visibility"; -import { getSystemMonitorOverview } from "@/services/cmds"; +import { getSystemMonitorOverviewSafe } from "@/services/cmds"; -// 流量数据项接口 +// 增强的流量数据点接口 export interface ITrafficDataPoint { up: number; down: number; @@ -12,215 +12,365 @@ export interface ITrafficDataPoint { name: string; } -// 流量监控数据接口 -export interface ITrafficMonitorData { - traffic: { - raw: { up_rate: number; down_rate: number }; - formatted: { up_rate: string; down_rate: string }; - is_fresh: boolean; - }; - memory: { - raw: { inuse: number; oslimit?: number }; - formatted: { inuse: string; usage_percent?: number }; - is_fresh: boolean; - }; +// 压缩的数据点(用于长期存储) +interface ICompressedDataPoint { + up: number; + down: number; + timestamp: number; + samples: number; // 压缩了多少个原始数据点 } -// 图表数据管理接口 -export interface ITrafficGraphData { - dataPoints: ITrafficDataPoint[]; - addDataPoint: (data: { - up: number; - down: number; - timestamp?: number; - }) => void; - clearData: () => void; - getDataForTimeRange: (minutes: number) => ITrafficDataPoint[]; +// 数据采样器配置 +interface ISamplingConfig { + // 原始数据保持时间(分钟) + rawDataMinutes: number; + // 压缩数据保持时间(分钟) + compressedDataMinutes: number; + // 压缩比例(多少个原始点压缩成1个) + compressionRatio: number; } +// 引用计数管理器 +class ReferenceCounter { + private count = 0; + private callbacks: (() => void)[] = []; + + increment(): () => void { + this.count++; + console.log(`[ReferenceCounter] 引用计数增加: ${this.count}`); + + if (this.count === 1) { + // 从0到1,开始数据收集 + this.callbacks.forEach((cb) => cb()); + } + + return () => { + this.count--; + console.log(`[ReferenceCounter] 引用计数减少: ${this.count}`); + + if (this.count === 0) { + // 从1到0,停止数据收集 + this.callbacks.forEach((cb) => cb()); + } + }; + } + + onCountChange(callback: () => void) { + this.callbacks.push(callback); + } + + getCount(): number { + return this.count; + } +} + +// 智能数据采样器 +class TrafficDataSampler { + private rawBuffer: ITrafficDataPoint[] = []; + private compressedBuffer: ICompressedDataPoint[] = []; + private config: ISamplingConfig; + private compressionQueue: ITrafficDataPoint[] = []; + + constructor(config: ISamplingConfig) { + this.config = config; + } + + addDataPoint(point: ITrafficDataPoint): void { + // 添加到原始缓冲区 + this.rawBuffer.push(point); + + // 清理过期的原始数据 + const rawCutoff = Date.now() - this.config.rawDataMinutes * 60 * 1000; + this.rawBuffer = this.rawBuffer.filter((p) => p.timestamp > rawCutoff); + + // 添加到压缩队列 + this.compressionQueue.push(point); + + // 当压缩队列达到压缩比例时,执行压缩 + if (this.compressionQueue.length >= this.config.compressionRatio) { + this.compressData(); + } + + // 清理过期的压缩数据 + const compressedCutoff = + Date.now() - this.config.compressedDataMinutes * 60 * 1000; + this.compressedBuffer = this.compressedBuffer.filter( + (p) => p.timestamp > compressedCutoff, + ); + } + + private compressData(): void { + if (this.compressionQueue.length === 0) return; + + // 计算平均值进行压缩 + const totalUp = this.compressionQueue.reduce((sum, p) => sum + p.up, 0); + const totalDown = this.compressionQueue.reduce((sum, p) => sum + p.down, 0); + const avgTimestamp = + this.compressionQueue.reduce((sum, p) => sum + p.timestamp, 0) / + this.compressionQueue.length; + + const compressedPoint: ICompressedDataPoint = { + up: totalUp / this.compressionQueue.length, + down: totalDown / this.compressionQueue.length, + timestamp: avgTimestamp, + samples: this.compressionQueue.length, + }; + + this.compressedBuffer.push(compressedPoint); + this.compressionQueue = []; + + console.log(`[DataSampler] 压缩了 ${compressedPoint.samples} 个数据点`); + } + + getDataForTimeRange(minutes: number): ITrafficDataPoint[] { + const cutoff = Date.now() - minutes * 60 * 1000; + + // 如果请求的时间范围在原始数据范围内,直接返回原始数据 + if (minutes <= this.config.rawDataMinutes) { + return this.rawBuffer.filter((p) => p.timestamp > cutoff); + } + + // 否则组合原始数据和压缩数据 + const rawData = this.rawBuffer.filter((p) => p.timestamp > cutoff); + const compressedData = this.compressedBuffer + .filter( + (p) => + p.timestamp > cutoff && + p.timestamp <= Date.now() - this.config.rawDataMinutes * 60 * 1000, + ) + .map((p) => ({ + up: p.up, + down: p.down, + timestamp: p.timestamp, + name: new Date(p.timestamp).toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }), + })); + + return [...compressedData, ...rawData].sort( + (a, b) => a.timestamp - b.timestamp, + ); + } + + getStats() { + return { + rawBufferSize: this.rawBuffer.length, + compressedBufferSize: this.compressedBuffer.length, + compressionQueueSize: this.compressionQueue.length, + totalMemoryPoints: this.rawBuffer.length + this.compressedBuffer.length, + }; + } + + clear(): void { + this.rawBuffer = []; + this.compressedBuffer = []; + this.compressionQueue = []; + } +} + +// 全局单例 +const refCounter = new ReferenceCounter(); +let globalSampler: TrafficDataSampler | null = null; +let lastValidData: ISystemMonitorOverview | null = null; + /** - * 全局流量监控数据管理Hook - * 提供统一的流量数据获取和图表数据管理 + * 增强的流量监控Hook - 支持数据压缩、采样和引用计数 */ -export const useTrafficMonitor = () => { +export const useTrafficMonitorEnhanced = () => { const { clashInfo } = useClashInfo(); const pageVisible = useVisibility(); - // 图表数据缓冲区 - 使用ref保持数据持久性 - const dataBufferRef = useRef([]); - const [, forceUpdate] = useState({}); + // 初始化采样器 + if (!globalSampler) { + globalSampler = new TrafficDataSampler({ + rawDataMinutes: 10, // 原始数据保持10分钟 + compressedDataMinutes: 60, // 压缩数据保持1小时 + compressionRatio: 5, // 每5个原始点压缩成1个 + }); + } - // 强制组件更新的函数 + const [, forceUpdate] = useState({}); + const cleanupRef = useRef<(() => void) | null>(null); + + // 强制组件更新 const triggerUpdate = useCallback(() => { forceUpdate({}); }, []); - // 最大缓冲区大小 (10分钟 * 60秒 = 600个数据点) - const MAX_BUFFER_SIZE = 600; - - // 初始化数据缓冲区 + // 注册引用计数 useEffect(() => { - if (dataBufferRef.current.length === 0) { - const now = Date.now(); - const tenMinutesAgo = now - 10 * 60 * 1000; + console.log("[TrafficMonitorEnhanced] 组件挂载,注册引用计数"); + const cleanup = refCounter.increment(); + cleanupRef.current = cleanup; - const initialBuffer = Array.from( - { length: MAX_BUFFER_SIZE }, - (_, index) => { - const pointTime = - tenMinutesAgo + index * ((10 * 60 * 1000) / MAX_BUFFER_SIZE); - const date = new Date(pointTime); + return () => { + console.log("[TrafficMonitorEnhanced] 组件卸载,清理引用计数"); + cleanup(); + cleanupRef.current = null; + }; + }, []); - let nameValue: string; - try { - if (isNaN(date.getTime())) { - nameValue = "??:??:??"; - } else { - nameValue = date.toLocaleTimeString("en-US", { - hour12: false, - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); - } - } catch (e) { - nameValue = "Err:Time"; - } - - return { - up: 0, - down: 0, - timestamp: pointTime, - name: nameValue, - }; - }, + // 设置引用计数变化回调 + useEffect(() => { + const handleCountChange = () => { + console.log( + `[TrafficMonitorEnhanced] 引用计数变化: ${refCounter.getCount()}`, ); + if (refCounter.getCount() === 0) { + console.log("[TrafficMonitorEnhanced] 所有组件已卸载,暂停数据收集"); + } else { + console.log("[TrafficMonitorEnhanced] 开始数据收集"); + } + }; - dataBufferRef.current = initialBuffer; - } - }, [MAX_BUFFER_SIZE]); + refCounter.onCountChange(handleCountChange); + }, []); + + // 只有在有引用时才启用SWR + const shouldFetch = clashInfo && pageVisible && refCounter.getCount() > 0; - // 使用SWR获取监控数据 const { data: monitorData, error } = useSWR( - clashInfo && pageVisible ? "getSystemMonitorOverview" : null, - getSystemMonitorOverview, + shouldFetch ? "getSystemMonitorOverviewSafe" : null, + getSystemMonitorOverviewSafe, { - refreshInterval: 1000, // 1秒刷新一次 + refreshInterval: shouldFetch ? 1000 : 0, // 只有在需要时才刷新 keepPreviousData: true, onSuccess: (data) => { - console.log("[TrafficMonitor] 获取到监控数据:", data); + // console.log("[TrafficMonitorEnhanced] 获取到监控数据:", data); - if (data?.traffic) { - // 为图表添加新数据点 - addDataPoint({ + if (data?.traffic?.raw && globalSampler) { + // 保存最后有效数据 + lastValidData = data; + + // 添加到采样器 + const timestamp = Date.now(); + const dataPoint: ITrafficDataPoint = { up: data.traffic.raw.up_rate || 0, down: data.traffic.raw.down_rate || 0, - timestamp: Date.now(), - }); + timestamp, + name: new Date(timestamp).toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }), + }; + + globalSampler.addDataPoint(dataPoint); + triggerUpdate(); } }, onError: (error) => { - console.error("[TrafficMonitor] 获取数据错误:", error); + console.error( + "[TrafficMonitorEnhanced] 网络错误,使用最后有效数据. 错误详情:", + { + message: error?.message || "未知错误", + stack: error?.stack || "无堆栈信息", + }, + ); + // 网络错误时不清空数据,继续使用最后有效值 + // 但是添加一个错误标记的数据点(流量为0) + if (globalSampler) { + const timestamp = Date.now(); + const errorPoint: ITrafficDataPoint = { + up: 0, + down: 0, + timestamp, + name: new Date(timestamp).toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }), + }; + globalSampler.addDataPoint(errorPoint); + triggerUpdate(); + } }, }, ); - // 添加数据点到缓冲区 - const addDataPoint = useCallback( - (data: { up: number; down: number; timestamp?: number }) => { - const timestamp = data.timestamp || Date.now(); - const date = new Date(timestamp); - - let nameValue: string; - try { - if (isNaN(date.getTime())) { - nameValue = "??:??:??"; - } else { - nameValue = date.toLocaleTimeString("en-US", { - hour12: false, - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); - } - } catch (e) { - nameValue = "Err:Time"; - } - - const newPoint: ITrafficDataPoint = { - up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0, - down: - typeof data.down === "number" && !isNaN(data.down) ? data.down : 0, - timestamp, - name: nameValue, - }; - - // 更新缓冲区,保持固定大小 - const newBuffer = [...dataBufferRef.current.slice(1), newPoint]; - dataBufferRef.current = newBuffer; - - // 触发使用该数据的组件更新 - triggerUpdate(); - }, - [triggerUpdate], - ); - - // 清空数据 - const clearData = useCallback(() => { - dataBufferRef.current = []; - triggerUpdate(); - }, [triggerUpdate]); - - // 根据时间范围获取数据 + // 获取指定时间范围的数据 const getDataForTimeRange = useCallback( (minutes: number): ITrafficDataPoint[] => { - const pointsToShow = minutes * 60; // 每分钟60个数据点 - return dataBufferRef.current.slice(-pointsToShow); + if (!globalSampler) return []; + return globalSampler.getDataForTimeRange(minutes); }, [], ); - // 构建图表数据管理对象 - const graphData: ITrafficGraphData = { - dataPoints: dataBufferRef.current, - addDataPoint, - clearData, - getDataForTimeRange, - }; + // 清空数据 + const clearData = useCallback(() => { + if (globalSampler) { + globalSampler.clear(); + triggerUpdate(); + } + }, [triggerUpdate]); - // 构建监控数据对象 - const trafficMonitorData: ITrafficMonitorData = { - traffic: monitorData?.traffic || { - raw: { up_rate: 0, down_rate: 0 }, - formatted: { up_rate: "0B", down_rate: "0B" }, + // 获取采样器统计信息 + const getSamplerStats = useCallback(() => { + return ( + globalSampler?.getStats() || { + rawBufferSize: 0, + compressedBufferSize: 0, + compressionQueueSize: 0, + totalMemoryPoints: 0, + } + ); + }, []); + + // 构建返回的监控数据,优先使用当前数据,fallback到最后有效数据 + const currentData = monitorData || lastValidData; + const trafficMonitorData = { + traffic: currentData?.traffic || { + raw: { up: 0, down: 0, up_rate: 0, down_rate: 0 }, + formatted: { + up_rate: "0B", + down_rate: "0B", + total_up: "0B", + total_down: "0B", + }, is_fresh: false, }, - memory: monitorData?.memory || { - raw: { inuse: 0 }, - formatted: { inuse: "0B" }, + memory: currentData?.memory || { + raw: { inuse: 0, oslimit: 0, usage_percent: 0 }, + formatted: { inuse: "0B", oslimit: "0B", usage_percent: 0 }, is_fresh: false, }, }; return { - // 原始监控数据 + // 监控数据 monitorData: trafficMonitorData, + // 图表数据管理 - graphData, - // 数据获取状态 - isLoading: !monitorData && !error, + graphData: { + dataPoints: globalSampler?.getDataForTimeRange(60) || [], // 默认获取1小时数据 + getDataForTimeRange, + clearData, + }, + + // 状态信息 + isLoading: !currentData && !error, error, - // 数据新鲜度 - isDataFresh: monitorData?.overall_status === "active", + isDataFresh: currentData?.traffic?.is_fresh || false, + hasValidData: !!lastValidData, + + // 性能统计 + samplerStats: getSamplerStats(), + referenceCount: refCounter.getCount(), }; }; /** - * 仅获取流量数据的轻量级Hook - * 适用于不需要图表数据的组件 + * 轻量级流量数据Hook */ -export const useTrafficData = () => { - const { monitorData, isLoading, error, isDataFresh } = useTrafficMonitor(); +export const useTrafficDataEnhanced = () => { + const { monitorData, isLoading, error, isDataFresh, hasValidData } = + useTrafficMonitorEnhanced(); return { traffic: monitorData.traffic, @@ -228,18 +378,21 @@ export const useTrafficData = () => { isLoading, error, isDataFresh, + hasValidData, }; }; /** - * 仅获取图表数据的Hook - * 适用于图表组件 + * 图表数据Hook */ -export const useTrafficGraphData = () => { - const { graphData, isDataFresh } = useTrafficMonitor(); +export const useTrafficGraphDataEnhanced = () => { + const { graphData, isDataFresh, samplerStats, referenceCount } = + useTrafficMonitorEnhanced(); return { ...graphData, isDataFresh, + samplerStats, + referenceCount, }; };