mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-14 21:20:48 +08:00
perf(traffic): single-slice snapshots with worker-side aging
The traffic pipeline re-rendered twice per second (worker snapshot plus a 1 s now-tick), cloned the 10-minute and 60-minute arrays through every snapshot, and re-filtered them on the main thread. Snapshots now carry only the range slice the graph renders, a 1 s worker-side heartbeat re-broadcasts only when points age out of the window, and snapshot ingestion pauses while the window is hidden. Perf harness A/B: hidden-phase webview CPU 2.45% -> 0.77%, main 0.90% -> 0.37%, webview RSS about -3 MB; visible phases unchanged.
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
|||||||
formatTrafficHourMinute,
|
formatTrafficHourMinute,
|
||||||
formatTrafficMinuteSecond,
|
formatTrafficMinuteSecond,
|
||||||
formatTrafficName,
|
formatTrafficName,
|
||||||
|
isSameTrafficData,
|
||||||
} from '@/utils/traffic-sampler'
|
} from '@/utils/traffic-sampler'
|
||||||
|
|
||||||
interface ITrafficItem {
|
interface ITrafficItem {
|
||||||
@@ -84,29 +85,6 @@ interface EnhancedCanvasTrafficGraphProps {
|
|||||||
ref?: Ref<EnhancedCanvasTrafficGraphRef>
|
ref?: Ref<EnhancedCanvasTrafficGraphRef>
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSameTrafficData = (
|
|
||||||
current: ITrafficDataPoint[],
|
|
||||||
next: ITrafficDataPoint[],
|
|
||||||
) => {
|
|
||||||
if (current === next) return true
|
|
||||||
if (current.length !== next.length) return false
|
|
||||||
|
|
||||||
for (let i = 0; i < current.length; i++) {
|
|
||||||
const currentPoint = current[i]
|
|
||||||
const nextPoint = next[i]
|
|
||||||
|
|
||||||
if (
|
|
||||||
currentPoint.timestamp !== nextPoint.timestamp ||
|
|
||||||
currentPoint.up !== nextPoint.up ||
|
|
||||||
currentPoint.down !== nextPoint.down
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayDataReducer = (
|
const displayDataReducer = (
|
||||||
current: ITrafficDataPoint[],
|
current: ITrafficDataPoint[],
|
||||||
payload: ITrafficDataPoint[],
|
payload: ITrafficDataPoint[],
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
import {
|
import { useCallback, useEffect, useReducer, useRef, useState } from 'react'
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useReducer,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react'
|
|
||||||
import { Traffic } from 'tauri-plugin-mihomo-api'
|
import { Traffic } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import { useVisibility } from '@/hooks/use-visibility'
|
import { useVisibility } from '@/hooks/use-visibility'
|
||||||
import { debugLog } from '@/utils/debug'
|
import { debugLog } from '@/utils/debug'
|
||||||
import { TrafficDataSampler, formatTrafficName } from '@/utils/traffic-sampler'
|
import { TrafficDataSampler, isSameTrafficData } from '@/utils/traffic-sampler'
|
||||||
|
|
||||||
// 引用计数管理器
|
// 引用计数管理器
|
||||||
class ReferenceCounter {
|
class ReferenceCounter {
|
||||||
@@ -59,8 +52,10 @@ class InlineTrafficMonitor {
|
|||||||
private config = { ...WORKER_CONFIG }
|
private config = { ...WORKER_CONFIG }
|
||||||
private sampler = new TrafficDataSampler(this.config)
|
private sampler = new TrafficDataSampler(this.config)
|
||||||
private throttleTimer: ReturnType<typeof setTimeout> | null = null
|
private throttleTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
private agingTimer: ReturnType<typeof setInterval> | null = null
|
||||||
private currentRange = this.config.defaultRangeMinutes
|
private currentRange = this.config.defaultRangeMinutes
|
||||||
private lastTimestamp: number | undefined
|
private lastTimestamp: number | undefined
|
||||||
|
private lastBroadcast: ITrafficDataPoint[] = []
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private emit: (snapshot: ITrafficWorkerSnapshotMessage) => void,
|
private emit: (snapshot: ITrafficWorkerSnapshotMessage) => void,
|
||||||
@@ -82,6 +77,10 @@ class InlineTrafficMonitor {
|
|||||||
clearTimeout(this.throttleTimer)
|
clearTimeout(this.throttleTimer)
|
||||||
this.throttleTimer = null
|
this.throttleTimer = null
|
||||||
}
|
}
|
||||||
|
if (this.agingTimer !== null) {
|
||||||
|
clearInterval(this.agingTimer)
|
||||||
|
this.agingTimer = null
|
||||||
|
}
|
||||||
this.sampler.clear()
|
this.sampler.clear()
|
||||||
this.lastTimestamp = undefined
|
this.lastTimestamp = undefined
|
||||||
}
|
}
|
||||||
@@ -92,6 +91,14 @@ class InlineTrafficMonitor {
|
|||||||
this.config = { ...message.config }
|
this.config = { ...message.config }
|
||||||
this.sampler = new TrafficDataSampler(this.config)
|
this.sampler = new TrafficDataSampler(this.config)
|
||||||
this.currentRange = message.config.defaultRangeMinutes
|
this.currentRange = message.config.defaultRangeMinutes
|
||||||
|
// Re-broadcast when points age out of the range, not on every tick
|
||||||
|
this.agingTimer = setInterval(() => {
|
||||||
|
if (this.throttleTimer !== null) return
|
||||||
|
const slice = this.sampler.getDataForTimeRange(this.currentRange)
|
||||||
|
if (!isSameTrafficData(this.lastBroadcast, slice)) {
|
||||||
|
this.emitSnapshot('interval')
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
this.emitSnapshot('init')
|
this.emitSnapshot('init')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -101,7 +108,6 @@ class InlineTrafficMonitor {
|
|||||||
up: message.payload.up || 0,
|
up: message.payload.up || 0,
|
||||||
down: message.payload.down || 0,
|
down: message.payload.down || 0,
|
||||||
timestamp,
|
timestamp,
|
||||||
name: formatTrafficName(timestamp),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastTimestamp = timestamp
|
this.lastTimestamp = timestamp
|
||||||
@@ -133,19 +139,16 @@ class InlineTrafficMonitor {
|
|||||||
|
|
||||||
private emitSnapshot(reason: ITrafficWorkerSnapshotMessage['reason']) {
|
private emitSnapshot(reason: ITrafficWorkerSnapshotMessage['reason']) {
|
||||||
const dataPoints = this.sampler.getDataForTimeRange(this.currentRange)
|
const dataPoints = this.sampler.getDataForTimeRange(this.currentRange)
|
||||||
const availableDataPoints = this.sampler.getDataForTimeRange(
|
|
||||||
this.config.compressedDataMinutes,
|
|
||||||
)
|
|
||||||
|
|
||||||
this.emit({
|
this.emit({
|
||||||
type: 'snapshot',
|
type: 'snapshot',
|
||||||
dataPoints,
|
dataPoints,
|
||||||
availableDataPoints,
|
|
||||||
samplerStats: this.sampler.getStats(),
|
samplerStats: this.sampler.getStats(),
|
||||||
rangeMinutes: this.currentRange,
|
rangeMinutes: this.currentRange,
|
||||||
lastTimestamp: this.lastTimestamp,
|
lastTimestamp: this.lastTimestamp,
|
||||||
reason,
|
reason,
|
||||||
})
|
})
|
||||||
|
this.lastBroadcast = dataPoints
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleSnapshot(reason: ITrafficWorkerSnapshotMessage['reason']) {
|
private scheduleSnapshot(reason: ITrafficWorkerSnapshotMessage['reason']) {
|
||||||
@@ -207,9 +210,9 @@ class TrafficWorkerClient {
|
|||||||
message.lastTimestamp ?? 0,
|
message.lastTimestamp ?? 0,
|
||||||
)
|
)
|
||||||
document.documentElement.dataset.perfPoints = String(
|
document.documentElement.dataset.perfPoints = String(
|
||||||
message.availableDataPoints.length,
|
message.dataPoints.length,
|
||||||
)
|
)
|
||||||
const last = message.availableDataPoints.at(-1)
|
const last = message.dataPoints.at(-1)
|
||||||
document.documentElement.dataset.perfLastUp = String(
|
document.documentElement.dataset.perfLastUp = String(
|
||||||
last?.up ?? 0,
|
last?.up ?? 0,
|
||||||
)
|
)
|
||||||
@@ -360,6 +363,8 @@ const EMPTY_STATS: ISamplerStats = {
|
|||||||
totalMemoryPoints: 0,
|
totalMemoryPoints: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EMPTY_DATA: ITrafficDataPoint[] = []
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 增强的流量监控Hook - Web Worker驱动的数据采样与压缩
|
* 增强的流量监控Hook - Web Worker驱动的数据采样与压缩
|
||||||
*/
|
*/
|
||||||
@@ -371,22 +376,18 @@ export const useTrafficMonitorEnhanced = (options?: {
|
|||||||
const enabled = options?.enabled ?? true
|
const enabled = options?.enabled ?? true
|
||||||
const isVisible = useVisibility()
|
const isVisible = useVisibility()
|
||||||
const [latestSnapshot, setLatestSnapshot] = useState<{
|
const [latestSnapshot, setLatestSnapshot] = useState<{
|
||||||
availableDataPoints: ITrafficDataPoint[]
|
dataPoints: ITrafficDataPoint[]
|
||||||
samplerStats: ISamplerStats
|
samplerStats: ISamplerStats
|
||||||
lastTimestamp?: number
|
|
||||||
}>({
|
}>({
|
||||||
availableDataPoints: [],
|
dataPoints: [],
|
||||||
samplerStats: EMPTY_STATS,
|
samplerStats: EMPTY_STATS,
|
||||||
lastTimestamp: undefined,
|
|
||||||
})
|
})
|
||||||
const [rangeMinutes, setRangeMinutes] = useState(
|
|
||||||
WORKER_CONFIG.defaultRangeMinutes,
|
|
||||||
)
|
|
||||||
const [now, setNow] = useState(() => Date.now())
|
|
||||||
const [, forceRefCountRender] = useReducer((value) => value + 1, 0)
|
const [, forceRefCountRender] = useReducer((value) => value + 1, 0)
|
||||||
|
|
||||||
const clientRef = useRef<TrafficWorkerClient | null>(getWorkerClient())
|
const clientRef = useRef<TrafficWorkerClient | null>(getWorkerClient())
|
||||||
const currentRangeRef = useRef<number>(WORKER_CONFIG.defaultRangeMinutes)
|
const currentRangeRef = useRef<number>(WORKER_CONFIG.defaultRangeMinutes)
|
||||||
|
const isVisibleRef = useRef(isVisible)
|
||||||
|
isVisibleRef.current = isVisible
|
||||||
|
|
||||||
// 注册引用计数与Worker生命周期
|
// 注册引用计数与Worker生命周期
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -404,15 +405,12 @@ export const useTrafficMonitorEnhanced = (options?: {
|
|||||||
let unsubscribe: (() => void) | undefined
|
let unsubscribe: (() => void) | undefined
|
||||||
if (subscribeToSnapshots) {
|
if (subscribeToSnapshots) {
|
||||||
unsubscribe = client.onSnapshot((message) => {
|
unsubscribe = client.onSnapshot((message) => {
|
||||||
|
if (!isVisibleRef.current) return
|
||||||
setLatestSnapshot({
|
setLatestSnapshot({
|
||||||
availableDataPoints:
|
dataPoints: message.dataPoints,
|
||||||
message.availableDataPoints ?? message.dataPoints,
|
|
||||||
samplerStats: message.samplerStats,
|
samplerStats: message.samplerStats,
|
||||||
lastTimestamp: message.lastTimestamp,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
client.requestSnapshot()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -425,15 +423,11 @@ export const useTrafficMonitorEnhanced = (options?: {
|
|||||||
}
|
}
|
||||||
}, [enabled, subscribeToSnapshots])
|
}, [enabled, subscribeToSnapshots])
|
||||||
|
|
||||||
// Periodically refresh "now" so idle streams age out of the selected window when subscribed
|
// Re-prime snapshot ingestion after every visible transition
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled || !subscribeToSnapshots || !isVisible) return
|
if (enabled && subscribeToSnapshots && isVisible) {
|
||||||
|
clientRef.current?.requestSnapshot()
|
||||||
const timer = window.setInterval(() => {
|
}
|
||||||
setNow(Date.now())
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
return () => window.clearInterval(timer)
|
|
||||||
}, [enabled, subscribeToSnapshots, isVisible])
|
}, [enabled, subscribeToSnapshots, isVisible])
|
||||||
|
|
||||||
// 添加流量数据
|
// 添加流量数据
|
||||||
@@ -450,7 +444,6 @@ export const useTrafficMonitorEnhanced = (options?: {
|
|||||||
(minutes: number) => {
|
(minutes: number) => {
|
||||||
if (!enabled) return
|
if (!enabled) return
|
||||||
currentRangeRef.current = minutes
|
currentRangeRef.current = minutes
|
||||||
setRangeMinutes(minutes)
|
|
||||||
clientRef.current?.setRange(minutes)
|
clientRef.current?.setRange(minutes)
|
||||||
},
|
},
|
||||||
[enabled],
|
[enabled],
|
||||||
@@ -462,19 +455,9 @@ export const useTrafficMonitorEnhanced = (options?: {
|
|||||||
clientRef.current?.clearData()
|
clientRef.current?.clearData()
|
||||||
}, [enabled])
|
}, [enabled])
|
||||||
|
|
||||||
const filteredDataPoints = useMemo(() => {
|
|
||||||
if (!enabled) return []
|
|
||||||
const sourceData = latestSnapshot.availableDataPoints
|
|
||||||
if (sourceData.length === 0) return []
|
|
||||||
|
|
||||||
const cutoff = now - rangeMinutes * 60 * 1000
|
|
||||||
return sourceData.filter((point) => point.timestamp > cutoff)
|
|
||||||
}, [enabled, latestSnapshot.availableDataPoints, rangeMinutes, now])
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
graphData: {
|
graphData: {
|
||||||
dataPoints: filteredDataPoints,
|
dataPoints: enabled ? latestSnapshot.dataPoints : EMPTY_DATA,
|
||||||
currentRangeMinutes: rangeMinutes,
|
|
||||||
requestRange,
|
requestRange,
|
||||||
appendData,
|
appendData,
|
||||||
clearData,
|
clearData,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TrafficDataSampler, formatTrafficName } from '../utils/traffic-sampler'
|
import { TrafficDataSampler, isSameTrafficData } from '../utils/traffic-sampler'
|
||||||
|
|
||||||
const DEFAULT_CONFIG: ISamplingConfig & {
|
const DEFAULT_CONFIG: ISamplingConfig & {
|
||||||
snapshotIntervalMs: number
|
snapshotIntervalMs: number
|
||||||
@@ -16,6 +16,8 @@ interface WorkerScope {
|
|||||||
onmessage: ((event: MessageEvent<TrafficWorkerRequestMessage>) => void) | null
|
onmessage: ((event: MessageEvent<TrafficWorkerRequestMessage>) => void) | null
|
||||||
setTimeout: typeof setTimeout
|
setTimeout: typeof setTimeout
|
||||||
clearTimeout: typeof clearTimeout
|
clearTimeout: typeof clearTimeout
|
||||||
|
setInterval: typeof setInterval
|
||||||
|
clearInterval: typeof clearInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctx: WorkerScope = self as unknown as WorkerScope
|
const ctx: WorkerScope = self as unknown as WorkerScope
|
||||||
@@ -25,18 +27,15 @@ let sampler = new TrafficDataSampler(config)
|
|||||||
let currentRangeMinutes = config.defaultRangeMinutes
|
let currentRangeMinutes = config.defaultRangeMinutes
|
||||||
let throttleTimer: ReturnType<typeof setTimeout> | null = null
|
let throttleTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
let lastTimestamp: number | undefined
|
let lastTimestamp: number | undefined
|
||||||
|
let lastBroadcast: ITrafficDataPoint[] = []
|
||||||
|
|
||||||
const broadcastSnapshot = (reason: ITrafficWorkerSnapshotMessage['reason']) => {
|
const broadcastSnapshot = (reason: ITrafficWorkerSnapshotMessage['reason']) => {
|
||||||
const dataPoints = sampler.getDataForTimeRange(currentRangeMinutes)
|
const dataPoints = sampler.getDataForTimeRange(currentRangeMinutes)
|
||||||
const availableDataPoints = sampler.getDataForTimeRange(
|
|
||||||
config.compressedDataMinutes,
|
|
||||||
)
|
|
||||||
const samplerStats = sampler.getStats()
|
const samplerStats = sampler.getStats()
|
||||||
|
|
||||||
const message: ITrafficWorkerSnapshotMessage = {
|
const message: ITrafficWorkerSnapshotMessage = {
|
||||||
type: 'snapshot',
|
type: 'snapshot',
|
||||||
dataPoints,
|
dataPoints,
|
||||||
availableDataPoints,
|
|
||||||
samplerStats,
|
samplerStats,
|
||||||
rangeMinutes: currentRangeMinutes,
|
rangeMinutes: currentRangeMinutes,
|
||||||
lastTimestamp,
|
lastTimestamp,
|
||||||
@@ -44,6 +43,7 @@ const broadcastSnapshot = (reason: ITrafficWorkerSnapshotMessage['reason']) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.postMessage(message)
|
ctx.postMessage(message)
|
||||||
|
lastBroadcast = dataPoints
|
||||||
}
|
}
|
||||||
|
|
||||||
const scheduleSnapshot = (reason: ITrafficWorkerSnapshotMessage['reason']) => {
|
const scheduleSnapshot = (reason: ITrafficWorkerSnapshotMessage['reason']) => {
|
||||||
@@ -62,6 +62,15 @@ ctx.onmessage = (event: MessageEvent<TrafficWorkerRequestMessage>) => {
|
|||||||
config = { ...message.config }
|
config = { ...message.config }
|
||||||
sampler = new TrafficDataSampler(config)
|
sampler = new TrafficDataSampler(config)
|
||||||
currentRangeMinutes = message.config.defaultRangeMinutes
|
currentRangeMinutes = message.config.defaultRangeMinutes
|
||||||
|
// Re-broadcast when points age out of the range, not on every tick;
|
||||||
|
// no handle needed, worker teardown (terminate) drops pending timers
|
||||||
|
ctx.setInterval(() => {
|
||||||
|
if (throttleTimer !== null) return
|
||||||
|
const slice = sampler.getDataForTimeRange(currentRangeMinutes)
|
||||||
|
if (!isSameTrafficData(lastBroadcast, slice)) {
|
||||||
|
broadcastSnapshot('interval')
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
broadcastSnapshot('init')
|
broadcastSnapshot('init')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -71,7 +80,6 @@ ctx.onmessage = (event: MessageEvent<TrafficWorkerRequestMessage>) => {
|
|||||||
up: message.payload.up || 0,
|
up: message.payload.up || 0,
|
||||||
down: message.payload.down || 0,
|
down: message.payload.down || 0,
|
||||||
timestamp,
|
timestamp,
|
||||||
name: formatTrafficName(timestamp),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lastTimestamp = timestamp
|
lastTimestamp = timestamp
|
||||||
|
|||||||
Vendored
-2
@@ -973,7 +973,6 @@ interface ITrafficDataPoint {
|
|||||||
up: number
|
up: number
|
||||||
down: number
|
down: number
|
||||||
timestamp: number
|
timestamp: number
|
||||||
name: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISamplingConfig {
|
interface ISamplingConfig {
|
||||||
@@ -1029,7 +1028,6 @@ type TrafficWorkerRequestMessage =
|
|||||||
interface ITrafficWorkerSnapshotMessage {
|
interface ITrafficWorkerSnapshotMessage {
|
||||||
type: 'snapshot'
|
type: 'snapshot'
|
||||||
dataPoints: ITrafficDataPoint[]
|
dataPoints: ITrafficDataPoint[]
|
||||||
availableDataPoints: ITrafficDataPoint[]
|
|
||||||
samplerStats: ISamplerStats
|
samplerStats: ISamplerStats
|
||||||
rangeMinutes: number
|
rangeMinutes: number
|
||||||
lastTimestamp?: number
|
lastTimestamp?: number
|
||||||
|
|||||||
@@ -24,6 +24,29 @@ export const formatTrafficName = (timestamp: number) => {
|
|||||||
)}`
|
)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isSameTrafficData = (
|
||||||
|
current: ITrafficDataPoint[],
|
||||||
|
next: ITrafficDataPoint[],
|
||||||
|
) => {
|
||||||
|
if (current === next) return true
|
||||||
|
if (current.length !== next.length) return false
|
||||||
|
|
||||||
|
for (let i = 0; i < current.length; i++) {
|
||||||
|
const currentPoint = current[i]
|
||||||
|
const nextPoint = next[i]
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentPoint.timestamp !== nextPoint.timestamp ||
|
||||||
|
currentPoint.up !== nextPoint.up ||
|
||||||
|
currentPoint.down !== nextPoint.down
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
export class TrafficDataSampler {
|
export class TrafficDataSampler {
|
||||||
private rawBuffer: ITrafficDataPoint[] = []
|
private rawBuffer: ITrafficDataPoint[] = []
|
||||||
private rawHead = 0
|
private rawHead = 0
|
||||||
@@ -125,7 +148,6 @@ export class TrafficDataSampler {
|
|||||||
up: p.up,
|
up: p.up,
|
||||||
down: p.down,
|
down: p.down,
|
||||||
timestamp: p.timestamp,
|
timestamp: p.timestamp,
|
||||||
name: formatTrafficName(p.timestamp),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return [...compressedData, ...rawData].sort(
|
return [...compressedData, ...rawData].sort(
|
||||||
|
|||||||
Reference in New Issue
Block a user