fix(optimizer): ускорить точный выбор проб из большого пула

This commit is contained in:
Denozordec committed 2026-10-07 12:24:07 +07:00
1 parent c6dfa5267b
commit 52d8a05480
8 files changed
+306 -32

No files matched your search

+62 -28
View File
@@ -307,7 +307,7 @@ export function buildLiveOptimizerData(
const probesForHome = speedProbes
.filter((p) => p.enabled !== false && p.srcServerId === home.id)
function scoreProbeForWanIface(p: RouteOptimizerSpeedProbe, wanIface: string): number {
function scoreProbeForWanIface(p: RouteOptimizerSpeedProbe, wanIface: string, at?: number): number {
const want = wanIface.trim().toLowerCase()
const got = String(p.srcInterface ?? "").trim().toLowerCase()
if (got && want && got !== want) return -1
@@ -317,7 +317,7 @@ export function buildLiveOptimizerData(
: (!want && got) ? 15
: 5
const freshness =
p.lastPingAt ? Math.max(0, 20 - Math.floor((Date.now() - Date.parse(p.lastPingAt)) / (60 * 60 * 1000))) : 0
p.lastPingAt ? Math.max(0, 20 - Math.floor(((at ?? Date.now()) - Date.parse(p.lastPingAt)) / (60 * 60 * 1000))) : 0
return ifaceScore
+ (p.lastPingRttMs != null ? 20 : 0)
+ (p.lastTxAvgMbps != null ? 10 : 0)
@@ -336,56 +336,90 @@ export function buildLiveOptimizerData(
let bestSum = -Infinity
let bestAssign: Map<string, RouteOptimizerSpeedProbe> | null = null
const wanList = [...home.wans]
const shouldSearch = wanList.length <= 7 && wanList.length <= pool.length
// Only the bounded-WAN exact search needs this matrix. A single time
// snapshot keeps rankings stable if a calculation crosses an hour boundary.
const scored = shouldSearch ? (() => {
const at = Date.now()
return pool.map(probe => ({ probe, values: wanList.map(wan => scoreProbeForWanIface(probe, wan.iface, at)) }))
})() : []
// A score-first greedy path is a valid leaf of the same forced-pick tree.
// Seed only its total, never its assignment: the original DFS must still
// visit the first equal leaf, so ties keep the original pool order.
if (shouldSearch && scored.every(candidate => candidate.values.every(Number.isFinite))) {
const used = new Set<string>()
let lower = 0
for (let i = 0; i < wanList.length; i++) {
let best: (typeof scored)[number] | undefined
let maximum = -1
for (const candidate of scored) {
if (!used.has(candidate.probe.id) && candidate.values[i]! > maximum) {
best = candidate
maximum = candidate.values[i]!
}
}
if (best) { used.add(best.probe.id); lower += maximum }
}
bestSum = lower
}
function dfs(i: number, used: Set<string>, cur: Map<string, RouteOptimizerSpeedProbe>, sum: number) {
if (i === wanList.length) {
if (sum > bestSum) {
if (sum > bestSum || (!bestAssign && sum === bestSum)) {
bestSum = sum
bestAssign = new Map(cur)
}
return
}
if (bestAssign) {
// Ignore competition for probes: independent maxima are an upper bound
// on every possible completion, including a WAN with no compatible probe.
let upper = sum
const candidateMax = new Map<string, number>()
for (let next = i; next < wanList.length; next++) {
let maximum = 0
for (const candidate of pool) {
if (used.has(candidate.id)) continue
const value = scoreProbeForWanIface(candidate, wanList[next]!.iface)
maximum = Math.max(maximum, value)
candidateMax.set(candidate.id, Math.max(candidateMax.get(candidate.id) ?? 0, value))
}
upper += maximum
// Independent WAN maxima and the highest distinct-probe maxima are
// both upper bounds: each completion picks at most one probe per WAN.
let upper = sum
const candidateMax = new Map<string, number>()
for (let next = i; next < wanList.length; next++) {
let maximum = 0
for (const candidate of scored) {
if (used.has(candidate.probe.id)) continue
const value = candidate.values[next]!
maximum = Math.max(maximum, value)
candidateMax.set(candidate.probe.id, Math.max(candidateMax.get(candidate.probe.id) ?? 0, value))
}
// A completion uses at most one distinct probe per remaining WAN.
// Its weight is bounded by the highest remaining probe maxima, even
// when those maxima cannot actually be assigned to different WANs.
const distinctUpper = sum + [...candidateMax.values()].sort((a, b) => b - a)
.slice(0, wanList.length - i).reduce((total, value) => total + value, 0)
upper = Math.min(upper, distinctUpper)
// Equal totals keep the first assignment in the original DFS order.
if (upper <= bestSum) return
upper += maximum
}
const remaining = wanList.length - i
const top = [...candidateMax.entries()].sort((a, b) => b[1] - a[1]).slice(0, remaining)
const topSum = top.reduce((total, [, value]) => total + value, 0)
const topById = new Map(top)
const distinctUpper = sum + topSum
upper = Math.min(upper, distinctUpper)
// Equal totals keep the first assignment in the original DFS order.
if (upper < bestSum || (bestAssign && upper <= bestSum)) return
const wan = wanList[i]!
let picked = false
for (const p of pool) {
for (const candidate of scored) {
const p = candidate.probe
if (used.has(p.id)) continue
const sc = scoreProbeForWanIface(p, wan.iface)
const sc = candidate.values[i]!
if (sc < 0) continue
picked = true
// Relax the child using this node's probe maxima. Removing a top
// probe leaves K-1 entries; otherwise remove the lowest of the top K.
// This avoids rescanning the whole pool for an already losing sibling.
const removed = topById.get(p.id) ?? (top.length === remaining ? top.at(-1)![1] : 0)
const childUpper = sum + sc + topSum - removed
if (childUpper < bestSum || (bestAssign && childUpper <= bestSum)) continue
used.add(p.id)
cur.set(wan.id, p)
dfs(i + 1, used, cur, sum + sc)
cur.delete(wan.id)
used.delete(p.id)
// No remaining sibling can improve a result reaching this node bound.
if (bestAssign && bestSum >= upper) return
}
if (!picked) dfs(i + 1, used, cur, sum)
}
if (wanList.length <= 7 && wanList.length <= pool.length) {
if (shouldSearch) {
dfs(0, new Set(), new Map(), 0)
}