fix(optimizer): убрать лишние поиски и расчёты маршрутов

This commit is contained in:
Denozordec committed 2026-10-07 13:16:12 +07:00
1 parent 057f0d352a
commit 4fee9e371f
8 files changed
+164 -9

No files matched your search

+9
View File
@@ -190,3 +190,12 @@ Pure child CPU1000: equal1217.5→15.6ms,varied7967.5→14.5ms,freshness10с tim
16 main browser +2 повторных новых профиля после copy-уточнения прошли:1000JH и1000Exit отдельно (без опасного cross product), repetitions/two sites/1000sites/long80chars/unknown/empty/offline/disabled/503 cache/recovery.320px без overflow, caption≤80px, scoped WCAG axe passed. Non-incremental TypeScript до dev, full page/helper/test lint и окончательный production build прошли. ReUI API/validate_usage проверены; primitive/theme не изменены. Historical9 optimizer PNG восстановлены, новые docs/ux-ui/screens/optimizer-sites-{jump-host,exit-node}-320.png просмотрены.
Далее12.3.68 pure child CPU profile full-route product≤100000/timeout10s/memory cap; не грузить тяжёлый baseline в UI. Остаточные DFS worst-case/full-route memory/native selector и внешняя12.5 не объявляются принятыми. Общая main/context регрессия относится к ранее записанным HEAD; текущий footer delta проверен профильными16+2.
## 12.3.69 — lookup leg и исключение ненужных расчётов без WAN
2026-10-07. WAN/JH map сохраняет первый matching leg, lookup вынесен из Exit loop. JH/Exit legs не строятся, если нет настроенного WAN: их не может использовать ни один full route. Порядок WAN/JH/Exit, сортировка/ties, raw source identity, all routes и unknown community indices сохранены.13pure tests прошли, включая1230 assignment oracle fixtures и новые проверки8route probe ownership/ties/zeroWAN.12browser (large homes/routes/observed/tables) passed; non-incremental typecheck до dev, targeted lint и production build успешны.
CPU final:1000routes13.3ms;100000(10×100×100)49.7ms;100000(100×10×100)44.0ms. До lookup ~284–329ms и267–287ms. Дополнительный zeroWAN baseline0×100×1000 строил100000unused JH/Exit legs:45.6ms/heapDelta24.6MB; после skip11.5ms/0.17MB. Новый profiler допускаетWAN0 и ограничивает также JH×Exit≤100000. HeapDelta не peak/retained memory; полный nonzero product по-прежнему хранится и сортируется в памяти. Evidence docs/ux-ui/acceptance/optimizer-routes-final-2026-10-07.json содержит source SHA256 и точные результаты.
После чтения реального producer обнаружены следующие вопросы достоверности:source btest выполняется на srcClient,TX/RX экспортируются без переименования,optimizer пока читаетTX какDL. srcInterface передаётся только after-ping, не bandwidthTest; поле не доказывает WAN скоростного теста. Producer parser/fallback отсутствующую скорость превращает в0, а explicit avg0 может заменяться mean series. Следующий12.3.70 фиксирует аудит с первичными RouterOS источниками и планирует точечные исправления данных/подписей. Команды/права/правила сетевых операций не менялись; real PG/RouterOS/integrations12.5 не выполнялись.
+1 -1
View File
@@ -1,6 +1,6 @@
# План реализации UX/UI по пятичасовым окнам
Актуальное продолжение 2026-10-07: 12.3.68 baseline full routes сохранён: pure product1000/100000 под timeout10s/memory256MB, все route IDs/unknown сохранены. Следующий12.3.69: заменить повторный wanJhLegs.find на map с first-match семантикой и доказать route/source/tie эквивалентность. Затем аудит направлений TX/RX и привязки wildcard-проб; внешняя12.5 остаётся.
Актуальное продолжение 2026-10-07: 12.3.69 завершён: first-match WAN/JH map и skip JH/Exit без WAN,13pure+12browser/type/lint/build.100000routes≈44–50ms; zeroWAN heapDelta24.6MB→0.17MB. Следующий12.3.70: зафиксировать аудит TX/RX, WAN-binding и missing→0 producer по RouterOS docs/code; затем адресные исправления, без изменения команд/правил сетевых операций. Внешняя12.5 отдельно.
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-04. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
+1 -1
View File
@@ -1,6 +1,6 @@
# UX/UI: журнал реализации
Актуальное продолжение 2026-10-07: 12.3.68 baseline full routes сохранён: pure product1000/100000 под timeout10s/memory256MB, все route IDs/unknown сохранены. Следующий12.3.69: заменить повторный wanJhLegs.find на map с first-match семантикой и доказать route/source/tie эквивалентность. Затем аудит направлений TX/RX и привязки wildcard-проб; внешняя12.5 остаётся.
Актуальное продолжение 2026-10-07: 12.3.69 завершён: first-match WAN/JH map и skip JH/Exit без WAN,13pure+12browser/type/lint/build.100000routes≈44–50ms; zeroWAN heapDelta24.6MB→0.17MB. Следующий12.3.70: зафиксировать аудит TX/RX, WAN-binding и missing→0 producer по RouterOS docs/code; затем адресные исправления, без изменения команд/правил сетевых операций. Внешняя12.5 отдельно.
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения.
+9
View File
@@ -774,3 +774,12 @@ Pure child CPU1000: equal1217.5→15.6ms,varied7967.5→14.5ms,freshness10с tim
2026-10-07. Pure child process без API/DB/браузера/сети, product≤100000/dimensions1–1000/timeout10s/V8heap256MB.1000WAN×1JH×1Exit1000routes≈16.9ms/heapDelta1.8MB;10×100×100100000routes≈329.6ms/22.0MB;100×10×100≈266.7ms/27.1MB. Первый измеренный прогон; точные повторные значения и source SHA256 в docs/ux-ui/acceptance/optimizer-routes-baseline-2026-10-07.json. Heap delta не peak/retained memory, время не production p95/SLA. Проверены count/first-last IDs/bestFirst/сохранение отсутствующих наблюдений. ESLint скрипта прошёл.
Baseline в UI не запускался. Повторный Array.find по1000wanJhLegs на каждую из100000комбинаций даёт лишние линейные сканирования. Следующий12.3.69: map lookup с сохранением первого matching leg и порядка routes/ties, профильный repeat. Полный product всё ещё хранится/сортируется в памяти; опасную произвольную размерность этот профиль не принимает. Далее аудит TX/RX и связи wildcard-пробы с конкретным WAN, прежде чем объявлять observed correctness закрытой. Внешняя12.5 отдельно.
## 12.3.69 — lookup leg и исключение ненужных расчётов без WAN
2026-10-07. WAN/JH map сохраняет первый matching leg, lookup вынесен из Exit loop. JH/Exit legs не строятся, если нет настроенного WAN: их не может использовать ни один full route. Порядок WAN/JH/Exit, сортировка/ties, raw source identity, all routes и unknown community indices сохранены.13pure tests прошли, включая1230 assignment oracle fixtures и новые проверки8route probe ownership/ties/zeroWAN.12browser (large homes/routes/observed/tables) passed; non-incremental typecheck до dev, targeted lint и production build успешны.
CPU final:1000routes13.3ms;100000(10×100×100)49.7ms;100000(100×10×100)44.0ms. До lookup ~284–329ms и267–287ms. Дополнительный zeroWAN baseline0×100×1000 строил100000unused JH/Exit legs:45.6ms/heapDelta24.6MB; после skip11.5ms/0.17MB. Новый profiler допускаетWAN0 и ограничивает также JH×Exit≤100000. HeapDelta не peak/retained memory; полный nonzero product по-прежнему хранится и сортируется в памяти. Evidence docs/ux-ui/acceptance/optimizer-routes-final-2026-10-07.json содержит source SHA256 и точные результаты.
После чтения реального producer обнаружены следующие вопросы достоверности:source btest выполняется на srcClient,TX/RX экспортируются без переименования,optimizer пока читаетTX какDL. srcInterface передаётся только after-ping, не bandwidthTest; поле не доказывает WAN скоростного теста. Producer parser/fallback отсутствующую скорость превращает в0, а explicit avg0 может заменяться mean series. Следующий12.3.70 фиксирует аудит с первичными RouterOS источниками и планирует точечные исправления данных/подписей. Команды/права/правила сетевых операций не менялись; real PG/RouterOS/integrations12.5 не выполнялись.
@@ -0,0 +1,92 @@
{
"baseHead": "057f0d352a58d7a4abe4dca18d64dd3d93a002d6",
"sourceSha256": "4f6f3b8b9c82efefdb82a2e02a241f3ca557cc1c5d689eaa079e20178f872947",
"runs": [
{
"wanCount": 1000,
"jhCount": 1,
"exitCount": 1,
"combinations": 1000,
"memoryCapMb": 256,
"timeoutMs": 10000,
"totalMs": 267,
"status": 0,
"timedOut": false,
"result": {
"elapsedMs": 13.307500000000005,
"heapDeltaBytes": 1925224,
"wanJhLegs": 1000,
"routes": 1000,
"firstId": "1-w0-2-3",
"lastId": "1-w999-2-3",
"bestMatchesFirst": true,
"missingObservations": true
},
"error": ""
},
{
"wanCount": 10,
"jhCount": 100,
"exitCount": 100,
"combinations": 100000,
"memoryCapMb": 256,
"timeoutMs": 10000,
"totalMs": 303,
"status": 0,
"timedOut": false,
"result": {
"elapsedMs": 49.748999999999995,
"heapDeltaBytes": 27920816,
"wanJhLegs": 1000,
"routes": 100000,
"firstId": "1-w0-2-102",
"lastId": "1-w9-101-201",
"bestMatchesFirst": true,
"missingObservations": true
},
"error": ""
},
{
"wanCount": 100,
"jhCount": 10,
"exitCount": 100,
"combinations": 100000,
"memoryCapMb": 256,
"timeoutMs": 10000,
"totalMs": 288,
"status": 0,
"timedOut": false,
"result": {
"elapsedMs": 43.97179999999997,
"heapDeltaBytes": 22567496,
"wanJhLegs": 1000,
"routes": 100000,
"firstId": "1-w0-2-12",
"lastId": "1-w99-11-111",
"bestMatchesFirst": true,
"missingObservations": true
},
"error": ""
},
{
"wanCount": 0,
"jhCount": 100,
"exitCount": 1000,
"combinations": 0,
"memoryCapMb": 256,
"timeoutMs": 10000,
"totalMs": 278,
"status": 0,
"timedOut": false,
"result": {
"elapsedMs": 11.54419999999999,
"heapDeltaBytes": 168744,
"wanJhLegs": 0,
"routes": 0,
"bestMatchesFirst": true,
"missingObservations": true
},
"error": ""
}
]
}
+13 -3
View File
@@ -512,8 +512,17 @@ export function buildLiveOptimizerData(
}
}
const wanJhById = new Map<string, Map<string, WanJhLeg>>()
for (const leg of wanJhLegs) {
let byHost = wanJhById.get(leg.wanId)
if (!byHost) { byHost = new Map(); wanJhById.set(leg.wanId, byHost) }
// Keep the first match, as the former Array.find did.
if (!byHost.has(leg.jhId)) byHost.set(leg.jhId, leg)
}
const jhExMap = new Map<string, JhExLeg>()
for (const jh of jumpHosts) {
// Without a configured WAN, no full route can consume a JH→Exit leg.
for (const jh of home.wans.length ? jumpHosts : []) {
for (const ex of exitNodes) {
const jhRow = byId.get(jh.id)
const exRow = byId.get(ex.id)
@@ -544,10 +553,11 @@ export function buildLiveOptimizerData(
const fullRoutes: FullRoute[] = []
for (const wan of home.wans) {
for (const jh of jumpHosts) {
const hw = wanJhById.get(wan.id)?.get(jh.id)
if (!hw) continue
for (const ex of exitNodes) {
const hw = wanJhLegs.find((l) => l.wanId === wan.id && l.jhId === jh.id)
const je = jhExMap.get(`${jh.id}::${ex.id}`)
if (!hw || !je) continue
if (!je) continue
const totalPing = hw.pingMs + je.pingMs
const dl = Math.min(hw.dlMbps, je.dlMbps)
const ul = Math.min(hw.ulMbps, je.ulMbps)
+33
View File
@@ -0,0 +1,33 @@
import {test} from "node:test"
import assert from "node:assert/strict"
import {buildLiveOptimizerData,DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS,type OptimizerApiServer,type RouteOptimizerSpeedProbe} from "./route-optimizer-data"
const common={host:"192.0.2.1",enabled:true,status:"online" as const,latency:10,country:"RU",site:"TEST",model:"CHR"}
const wans=Array.from({length:2},(_,i)=>({id:`w${i+1}`,name:`WAN${i+1}`,iface:`ether${i+1}`,isp:"ISP",ip:"198.51.100.1",maxDl:100,maxUl:100}))
const rows:OptimizerApiServer[]=[{...common,id:1,name:"Home",type:"home-router",wanUplinks:wans},...Array.from({length:2},(_,i)=>({...common,id:i+2,name:`Jump${i}`,type:"jump-host" as const})),...Array.from({length:2},(_,i)=>({...common,id:i+4,name:`Exit${i}`,type:"exit-node" as const}))]
const probe=(id:string,src:number,dst:number,iface:string):RouteOptimizerSpeedProbe=>({id,srcServerId:String(src),dstServerId:String(dst),srcInterface:iface,dstInterface:"",enabled:true,lastPingRttMs:10,lastPingLossPct:0,lastTxAvgMbps:100,lastRxAvgMbps:100,lastPingAt:"2026-10-07T00:00:00Z"})
test("full route keeps the matching WAN/JH and JH/Exit probe snapshots",()=>{
const probes=[...wans.flatMap(wan=>[2,3].map(jh=>probe(`wan-${wan.id}-${jh}`,1,jh,wan.iface))),...[2,3].flatMap(jh=>[4,5].map(exit=>probe(`exit-${jh}-${exit}`,jh,exit,"")))]
const entry=buildLiveOptimizerData(rows,[],DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS,probes).homes[0]
assert.equal(entry.fullRoutes.length,8);assert.equal(new Set(entry.fullRoutes.map(route=>route.id)).size,8)
for(const route of entry.fullRoutes){
assert.equal(route.hw.observed?.probeId,`wan-${route.wan.id}-${route.jh.id}`)
assert.equal(route.je.observed?.probeId,`exit-${route.jh.id}-${route.exit.id}`)
assert.equal(route.hw.wanId,route.wan.id);assert.equal(route.hw.jhId,route.jh.id)
assert.equal(route.je.jhId,route.jh.id);assert.equal(route.je.exitId,route.exit.id)
}
assert.equal(entry.bestRoute,entry.fullRoutes[0])
})
test("equal modeled routes keep original WAN/JH/Exit order and unknown observations",()=>{
const entry=buildLiveOptimizerData(rows,[],DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS).homes[0]
assert.deepEqual(entry.fullRoutes.map(route=>route.id),wans.flatMap(wan=>[2,3].flatMap(jh=>[4,5].map(exit=>`1-${wan.id}-${jh}-${exit}`))))
assert.ok(entry.fullRoutes.every(route=>route.hw.observed?.probeId===null&&route.je.observed?.probeId===null))
})
test("no WAN leaves routes and community indices unknown without generating JH/Exit legs",()=>{
const inventory=rows.map(row=>row.id===1?{...row,wanUplinks:[]}:row)
const entry=buildLiveOptimizerData(inventory,[{serverId:"1",rules:[{id:"a",community:"65001:1",gateway:"192.0.2.1",gatewayTunnelId:"",description:""}]}],DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS).homes[0]
assert.deepEqual(entry.wanJhLegs,[]);assert.deepEqual(entry.fullRoutes,[]);assert.equal(entry.bestRoute,null)
assert.equal(entry.commRecs.length,1);assert.equal(entry.commRecs[0].current,null);assert.equal(entry.commRecs[0].recommended,null)
})
@@ -1,9 +1,11 @@
// Bounded pure full-route calculation: no API, database, network or browser.
import {spawnSync} from 'node:child_process'
import assert from 'node:assert/strict'
const [wanCount,jhCount,exitCount]=process.argv.slice(2).length?process.argv.slice(2).map(Number):[10,100,100]
const args=process.argv.slice(2)
if(args.length&&args.length!==3)throw new Error('Provide exactly three dimensions')
const [wanCount,jhCount,exitCount]=args.length?args.map(Number):[10,100,100]
const combinations=wanCount*jhCount*exitCount
if([wanCount,jhCount,exitCount].some(n=>!Number.isInteger(n)||n<1||n>1000)||!Number.isFinite(combinations)||combinations>100000)throw new Error('Three dimensions 1..1000; product <=100000')
if([wanCount,jhCount,exitCount].some(n=>!Number.isInteger(n)||n<0||n>1000)||jhCount<1||exitCount<1||!Number.isFinite(combinations)||combinations>100000||jhCount*exitCount>100000)throw new Error('WAN0..1000, JH/Exit1..1000; products <=100000')
const code=`
const fs=require('node:fs'),ts=require('typescript');
const source=ts.transpileModule(fs.readFileSync('lib/route-optimizer-data.ts','utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText;
@@ -12,12 +14,12 @@ const {buildLiveOptimizerData,DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS}=mod.exports;
const common={host:'192.0.2.1',enabled:true,status:'online',latency:10,country:'RU',site:'TEST',model:'CHR'};
const rows=[{...common,id:1,name:'Home',type:'home-router',wanUplinks:Array.from({length:${wanCount}},(_,i)=>({id:'w'+i,name:'WAN'+(i+1),iface:'ether'+(i+1),isp:'ISP',ip:'198.51.100.1',maxDl:100,maxUl:100}))},...Array.from({length:${jhCount}},(_,i)=>({...common,id:i+2,name:'Jump'+i,type:'jump-host'})),...Array.from({length:${exitCount}},(_,i)=>({...common,id:i+2+${jhCount},name:'Exit'+i,type:'exit-node'}))];
const before=process.memoryUsage().heapUsed,start=performance.now();const entry=buildLiveOptimizerData(rows,[],DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS).homes[0];
console.log(JSON.stringify({elapsedMs:performance.now()-start,heapDeltaBytes:process.memoryUsage().heapUsed-before,wanJhLegs:entry.wanJhLegs.length,routes:entry.fullRoutes.length,firstId:entry.fullRoutes[0]?.id,lastId:entry.fullRoutes.at(-1)?.id,bestMatchesFirst:entry.bestRoute===entry.fullRoutes[0],missingObservations:entry.fullRoutes.every(route=>route.hw.observed.probeId===null&&route.je.observed.probeId===null)}));
console.log(JSON.stringify({elapsedMs:performance.now()-start,heapDeltaBytes:process.memoryUsage().heapUsed-before,wanJhLegs:entry.wanJhLegs.length,routes:entry.fullRoutes.length,firstId:entry.fullRoutes[0]?.id,lastId:entry.fullRoutes.at(-1)?.id,bestMatchesFirst:entry.bestRoute===(entry.fullRoutes[0]??null),missingObservations:entry.fullRoutes.every(route=>route.hw.observed.probeId===null&&route.je.observed.probeId===null)}));
`;
const start=performance.now(),r=spawnSync(process.execPath,['--max-old-space-size=256','-e',code],{cwd:process.cwd(),timeout:10000,encoding:'utf8',windowsHide:true,maxBuffer:16384});
const result=r.status===0?JSON.parse(r.stdout):null;
const profile={wanCount,jhCount,exitCount,combinations,memoryCapMb:256,timeoutMs:10000,totalMs:Math.round(performance.now()-start),status:r.status,timedOut:r.error?.code==='ETIMEDOUT',result,error:r.stderr.slice(0,2000)};
console.log(JSON.stringify(profile,null,2));
if(result){assert.equal(result.routes,combinations);assert.equal(result.wanJhLegs,wanCount*jhCount);assert.equal(result.firstId,`1-w0-2-${jhCount+2}`);assert.equal(result.lastId,`1-w${wanCount-1}-${jhCount+1}-${jhCount+exitCount+1}`);assert.equal(result.bestMatchesFirst,true);assert.equal(result.missingObservations,true)}
if(result){assert.equal(result.routes,combinations);assert.equal(result.wanJhLegs,wanCount*jhCount);assert.equal(result.firstId,combinations?`1-w0-2-${jhCount+2}`:undefined);assert.equal(result.lastId,combinations?`1-w${wanCount-1}-${jhCount+1}-${jhCount+exitCount+1}`:undefined);assert.equal(result.bestMatchesFirst,true);assert.equal(result.missingObservations,true)}
if(r.error&&!profile.timedOut)throw r.error;
if(r.status!==0&&!profile.timedOut)process.exitCode=1;