mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-27 16:22:11 +08:00
perf: reduce frontend subscription and raf churn
This commit is contained in:
@@ -12,6 +12,7 @@ const upLineWidth = 4
|
||||
|
||||
const downLineAlpha = 1
|
||||
const downLineWidth = 4
|
||||
const frameIntervalMs = 1000 / 15
|
||||
|
||||
const defaultList = Array(maxPoint + 2).fill({ up: 0, down: 0 })
|
||||
|
||||
@@ -69,6 +70,7 @@ export function TrafficGraph({ ref }: { ref?: Ref<TrafficRef> }) {
|
||||
|
||||
useEffect(() => {
|
||||
let raf = 0
|
||||
let frameTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const canvas = canvasRef.current!
|
||||
|
||||
if (!canvas) return
|
||||
@@ -144,14 +146,37 @@ export function TrafficGraph({ ref }: { ref?: Ref<TrafficRef> }) {
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleDraw = (lastTime: number, delay = 0) => {
|
||||
if (frameTimer !== null) {
|
||||
clearTimeout(frameTimer)
|
||||
frameTimer = null
|
||||
}
|
||||
|
||||
if (delay > 0) {
|
||||
frameTimer = setTimeout(() => {
|
||||
frameTimer = null
|
||||
raf = requestAnimationFrame(() => {
|
||||
raf = 0
|
||||
drawGraph(lastTime)
|
||||
})
|
||||
}, delay)
|
||||
return
|
||||
}
|
||||
|
||||
raf = requestAnimationFrame(() => {
|
||||
raf = 0
|
||||
drawGraph(lastTime)
|
||||
})
|
||||
}
|
||||
|
||||
const drawGraph = (lastTime: number) => {
|
||||
const list = listRef.current
|
||||
const lineStyle = styleRef.current
|
||||
|
||||
const now = Date.now()
|
||||
const diff = now - lastTime
|
||||
if (diff < 33) {
|
||||
raf = requestAnimationFrame(() => drawGraph(lastTime))
|
||||
if (diff < frameIntervalMs) {
|
||||
scheduleDraw(lastTime, frameIntervalMs - diff)
|
||||
return
|
||||
}
|
||||
const temp = Math.min((diff / 1000) * dx + countRef.current, dx)
|
||||
@@ -196,13 +221,18 @@ export function TrafficGraph({ ref }: { ref?: Ref<TrafficRef> }) {
|
||||
context.stroke()
|
||||
context.closePath()
|
||||
|
||||
raf = requestAnimationFrame(() => drawGraph(now))
|
||||
scheduleDraw(now, frameIntervalMs)
|
||||
}
|
||||
|
||||
drawGraph(Date.now())
|
||||
drawGraph(Date.now() - frameIntervalMs)
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(raf)
|
||||
if (frameTimer !== null) {
|
||||
clearTimeout(frameTimer)
|
||||
}
|
||||
if (raf) {
|
||||
cancelAnimationFrame(raf)
|
||||
}
|
||||
}
|
||||
}, [palette])
|
||||
|
||||
|
||||
@@ -1,10 +1,138 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useLocalStorage } from 'foxact/use-local-storage'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { type MutableRefObject, useCallback, useEffect, useRef } from 'react'
|
||||
import { type Message, type MihomoWebSocket } from 'tauri-plugin-mihomo-api'
|
||||
|
||||
export const RECONNECT_DELAY_MS = 1000
|
||||
|
||||
interface SharedSubscriptionOwner {
|
||||
handleMessage: (data: string) => void
|
||||
onConnected?: (ws: MihomoWebSocket) => Promise<void> | void
|
||||
cleanup?: () => void
|
||||
isMounted: () => boolean
|
||||
}
|
||||
|
||||
interface SharedSubscriptionEntry {
|
||||
refs: number
|
||||
ws: MihomoWebSocket | null
|
||||
reconnectTimer: ReturnType<typeof setTimeout> | null
|
||||
connecting: boolean
|
||||
refHolders: Set<MutableRefObject<MihomoWebSocket | null>>
|
||||
owners: Set<SharedSubscriptionOwner>
|
||||
activeOwner: SharedSubscriptionOwner | null
|
||||
closed: boolean
|
||||
connectWs: () => Promise<void>
|
||||
scheduleReconnect: () => Promise<void>
|
||||
}
|
||||
|
||||
const sharedSubscriptions = new Map<string, SharedSubscriptionEntry>()
|
||||
|
||||
const syncSharedWsRefs = (entry: SharedSubscriptionEntry) => {
|
||||
entry.refHolders.forEach((ref) => {
|
||||
ref.current = entry.ws
|
||||
})
|
||||
}
|
||||
|
||||
const pickActiveOwner = (entry: SharedSubscriptionEntry) => {
|
||||
if (entry.activeOwner?.isMounted()) return entry.activeOwner
|
||||
|
||||
for (const owner of entry.owners) {
|
||||
if (owner.isMounted()) {
|
||||
entry.activeOwner = owner
|
||||
return owner
|
||||
}
|
||||
}
|
||||
|
||||
entry.activeOwner = null
|
||||
return null
|
||||
}
|
||||
|
||||
const closeSharedSocket = async (entry: SharedSubscriptionEntry) => {
|
||||
const ws = entry.ws
|
||||
if (!ws) return
|
||||
|
||||
entry.ws = null
|
||||
syncSharedWsRefs(entry)
|
||||
await ws.close()
|
||||
}
|
||||
|
||||
const createSharedSubscriptionEntry = (
|
||||
connect: () => Promise<MihomoWebSocket>,
|
||||
): SharedSubscriptionEntry => {
|
||||
const entry: SharedSubscriptionEntry = {
|
||||
refs: 0,
|
||||
ws: null,
|
||||
reconnectTimer: null,
|
||||
connecting: false,
|
||||
refHolders: new Set(),
|
||||
owners: new Set(),
|
||||
activeOwner: null,
|
||||
closed: false,
|
||||
connectWs: async () => {},
|
||||
scheduleReconnect: async () => {},
|
||||
}
|
||||
|
||||
const clearReconnectTimer = () => {
|
||||
if (entry.reconnectTimer) {
|
||||
clearTimeout(entry.reconnectTimer)
|
||||
entry.reconnectTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
entry.connectWs = async () => {
|
||||
if (entry.closed || entry.connecting || entry.ws) return
|
||||
|
||||
entry.connecting = true
|
||||
try {
|
||||
const ws = await connect()
|
||||
if (entry.closed) {
|
||||
await ws.close()
|
||||
return
|
||||
}
|
||||
|
||||
entry.ws = ws
|
||||
syncSharedWsRefs(entry)
|
||||
clearReconnectTimer()
|
||||
|
||||
const owner = pickActiveOwner(entry)
|
||||
if (owner?.onConnected) {
|
||||
await owner.onConnected(ws)
|
||||
if (entry.closed) {
|
||||
await ws.close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ws.addListener((msg: Message) => {
|
||||
if (msg.type !== 'Text') return
|
||||
const activeOwner = pickActiveOwner(entry)
|
||||
if (!activeOwner) return
|
||||
|
||||
activeOwner.handleMessage(msg.data)
|
||||
})
|
||||
} catch (ignoreError) {
|
||||
if (!entry.closed && !entry.ws) {
|
||||
clearReconnectTimer()
|
||||
entry.reconnectTimer = setTimeout(entry.connectWs, RECONNECT_DELAY_MS)
|
||||
}
|
||||
} finally {
|
||||
entry.connecting = false
|
||||
}
|
||||
}
|
||||
|
||||
entry.scheduleReconnect = async () => {
|
||||
if (entry.closed) return
|
||||
|
||||
clearReconnectTimer()
|
||||
await closeSharedSocket(entry)
|
||||
if (!entry.closed) {
|
||||
entry.reconnectTimer = setTimeout(entry.connectWs, RECONNECT_DELAY_MS)
|
||||
}
|
||||
}
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors SWR's MutatorCallback: consumers can pass either a plain value or a
|
||||
* functional updater `(current?: T) => T`. The functional form is resolved
|
||||
@@ -64,8 +192,6 @@ export const useMihomoWsSubscription = <T>(
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const wsRef = useRef<MihomoWebSocket | null>(null)
|
||||
const wsFirstConnectionRef = useRef<boolean>(true)
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const resolveNextData = useCallback(
|
||||
(
|
||||
@@ -101,28 +227,15 @@ export const useMihomoWsSubscription = <T>(
|
||||
if (!subscriptionCacheKey) return
|
||||
|
||||
let isMounted = true
|
||||
|
||||
const clearReconnectTimer = () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
timeoutRef.current = null
|
||||
}
|
||||
let entry = sharedSubscriptions.get(subscriptionCacheKey)
|
||||
if (!entry) {
|
||||
entry = createSharedSubscriptionEntry(connect)
|
||||
sharedSubscriptions.set(subscriptionCacheKey, entry)
|
||||
}
|
||||
|
||||
const closeSocket = async () => {
|
||||
if (wsRef.current) {
|
||||
await wsRef.current.close()
|
||||
wsRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleReconnect = async () => {
|
||||
if (!isMounted) return
|
||||
clearReconnectTimer()
|
||||
await closeSocket()
|
||||
if (!isMounted) return
|
||||
timeoutRef.current = setTimeout(connectWs, RECONNECT_DELAY_MS)
|
||||
}
|
||||
entry.refs += 1
|
||||
entry.refHolders.add(wsRef)
|
||||
wsRef.current = entry.ws
|
||||
|
||||
let throttleCleanup: (() => void) | undefined
|
||||
let wrappedNext: NextFn<T>
|
||||
@@ -184,59 +297,51 @@ export const useMihomoWsSubscription = <T>(
|
||||
cleanup,
|
||||
} = setupHandlers({
|
||||
next: wrappedNext,
|
||||
scheduleReconnect,
|
||||
scheduleReconnect: entry.scheduleReconnect,
|
||||
isMounted: () => isMounted,
|
||||
})
|
||||
|
||||
const cleanupAll = () => {
|
||||
clearReconnectTimer()
|
||||
throttleCleanup?.()
|
||||
cleanup?.()
|
||||
void closeSocket()
|
||||
const owner: SharedSubscriptionOwner = {
|
||||
handleMessage: handleTextMessage,
|
||||
onConnected,
|
||||
cleanup: () => {
|
||||
throttleCleanup?.()
|
||||
cleanup?.()
|
||||
},
|
||||
isMounted: () => isMounted,
|
||||
}
|
||||
|
||||
const handleMessage = (msg: Message) => {
|
||||
if (msg.type !== 'Text') return
|
||||
handleTextMessage(msg.data)
|
||||
}
|
||||
|
||||
async function connectWs() {
|
||||
try {
|
||||
const ws_ = await connect()
|
||||
if (!isMounted) {
|
||||
await ws_.close()
|
||||
return
|
||||
}
|
||||
|
||||
wsRef.current = ws_
|
||||
clearReconnectTimer()
|
||||
|
||||
if (onConnected) {
|
||||
await onConnected(ws_)
|
||||
if (!isMounted) {
|
||||
await ws_.close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ws_.addListener(handleMessage)
|
||||
} catch (ignoreError) {
|
||||
if (!wsRef.current && isMounted) {
|
||||
timeoutRef.current = setTimeout(connectWs, RECONNECT_DELAY_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wsFirstConnectionRef.current || !wsRef.current) {
|
||||
wsFirstConnectionRef.current = false
|
||||
cleanupAll()
|
||||
void connectWs()
|
||||
entry.owners.add(owner)
|
||||
if (!entry.activeOwner) {
|
||||
entry.activeOwner = owner
|
||||
}
|
||||
void entry.connectWs()
|
||||
|
||||
return () => {
|
||||
isMounted = false
|
||||
wsFirstConnectionRef.current = true
|
||||
cleanupAll()
|
||||
entry.refHolders.delete(wsRef)
|
||||
wsRef.current = null
|
||||
entry.owners.delete(owner)
|
||||
owner.cleanup?.()
|
||||
|
||||
if (entry.activeOwner === owner) {
|
||||
entry.activeOwner = null
|
||||
const nextOwner = pickActiveOwner(entry)
|
||||
if (entry.ws && nextOwner?.onConnected) {
|
||||
void nextOwner.onConnected(entry.ws)
|
||||
}
|
||||
}
|
||||
|
||||
entry.refs -= 1
|
||||
if (entry.refs <= 0) {
|
||||
entry.closed = true
|
||||
if (entry.reconnectTimer) {
|
||||
clearTimeout(entry.reconnectTimer)
|
||||
entry.reconnectTimer = null
|
||||
}
|
||||
sharedSubscriptions.delete(subscriptionCacheKey)
|
||||
void closeSharedSocket(entry)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps, @eslint-react/exhaustive-deps
|
||||
|
||||
Reference in New Issue
Block a user