From b3da4216e6cdd6b9cef48be47fce43991ef0314e Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 4 Oct 2026 21:52:05 +0700 Subject: [PATCH] =?UTF-8?q?chore(ux):=20=D0=B8=D0=B7=D0=BC=D0=B5=D1=80?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BC=D0=B0=D1=82=D1=80=D0=B8=D1=86=D1=83?= =?UTF-8?q?=20WAN=20=D0=B8=20=D1=80=D0=B0=D1=81=D1=87=D1=91=D1=82=20=D0=BD?= =?UTF-8?q?=D0=B0=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ux-ui-performance.md | 7 +++++ docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 2 +- docs/ux-ui-stage-12.md | 7 +++++ .../performance/optimizer-probes-profile.mjs | 19 ++++++++++++ tests/e2e/large-optimizer-wan-matrix.spec.ts | 30 +++++++++++++++++++ 6 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 scripts/performance/optimizer-probes-profile.mjs create mode 100644 tests/e2e/large-optimizer-wan-matrix.spec.ts diff --git a/docs/ux-ui-performance.md b/docs/ux-ui-performance.md index de69ea77..2ca3f6de 100644 --- a/docs/ux-ui-performance.md +++ b/docs/ux-ui-performance.md @@ -117,3 +117,10 @@ Statistics pivot в backend/src/services/statistics-aggregate.ts огранич buildLiveOptimizerData/assignProbesForWanJh делает DFS, когда WAN<=7 и WAN<=pool.length. Лимита pool нет. scoreProbeForWanIface допускает srcInterface="" для любого WAN; при7WAN/20 подходящих разных probes полное дерево содержит20P7=390700800 полных назначений (вывод из кода, не измеренный benchmark). Не запускать этот случай через Next/UI: сначала отдельный чистый CPU-профиль в дочернем процессе с жёстким timeout, без DB/RouterOS. Стратегию назначения/оптимальность нельзя менять без доказательства эквивалентности на малых наборах; простое обрезание pool может скрыть лучшее назначение. commRecs строятся через filter(sourceServerId).slice(0,24), а HomeCard подписывает BGP community только длиной среза. Нужны полный source count/явное сообщение24 изN; нельзя выдавать24 за весь каталог или менять правила применения. Локальная вкладка карточки сбрасывается при pagination, native OSPF select1000 options остаётся. Общая main/context после delta и реальная внешняя12.5 ещё не закрыты. Этот блок — карта приёмки, не доказательство нагрузочной готовности. Product build a16bfae уже passed; для документации повтор не нужен. Следующий12.3.59 — указанные bounded profiles/CPU timeout. Локальный коммит без push. + + +## 12.3.59 — отдельные профили матрицы и CPU + +2026-10-04. Live API подменён строгими DTO, без Exit и API mutations. Два браузерных профиля прошли: WAN1000×JH1 — 1000 строк,4 столбца,28019 DOM таблицы,28712 DOM страницы,4977мс dev-ready,108917байт; WAN10×JH100 — 10 строк,103 столбца,9803 DOM таблицы,10595 DOM страницы,2258мс dev-ready,16593байт. Время dev измеряет также загрузку/компиляцию и не является production benchmark. + +Чистый расчёт7WAN×20 wildcard probes воспроизводится scripts/performance/optimizer-probes-profile.mjs, в отдельном child process с жёстким timeout10000мс. Расчёт не завершился за timeout; тяжёлый DFS нельзя воспроизводить в UI. Нет DB/API/сетевых операций. Следующий12.3.60 — страницы50WAN и ограниченное окно JH, поиск по полному каталогу, проверка обновления/сжатия и320px. Семантику назначения probes этим блоком не меняем. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index a35d04e7..c63f7d09 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,6 +1,6 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-04: 12.3.58 сохранён — статический аудит WAN matrix/CPU назначения probes/среза24 commRecs. Следующий12.3.59: bounded profile1000 WAN×1JH и10WAN×100JH без Exit, отдельно ограниченный по времени CPU-профиль7WAN×20 wildcard probes. Не запускать тяжёлый DFS через UI. Последний UI-коммит a16bfae; общая регрессия после delta/внешняя12.5 остаются. +Актуальное продолжение 2026-10-04: 12.3.59 завершён: отдельные профили WAN1000×JH1, WAN10×JH100 и CPU7WAN×20 probes. Следующий12.3.60: ограничить отображение WAN/JH без среза исходных данных; тяжёлый DFS пока остаётся отдельным риском. Общая регрессия после delta и внешняя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 47bd4f9b..95dc60c2 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-04: 12.3.58 сохранён — статический аудит WAN matrix/CPU назначения probes/среза24 commRecs. Следующий12.3.59: bounded profile1000 WAN×1JH и10WAN×100JH без Exit, отдельно ограниченный по времени CPU-профиль7WAN×20 wildcard probes. Не запускать тяжёлый DFS через UI. Последний UI-коммит a16bfae; общая регрессия после delta/внешняя12.5 остаются. +Актуальное продолжение 2026-10-04: 12.3.59 завершён: отдельные профили WAN1000×JH1, WAN10×JH100 и CPU7WAN×20 probes. Следующий12.3.60: ограничить отображение WAN/JH без среза исходных данных; тяжёлый DFS пока остаётся отдельным риском. Общая регрессия после delta и внешняя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 5848c3c4..ccf578c0 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -669,3 +669,10 @@ ReUI client pagination50 только раскрытого списка, autoRes buildLiveOptimizerData/assignProbesForWanJh делает DFS, когда WAN<=7 и WAN<=pool.length. Лимита pool нет. scoreProbeForWanIface допускает srcInterface="" для любого WAN; при7WAN/20 подходящих разных probes полное дерево содержит20P7=390700800 полных назначений (вывод из кода, не измеренный benchmark). Не запускать этот случай через Next/UI: сначала отдельный чистый CPU-профиль в дочернем процессе с жёстким timeout, без DB/RouterOS. Стратегию назначения/оптимальность нельзя менять без доказательства эквивалентности на малых наборах; простое обрезание pool может скрыть лучшее назначение. commRecs строятся через filter(sourceServerId).slice(0,24), а HomeCard подписывает BGP community только длиной среза. Нужны полный source count/явное сообщение24 изN; нельзя выдавать24 за весь каталог или менять правила применения. Локальная вкладка карточки сбрасывается при pagination, native OSPF select1000 options остаётся. Общая main/context после delta и реальная внешняя12.5 ещё не закрыты. Этот блок — карта приёмки, не доказательство нагрузочной готовности. Product build a16bfae уже passed; для документации повтор не нужен. Следующий12.3.59 — указанные bounded profiles/CPU timeout. Локальный коммит без push. + + +## 12.3.59 — отдельные профили матрицы и CPU + +2026-10-04. Live API подменён строгими DTO, без Exit и API mutations. Два браузерных профиля прошли: WAN1000×JH1 — 1000 строк,4 столбца,28019 DOM таблицы,28712 DOM страницы,4977мс dev-ready,108917байт; WAN10×JH100 — 10 строк,103 столбца,9803 DOM таблицы,10595 DOM страницы,2258мс dev-ready,16593байт. Время dev измеряет также загрузку/компиляцию и не является production benchmark. + +Чистый расчёт7WAN×20 wildcard probes воспроизводится scripts/performance/optimizer-probes-profile.mjs, в отдельном child process с жёстким timeout10000мс. Расчёт не завершился за timeout; тяжёлый DFS нельзя воспроизводить в UI. Нет DB/API/сетевых операций. Следующий12.3.60 — страницы50WAN и ограниченное окно JH, поиск по полному каталогу, проверка обновления/сжатия и320px. Семантику назначения probes этим блоком не меняем. diff --git a/scripts/performance/optimizer-probes-profile.mjs b/scripts/performance/optimizer-probes-profile.mjs new file mode 100644 index 00000000..6d1e5ada --- /dev/null +++ b/scripts/performance/optimizer-probes-profile.mjs @@ -0,0 +1,19 @@ +// Pure calculation in a child process: no API, database, browser or network operations. +import { spawnSync } from 'node:child_process' +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; + const mod={exports:{}};new Function('exports','require','module',source)(mod.exports,require,mod); + 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:7},(_,i)=>({id:'wan'+i,name:'WAN'+i,isp:'ISP',iface:'ether'+i,ip:'198.51.100.1',maxDl:100,maxUl:100}))},{...common,id:2,name:'Jump',type:'jump-host'}]; + const probes=Array.from({length:20},(_,i)=>({id:'p'+i,srcServerId:'1',dstServerId:'2',srcInterface:'',dstInterface:'',enabled:true,lastPingRttMs:10,lastPingLossPct:0,lastTxAvgMbps:100,lastRxAvgMbps:100,lastPingAt:null})); + const start=performance.now();const result=buildLiveOptimizerData(rows,[],DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS,probes); + console.log(JSON.stringify({elapsedMs:performance.now()-start,legs:result.homes[0].wanJhLegs.length})); +`; +const start=performance.now(); +const result=spawnSync(process.execPath,['-e',code],{cwd:process.cwd(),timeout:10000,encoding:'utf8',windowsHide:true}); +const profile={wanCount:7,probeCount:20,timeoutMs:10000,elapsedMs:Math.round(performance.now()-start),status:result.status,timedOut:result.error?.code==='ETIMEDOUT',result:result.stdout.trim(),error:result.stderr.trim()}; +console.log(JSON.stringify(profile,null,2)); +if(result.error && !profile.timedOut) throw result.error; +if(result.status !== 0 && !profile.timedOut) process.exitCode=1; diff --git a/tests/e2e/large-optimizer-wan-matrix.spec.ts b/tests/e2e/large-optimizer-wan-matrix.spec.ts new file mode 100644 index 00000000..bf882228 --- /dev/null +++ b/tests/e2e/large-optimizer-wan-matrix.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from "@playwright/test" +import { writeFileSync } from "node:fs" +import { optimizerServersSchema } from "../../lib/route-optimizer-response" + +for (const [wanCount,jhCount] of [[1000,1],[10,100]]) { + test(`Матрица WAN${wanCount} × JH${jhCount} без Exit`, async ({page},info) => { + test.setTimeout(90000) + const common = { host: "192.0.2.1", enabled: true, status: "online", latency: 10, country: "RU", site: "TEST", model: "CHR" } + const servers = optimizerServersSchema.parse([ + { ...common, id: 1, name: "Домашний роутер", type: "home-router", wanUplinks: Array.from({ length: wanCount }, (_, i) => ({ id: `wan${i+1}`, name: `WAN${i+1}`, isp: "ISP", iface: `ether${i+1}`, ip: `198.51.100.${i+1}`, maxDl: 100, maxUl: 100 })) }, + ...Array.from({ length: jhCount }, (_, i) => ({ ...common, id: i+2, name: `Jump ${i+1}`, type: "jump-host" })), + ]) + + const writes: string[] = [] + await page.route("**/health", r => r.fulfill({ json: { ok: true } })) + await page.route("**/api/**", r => { + const path = new URL(r.request().url()).pathname + + if (r.request().method() !== "GET" && !path.endsWith("/preview")) writes.push(path) + return r.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? servers : path === "/api/filters/rules" ? { rulesets: [] } : path === "/api/uptime/speed-probes" ? { probes: [] } : path === "/api/ospf/catalog" ? { interfaces: [], instances: [], neighbors: [], bfdSessions: [], routes: [], failures: [] } : path.endsWith("/ospf/optimize/preview") ? { serverId: 1, serverName: "Домашний роутер", pingWeight: 60, interfacesTotal: 0, changedCount: 0, unchangedCount: 0, interfaces: [], changes: [] } : {} }) + }) + + const started=Date.now(); await page.goto("/route-optimizer?server=1&keep=1") + const table=page.getByRole("table").filter({hasText:"WAN-аплинк"}) + await expect(table.locator("tr[data-row-id]")).toHaveCount(wanCount) + const profile={wanCount,jhCount,bytes:Buffer.byteLength(JSON.stringify(servers)),rows:await table.locator("tr[data-row-id]").count(),columns:await table.getByRole("columnheader").count(),tableNodes:await table.locator("*").count(),pageNodes:await page.locator("*").count(),readyMs:Date.now()-started} + console.log("WAN matrix",JSON.stringify(profile)); writeFileSync(info.outputPath("matrix-profile.json"),JSON.stringify(profile,null,2)) + expect(writes).toEqual([]) + }) +}