From 4fee9e371f5e779bcfef2fe83c084b16b746c3fa Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 7 Oct 2026 13:16:12 +0700 Subject: [PATCH] =?UTF-8?q?fix(optimizer):=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BB=D0=B8=D1=88=D0=BD=D0=B8=D0=B5=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B8=D1=81=D0=BA=D0=B8=20=D0=B8=20=D1=80=D0=B0=D1=81?= =?UTF-8?q?=D1=87=D1=91=D1=82=D1=8B=20=D0=BC=D0=B0=D1=80=D1=88=D1=80=D1=83?= =?UTF-8?q?=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ux-ui-performance.md | 9 ++ docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 2 +- docs/ux-ui-stage-12.md | 9 ++ .../optimizer-routes-final-2026-10-07.json | 92 +++++++++++++++++++ lib/route-optimizer-data.ts | 16 +++- lib/route-optimizer-routes.test.ts | 33 +++++++ .../performance/optimizer-routes-profile.mjs | 10 +- 8 files changed, 164 insertions(+), 9 deletions(-) create mode 100644 docs/ux-ui/acceptance/optimizer-routes-final-2026-10-07.json create mode 100644 lib/route-optimizer-routes.test.ts diff --git a/docs/ux-ui-performance.md b/docs/ux-ui-performance.md index 77643b1f..016c075b 100644 --- a/docs/ux-ui-performance.md +++ b/docs/ux-ui-performance.md @@ -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 не выполнялись. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index d25f652f..77c7352c 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -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). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index b7d1c8c4..ec021e59 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -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). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index a93e2bd8..7a39dcf5 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -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 не выполнялись. diff --git a/docs/ux-ui/acceptance/optimizer-routes-final-2026-10-07.json b/docs/ux-ui/acceptance/optimizer-routes-final-2026-10-07.json new file mode 100644 index 00000000..76aff6ec --- /dev/null +++ b/docs/ux-ui/acceptance/optimizer-routes-final-2026-10-07.json @@ -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": "" + } + ] +} diff --git a/lib/route-optimizer-data.ts b/lib/route-optimizer-data.ts index 8ee3df4c..b4e38957 100644 --- a/lib/route-optimizer-data.ts +++ b/lib/route-optimizer-data.ts @@ -512,8 +512,17 @@ export function buildLiveOptimizerData( } } + const wanJhById = new Map>() + 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() - 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) diff --git a/lib/route-optimizer-routes.test.ts b/lib/route-optimizer-routes.test.ts new file mode 100644 index 00000000..9dd2869a --- /dev/null +++ b/lib/route-optimizer-routes.test.ts @@ -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) +}) diff --git a/scripts/performance/optimizer-routes-profile.mjs b/scripts/performance/optimizer-routes-profile.mjs index 29580e1d..7454bd1d 100644 --- a/scripts/performance/optimizer-routes-profile.mjs +++ b/scripts/performance/optimizer-routes-profile.mjs @@ -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;