perf(inbounds): keep unchanged rows and online sets across websocket pushes

Every client_stats push carries the totals of all inbounds, and
applyClientStatsEvent rebuilt each row it listed, so every push replaced
all rows, re-ran the client rollup (a JSON parse of every inbound's
settings) and re-rendered the whole table even when no number moved. Every
traffic push also built new online and active maps, re-running the same
rollup.

Rows are now rebuilt only when their totals or a client's numbers change,
and the previous maps are kept when a push repeats the same sets. Measured
in jsdom with 450 inbounds of 50 clients each: an unchanged client_stats
push went from 7.9ms to 0.6ms with no row rebuilt, and a repeated traffic
push from 13.5ms to 8.3ms without the rollup.
This commit is contained in:
Sanaei
2026-09-15 21:06:32 +02:00
parent 3c1498d806
commit 7fc86f87de
2 changed files with 143 additions and 5 deletions
+36 -5
View File
@@ -119,6 +119,18 @@ function toGuidOnlineMap(data: Record<string, string[]>): Map<string, Set<string
return map;
}
// Most pushes repeat the previous online sets; handing back a new Map anyway
// re-ran the client rollup over every inbound on each traffic event.
function sameGuidSets(a: Map<string, Set<string>>, b: Map<string, Set<string>>): boolean {
if (a.size !== b.size) return false;
for (const [key, set] of b) {
const prev = a.get(key);
if (!prev || prev.size !== set.size) return false;
for (const value of set) if (!prev.has(value)) return false;
}
return true;
}
async function fetchLastOnlineMap(): Promise<Record<string, number>> {
const msg = await HttpUtil.post('/panel/api/clients/lastOnline', undefined, { silent: true });
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch lastOnline');
@@ -440,10 +452,12 @@ export function useInbounds() {
setOnlineClients(p.onlineClients);
}
if (p.onlineByGuid && typeof p.onlineByGuid === 'object') {
setOnlineByGuid(toGuidOnlineMap(p.onlineByGuid));
const next = toGuidOnlineMap(p.onlineByGuid);
setOnlineByGuid((prev) => (sameGuidSets(prev, next) ? prev : next));
}
if (p.activeInbounds && typeof p.activeInbounds === 'object') {
setActiveByGuid(toGuidOnlineMap(p.activeInbounds));
const next = toGuidOnlineMap(p.activeInbounds);
setActiveByGuid((prev) => (sameGuidSets(prev, next) ? prev : next));
}
if (p.lastOnlineMap && typeof p.lastOnlineMap === 'object') {
setLastOnlineMap((prev) => ({ ...prev, ...p.lastOnlineMap! }));
@@ -537,8 +551,7 @@ export function useInbounds() {
? stats.map((stat) => {
const su = byEmail.get(stat.email);
if (!su) return stat;
statsTouched = true;
return {
const merged = {
...stat,
up: typeof su.up === 'number' ? su.up : stat.up,
down: typeof su.down === 'number' ? su.down : stat.down,
@@ -546,9 +559,27 @@ export function useInbounds() {
expiryTime: typeof su.expiryTime === 'number' ? su.expiryTime : stat.expiryTime,
enable: typeof su.enable === 'boolean' ? su.enable : stat.enable,
} as ClientStats;
if (
merged.up === stat.up &&
merged.down === stat.down &&
merged.total === stat.total &&
merged.expiryTime === stat.expiryTime &&
merged.enable === stat.enable
) {
return stat;
}
statsTouched = true;
return merged;
})
: null;
if (!upd && !statsTouched) return ib;
// Every push lists all inbounds' totals, so only a row whose numbers moved counts.
const inboundMoved =
!!upd &&
((typeof upd.up === 'number' && upd.up !== ib.up) ||
(typeof upd.down === 'number' && upd.down !== ib.down) ||
(typeof upd.total === 'number' && upd.total !== ib.total) ||
(typeof upd.enable === 'boolean' && upd.enable !== ib.enable));
if (!inboundMoved && !statsTouched) return ib;
touched = true;
const row = new DBInbound(ib as DBInboundInit) as DBInboundInstance;
if (upd) {