perf(delay): cache latency updates and smooth proxy list refresh
- track delay as structured updates with TTL-backed cache - batch listener notifications to avoid render storms during checks - surface cached latency in proxy items for quicker, steadier UI feedback
This commit is contained in:
@@ -2,16 +2,111 @@ import { delayProxyByName, ProxyDelay } from "tauri-plugin-mihomo-api";
|
||||
|
||||
const hashKey = (name: string, group: string) => `${group ?? ""}::${name}`;
|
||||
|
||||
export interface DelayUpdate {
|
||||
delay: number;
|
||||
elapsed?: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
const CACHE_TTL = 30 * 60 * 1000;
|
||||
|
||||
class DelayManager {
|
||||
private cache = new Map<string, [number, number]>();
|
||||
private cache = new Map<string, DelayUpdate>();
|
||||
private urlMap = new Map<string, string>();
|
||||
|
||||
// 每个item的监听
|
||||
private listenerMap = new Map<string, (time: number) => void>();
|
||||
// 每个节点的监听
|
||||
private listenerMap = new Map<string, (update: DelayUpdate) => void>();
|
||||
|
||||
// 每个分组的监听
|
||||
private groupListenerMap = new Map<string, () => void>();
|
||||
|
||||
private pendingItemUpdates = new Map<string, DelayUpdate[]>();
|
||||
private pendingGroupUpdates = new Set<string>();
|
||||
private itemFlushScheduled = false;
|
||||
private groupFlushScheduled = false;
|
||||
|
||||
private scheduleItemFlush() {
|
||||
if (this.itemFlushScheduled) return;
|
||||
this.itemFlushScheduled = true;
|
||||
|
||||
const run = () => {
|
||||
this.itemFlushScheduled = false;
|
||||
const updates = this.pendingItemUpdates;
|
||||
this.pendingItemUpdates = new Map();
|
||||
|
||||
updates.forEach((queue, key) => {
|
||||
const listener = this.listenerMap.get(key);
|
||||
if (!listener) return;
|
||||
|
||||
queue.forEach((update) => {
|
||||
try {
|
||||
listener(update);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[DelayManager] 通知节点延迟监听器失败: ${key}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
if (typeof window.requestAnimationFrame === "function") {
|
||||
window.requestAnimationFrame(run);
|
||||
return;
|
||||
}
|
||||
if (typeof window.setTimeout === "function") {
|
||||
window.setTimeout(run, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Promise.resolve().then(run);
|
||||
}
|
||||
|
||||
private scheduleGroupFlush() {
|
||||
if (this.groupFlushScheduled) return;
|
||||
this.groupFlushScheduled = true;
|
||||
|
||||
const run = () => {
|
||||
this.groupFlushScheduled = false;
|
||||
const groups = this.pendingGroupUpdates;
|
||||
this.pendingGroupUpdates = new Set();
|
||||
|
||||
groups.forEach((group) => {
|
||||
const listener = this.groupListenerMap.get(group);
|
||||
if (!listener) return;
|
||||
try {
|
||||
listener();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[DelayManager] 通知分组延迟监听器失败: ${group}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
if (typeof window.requestAnimationFrame === "function") {
|
||||
window.requestAnimationFrame(run);
|
||||
return;
|
||||
}
|
||||
if (typeof window.setTimeout === "function") {
|
||||
window.setTimeout(run, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Promise.resolve().then(run);
|
||||
}
|
||||
|
||||
private queueGroupNotification(group: string) {
|
||||
this.pendingGroupUpdates.add(group);
|
||||
this.scheduleGroupFlush();
|
||||
}
|
||||
|
||||
setUrl(group: string, url: string) {
|
||||
console.log(`[DelayManager] 设置测试URL,组: ${group}, URL: ${url}`);
|
||||
this.urlMap.set(group, url);
|
||||
@@ -26,7 +121,11 @@ class DelayManager {
|
||||
return url || "https://cp.cloudflare.com/generate_204";
|
||||
}
|
||||
|
||||
setListener(name: string, group: string, listener: (time: number) => void) {
|
||||
setListener(
|
||||
name: string,
|
||||
group: string,
|
||||
listener: (update: DelayUpdate) => void,
|
||||
) {
|
||||
const key = hashKey(name, group);
|
||||
this.listenerMap.set(key, listener);
|
||||
}
|
||||
@@ -44,34 +143,60 @@ class DelayManager {
|
||||
this.groupListenerMap.delete(group);
|
||||
}
|
||||
|
||||
setDelay(name: string, group: string, delay: number) {
|
||||
setDelay(
|
||||
name: string,
|
||||
group: string,
|
||||
delay: number,
|
||||
meta?: { elapsed?: number },
|
||||
): DelayUpdate {
|
||||
const key = hashKey(name, group);
|
||||
console.log(
|
||||
`[DelayManager] 设置延迟,代理: ${name}, 组: ${group}, 延迟: ${delay}`,
|
||||
);
|
||||
const update: DelayUpdate = {
|
||||
delay,
|
||||
elapsed: meta?.elapsed,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
this.cache.set(key, [delay, Date.now()]);
|
||||
const listener = this.listenerMap.get(key);
|
||||
if (listener) listener(delay);
|
||||
this.cache.set(key, update);
|
||||
|
||||
const queue = this.pendingItemUpdates.get(key);
|
||||
if (queue) {
|
||||
queue.push(update);
|
||||
} else {
|
||||
this.pendingItemUpdates.set(key, [update]);
|
||||
}
|
||||
this.scheduleItemFlush();
|
||||
|
||||
return update;
|
||||
}
|
||||
|
||||
getDelayUpdate(name: string, group: string) {
|
||||
const key = hashKey(name, group);
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) return undefined;
|
||||
|
||||
if (Date.now() - entry.updatedAt > CACHE_TTL) {
|
||||
this.cache.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { ...entry };
|
||||
}
|
||||
|
||||
getDelay(name: string, group: string) {
|
||||
const key = hashKey(name, group);
|
||||
const val = this.cache.get(key);
|
||||
if (!val) return -1;
|
||||
|
||||
// 缓存30分钟
|
||||
if (Date.now() - val[1] > 30 * 60 * 1000) {
|
||||
return -1;
|
||||
}
|
||||
return val[0];
|
||||
const update = this.getDelayUpdate(name, group);
|
||||
return update ? update.delay : -1;
|
||||
}
|
||||
|
||||
/// 暂时修复provider的节点延迟排序的问题
|
||||
getDelayFix(proxy: IProxyItem, group: string) {
|
||||
if (!proxy.provider) {
|
||||
const delay = this.getDelay(proxy.name, group);
|
||||
if (delay >= 0 || delay === -2) return delay;
|
||||
const update = this.getDelayUpdate(proxy.name, group);
|
||||
if (update && (update.delay >= 0 || update.delay === -2)) {
|
||||
return update.delay;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 history 属性的安全检查
|
||||
@@ -82,7 +207,11 @@ class DelayManager {
|
||||
return -1;
|
||||
}
|
||||
|
||||
async checkDelay(name: string, group: string, timeout: number) {
|
||||
async checkDelay(
|
||||
name: string,
|
||||
group: string,
|
||||
timeout: number,
|
||||
): Promise<DelayUpdate> {
|
||||
console.log(
|
||||
`[DelayManager] 开始测试延迟,代理: ${name}, 组: ${group}, 超时: ${timeout}ms`,
|
||||
);
|
||||
@@ -91,16 +220,16 @@ class DelayManager {
|
||||
this.setDelay(name, group, -2);
|
||||
|
||||
let delay = -1;
|
||||
let elapsed = 0;
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const url = this.getUrl(group);
|
||||
console.log(`[DelayManager] 调用API测试延迟,代理: ${name}, URL: ${url}`);
|
||||
|
||||
// 记录开始时间,用于计算实际延迟
|
||||
const startTime = Date.now();
|
||||
|
||||
// 设置超时处理, delay = 0 为超时
|
||||
const timeoutPromise = new Promise<ProxyDelay>((resolve, _) => {
|
||||
const timeoutPromise = new Promise<ProxyDelay>((resolve) => {
|
||||
setTimeout(() => resolve({ delay: 0 }), timeout);
|
||||
});
|
||||
|
||||
@@ -117,6 +246,7 @@ class DelayManager {
|
||||
}
|
||||
|
||||
delay = result.delay;
|
||||
elapsed = elapsedTime;
|
||||
console.log(
|
||||
`[DelayManager] 延迟测试完成,代理: ${name}, 结果: ${delay}ms`,
|
||||
);
|
||||
@@ -125,10 +255,10 @@ class DelayManager {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
console.error(`[DelayManager] 延迟测试出错,代理: ${name}`, error);
|
||||
delay = 1e6; // error
|
||||
elapsed = Date.now() - startTime;
|
||||
}
|
||||
|
||||
this.setDelay(name, group, delay);
|
||||
return delay;
|
||||
return this.setDelay(name, group, delay, { elapsed });
|
||||
}
|
||||
|
||||
async checkListDelay(
|
||||
@@ -165,7 +295,9 @@ class DelayManager {
|
||||
}
|
||||
|
||||
await this.checkDelay(currName, group, timeout);
|
||||
if (listener) listener();
|
||||
if (listener) {
|
||||
this.queueGroupNotification(group);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[DelayManager] 批量测试单个代理出错,代理: ${currName}`,
|
||||
|
||||
Reference in New Issue
Block a user