fix(optimizer): ускорить точный выбор проб из большого пула
This commit is contained in:
1 parent
c6dfa5267b
commit
52d8a05480
8 files changed
+306
-32
No files matched your search
+62
-28
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user