Compare commits

..
Author SHA1 Message Date
Tunglies c450e830de remove useless test 2026-11-11 07:26:44 +08:00
Tunglies d88fe8dec2 test(connections): retain only allocation and formatting guards
Reduce snapshot and table tests to reference reuse, notification suppression, and visible-row formatting cost.
2026-11-10 10:56:31 +08:00
Tunglies dc641bf1f4 test(connections): focus clock regression on timer cost
Replace behavioral clock assertions with one resource-count check for shared, hidden, and released intervals.
2026-11-10 10:55:09 +08:00
Tunglies 73b18a28ed test(connections): limit coverage to changed behavior
Keep snapshot sharing, speed decay, bounded history, numeric sorting, and visibility regression guards. Remove unrelated clear-history and pending-delivery assertions.
2026-11-10 07:52:07 +08:00
Tunglies f84be8df72 perf(connections): reduce snapshot and table allocation churn
Reuse unchanged connection lists, sort traffic columns without display snapshots, and suspend the relative-time clock while hidden. Keep rule rows indexed and release resize listeners on unmount.
2026-11-10 07:52:02 +08:00
10 changed files with 121 additions and 58 deletions
Generated
+6 -7
View File
@@ -7520,15 +7520,14 @@ dependencies = [
[[package]]
name = "tauri-plugin-http"
version = "2.9.0"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c278945f30912c8c32728265c66d16ca1b139509c4874d00579f5c8c5276605a"
checksum = "2c57e3451553f95a1db3bfed6a358a77ce5080b6399a242c6eadac113f97abd2"
dependencies = [
"bytes",
"cookie_store",
"data-url",
"http",
"percent-encoding",
"regex",
"reqwest 0.12.28",
"schemars 0.8.22",
@@ -7570,9 +7569,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-notification"
version = "2.8.1"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14a226bfb8189ec4ab2c1936e66330ad06c44d9f6c54a674338208491dee177f"
checksum = "5bc6c194a95a535abdfbe3bc1af9a43935b8b9b29c216b8e71af8b59132703cf"
dependencies = [
"log",
"notify-rust",
@@ -7590,9 +7589,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-opener"
version = "2.7.3"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54592c6718b42ab198bf11bda6a428a3553ca2f2f8e506df56f6a063ec328331"
checksum = "53810be1b4fed22bb6ac3f4cab5a2b80d9391d1af436bda145607532bc0bb219"
dependencies = [
"dunce",
"glob",
+1
View File
@@ -48,6 +48,7 @@
- 优化连接、规则和日志筛选:切换页面后保留过滤条件
- 优化全局模式代理列表:仅显示 `GLOBAL` 节点,选中其中的策略组时才显示该组
- 优化连接和规则页面的资源占用,减少连接列表更新及排序时的卡顿
**🍎 macOS**
+2 -2
View File
@@ -67,7 +67,7 @@
"dayjs": "1.11.23",
"foxact": "^0.3.12",
"foxts": "^5.9.1",
"i18next": "^27.0.0",
"i18next": "^26.4.2",
"js-yaml": "^5.4.2",
"lodash-es": "^4.18.1",
"meta-json-schema": "^1.19.31",
@@ -79,7 +79,7 @@
"react-dom": "19.3.1",
"react-error-boundary": "6.1.6",
"react-hook-form": "^7.88.0",
"react-i18next": "18.0.1",
"react-i18next": "17.1.13",
"react-markdown": "10.1.0",
"react-router": "^8.4.0",
"rehype-raw": "^7.0.0",
+21 -18
View File
@@ -239,8 +239,8 @@ importers:
specifier: ^5.9.1
version: 5.9.1
i18next:
specifier: ^27.0.0
version: 27.0.1(typescript@6.0.3)
specifier: ^26.4.2
version: 26.4.2(typescript@6.0.3)
js-yaml:
specifier: ^5.4.2
version: 5.4.2
@@ -275,8 +275,8 @@ importers:
specifier: ^7.88.0
version: 7.88.0(react@19.3.1)
react-i18next:
specifier: 18.0.1
version: 18.0.1(@types/react@19.3.0)(i18next@27.0.1(typescript@6.0.3))(react@19.3.1)(typescript@6.0.3)
specifier: 17.1.13
version: 17.1.13(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3)
react-markdown:
specifier: 10.1.0
version: 10.1.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0)
@@ -3689,9 +3689,8 @@ packages:
engines: {node: '>=18'}
hasBin: true
i18next@27.0.1:
resolution: {integrity: sha512-KaE5ozP8UUeVpOJIwMVbbnpRKpyYQuxd9ZbseJoDzezE3nvLnZZQn20pkzyIJwPI2Uet/Qs3WzPTG812f6F/Eg==}
engines: {node: '>=22'}
i18next@26.4.2:
resolution: {integrity: sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug==}
peerDependencies:
typescript: ^5 || ^6 || ^7
peerDependenciesMeta:
@@ -4397,16 +4396,18 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
react-i18next@18.0.1:
resolution: {integrity: sha512-kPWXI0E/lrPj3+/umcKNzYZVnWmEedLiamMzDIHfbs+5ANf6yzvlZsjKou8f7kaGkfMMDLIZBejV+4aSN1CyBw==}
engines: {node: '>=22'}
react-i18next@17.1.13:
resolution: {integrity: sha512-ZxhOSlyKSKPtbUNPLwYMmE3w3bhoc0jaZJZ6bc7wGOT6cLbrbs//6m01HxCLtFdSjHvFpwFwoPhxxzIlHSs9cw==}
peerDependencies:
'@types/react': '*'
i18next: '>= 27.0.1'
react: '>= 18.0.0'
i18next: '>= 26.2.0'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
typescript: ^5 || ^6 || ^7
peerDependenciesMeta:
'@types/react':
react-dom:
optional: true
react-native:
optional: true
typescript:
optional: true
@@ -8532,7 +8533,7 @@ snapshots:
husky@9.1.7: {}
i18next@27.0.1(typescript@6.0.3):
i18next@26.4.2(typescript@6.0.3):
optionalDependencies:
typescript: 6.0.3
@@ -9356,13 +9357,15 @@ snapshots:
dependencies:
react: 19.3.1
react-i18next@18.0.1(@types/react@19.3.0)(i18next@27.0.1(typescript@6.0.3))(react@19.3.1)(typescript@6.0.3):
react-i18next@17.1.13(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3):
dependencies:
'@babel/runtime': 7.29.7
html-parse-stringify: 4.0.1
i18next: 27.0.1(typescript@6.0.3)
i18next: 26.4.2(typescript@6.0.3)
react: 19.3.1
use-sync-external-store: 1.7.0(react@19.3.1)
optionalDependencies:
'@types/react': 19.3.0
react-dom: 19.3.1(react@19.3.1)
typescript: 6.0.3
react-is@16.13.1: {}
+3 -3
View File
@@ -72,7 +72,7 @@ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch
] }
network-interface = { version = "2.0.5", features = ["serde"] }
tauri-plugin-shell = "2.6.0"
tauri-plugin-opener = "2.7.3"
tauri-plugin-opener = "2.7.2"
tauri-plugin-dialog = "2.8.4"
tauri-plugin-fs = "2.7.1"
tauri-plugin-deep-link = "2.6.3"
@@ -87,9 +87,9 @@ getrandom = "0.4.3"
futures = "0.3.34"
gethostname = "1.1.0"
scopeguard = "1.2.0"
tauri-plugin-notification = "2.8.1"
tauri-plugin-notification = "2.8.0"
backon = { version = "1.6.0", features = ["tokio-sleep"] }
tauri-plugin-http = "2.9.0"
tauri-plugin-http = "2.8.3"
tracing = { workspace = true }
tauri-plugin-devtools = { version = "2.2.3", optional = true }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" }
@@ -8,7 +8,7 @@ let timerId: number | null = null
const listeners = new Set<RelativeTimeListener>()
const startTimer = () => {
if (timerId !== null) return
if (timerId !== null || document.hidden || listeners.size === 0) return
currentTime = Date.now()
timerId = window.setInterval(() => {
@@ -18,19 +18,34 @@ const startTimer = () => {
}
const stopTimer = () => {
if (listeners.size > 0 || timerId === null) return
if (timerId === null) return
window.clearInterval(timerId)
timerId = null
}
const handleVisibilityChange = () => {
if (document.hidden) {
stopTimer()
} else {
startTimer()
listeners.forEach((listener) => listener())
}
}
const subscribeRelativeTime = (listener: RelativeTimeListener) => {
listeners.add(listener)
if (listeners.size === 1) {
document.addEventListener('visibilitychange', handleVisibilityChange)
}
startTimer()
return () => {
listeners.delete(listener)
stopTimer()
if (listeners.size === 0) {
stopTimer()
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
}
}
@@ -223,6 +223,16 @@ const compareConnectionCellValue = (
right: IConnectionsItem,
getSnapshot: (row: IConnectionsItem) => TableRowSnapshot,
) => {
switch (field) {
case 'download':
return (left.download ?? 0) - (right.download ?? 0)
case 'upload':
return (left.upload ?? 0) - (right.upload ?? 0)
case 'dlSpeed':
return (left.curDownload ?? 0) - (right.curDownload ?? 0)
case 'ulSpeed':
return (left.curUpload ?? 0) - (right.curUpload ?? 0)
}
const leftValue = getConnectionCellValue(field, getSnapshot(left))
const rightValue = getConnectionCellValue(field, getSnapshot(right))
@@ -491,6 +501,8 @@ export const ConnectionTable = (props: Props) => {
const [sorting, setSorting] = useState<SortingState | null>(null)
const [viewport, setViewport] = useState({ scrollTop: 0, height: 0 })
const scrollContainerRef = useRef<HTMLDivElement | null>(null)
const resizeCleanupRef = useRef<(() => void) | null>(null)
useEffect(() => () => resizeCleanupRef.current?.(), [])
const rowSnapshotCacheRef = useRef(new Map<string, TableRowSnapshot>())
const getRowSnapshot = useCallback((row: IConnectionsItem) => {
const cache = rowSnapshotCacheRef.current
@@ -664,6 +676,7 @@ export const ConnectionTable = (props: Props) => {
minWidth: number,
maxWidth: number | undefined,
) => {
resizeCleanupRef.current?.()
const handleMove = (clientX: number) => {
const nextWidth = Math.max(
minWidth,
@@ -686,8 +699,10 @@ export const ConnectionTable = (props: Props) => {
window.removeEventListener('touchmove', handleTouchMove)
window.removeEventListener('touchend', cleanup)
window.removeEventListener('touchcancel', cleanup)
resizeCleanupRef.current = null
}
resizeCleanupRef.current = cleanup
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('mouseup', cleanup)
window.addEventListener('touchmove', handleTouchMove, { passive: true })
+4 -3
View File
@@ -16,7 +16,8 @@ const COLOR = [
]
interface Props {
value: Rule & { lineNo: number }
value: Rule
lineNo: number
}
const parseColor = (text: string) => {
@@ -31,7 +32,7 @@ const parseColor = (text: string) => {
}
const RuleItem = (props: Props) => {
const { value } = props
const { value, lineNo } = props
return (
<Item sx={{ borderBottom: '1px solid var(--divider-color)' }}>
@@ -40,7 +41,7 @@ const RuleItem = (props: Props) => {
variant="body2"
sx={{ lineHeight: 2, minWidth: 30, mr: 2.25, textAlign: 'center' }}
>
{value.lineNo}
{lineNo}
</Typography>
<Box sx={{ userSelect: 'text' }}>
+40 -14
View File
@@ -150,25 +150,49 @@ const mergeConnectionSnapshot = (
const nextConnections = payload.connections ?? []
const previousActive = previous.activeConnections ?? []
const previousClosed = previous.closedConnections ?? []
const previousActiveById = new Map<string, IConnectionsItem>()
const sameOrder =
nextConnections.length === previousActive.length &&
nextConnections.every(
(connection, i) => connection.id === previousActive[i].id,
)
let previousActiveById: Map<string, IConnectionsItem> | undefined
for (let i = 0; i < previousActive.length; i++) {
const previousConnection = previousActive[i]
previousActiveById.set(previousConnection.id, previousConnection)
if (!sameOrder) {
previousActiveById = new Map()
for (let i = 0; i < previousActive.length; i++) {
const previousConnection = previousActive[i]
previousActiveById.set(previousConnection.id, previousConnection)
}
}
const activeConnections: IConnectionsItem[] = []
let activeConnections = sameOrder ? previousActive : []
for (let i = 0; i < nextConnections.length; i++) {
const connection = nextConnections[i]
const previousConnection = previousActiveById.get(connection.id)
if (previousConnection) previousActiveById.delete(connection.id)
activeConnections.push(normalizeConnection(connection, previousConnection))
const previousConnection = sameOrder
? previousActive[i]
: previousActiveById?.get(connection.id)
previousActiveById?.delete(connection.id)
const normalized = normalizeConnection(connection, previousConnection)
if (activeConnections[i] === normalized) continue
if (activeConnections === previousActive) {
activeConnections = previousActive.slice()
}
activeConnections[i] = normalized
}
if (previousActiveById.size === 0) {
const uploadTotal = payload.uploadTotal ?? 0
const downloadTotal = payload.downloadTotal ?? 0
if (!previousActiveById?.size) {
if (
activeConnections === previousActive &&
uploadTotal === previous.uploadTotal &&
downloadTotal === previous.downloadTotal
) {
return previous
}
return {
uploadTotal: payload.uploadTotal ?? 0,
downloadTotal: payload.downloadTotal ?? 0,
uploadTotal,
downloadTotal,
activeConnections,
closedConnections: previousClosed,
}
@@ -198,8 +222,8 @@ const mergeConnectionSnapshot = (
}
return {
uploadTotal: payload.uploadTotal ?? 0,
downloadTotal: payload.downloadTotal ?? 0,
uploadTotal,
downloadTotal,
activeConnections,
closedConnections,
}
@@ -227,7 +251,9 @@ const flushPendingMessage = () => {
lastFlushAt = Date.now()
connectionData = mergeConnectionSnapshot(payload, connectionData)
const nextData = mergeConnectionSnapshot(payload, connectionData)
if (nextData === connectionData) return
connectionData = nextData
notifyConnectionListeners()
}
+11 -8
View File
@@ -34,13 +34,11 @@ const RulesPage = () => {
}, [refreshRules, refreshRuleProviders, pageVisible])
const filteredRules = useMemo(() => {
const rulesWithLineNo = rules.map((item, index) => ({
...item,
// UI-only derived data; keep app context/SWR data immutable
lineNo: index + 1,
}))
return rulesWithLineNo.filter((item) => match(item.payload ?? ''))
const indices: number[] = []
for (let i = 0; i < rules.length; i++) {
if (match(rules[i].payload ?? '')) indices.push(i)
}
return indices
}, [rules, match])
const handleScroll = useCallback((e: Event) => {
@@ -90,7 +88,12 @@ const RulesPage = () => {
ref={virtuosoRef}
count={filteredRules.length}
estimateSize={40}
renderItem={(i) => <RuleItem value={filteredRules[i]} />}
renderItem={(i) => (
<RuleItem
value={rules[filteredRules[i]]}
lineNo={filteredRules[i] + 1}
/>
)}
style={{ flex: 1 }}
onScroll={handleScroll}
/>