Сучасні C2-дашборди вже не рендерять сотні треків. Вони рендерять десятки тисяч. Повітряна обстановка з федеративних радарних мереж, морські фіди AIS, телеметрія роїв БПЛА, GPS-пінги наземних підрозділів та об'єднані багатосенсорні треки — усе це надходить на той самий екран оператора. Робочий єдиний оперативний образ у 2026 році регулярно сягає піків понад 100 000 одночасних треків. Шар візуалізації або тримає 60 fps під цим навантаженням, або оператор втрачає довіру до системи.
Ця стаття — інженерний огляд того, як зберегти чесний бюджет кадру на такому масштабі. Ми розглядаємо інстансинг WebGL, батчинг примітивів Cesium, композицію шарів deck.gl, стратегію LOD, бюджети GPU-пам'яті, арифметику темпу кадрів і тестову обв'язку, потрібну для виявлення регресій раніше, ніж їх помітить оператор.
Реальність 100 000 треків
Старе емпіричне правило — "тактичний дисплей повинен витримувати кілька тисяч треків" — було правильним у 2010 році й небезпечно хибним у 2026-му. Багатосенсорні фьюжн-фіди NATO, низьковисотні радарні мережі та комерційні агрегатори ADS-B/AIS регулярно видають шестизначні кількості треків під час навчань та активних операцій. Лише доктрина рою БПЛА може помістити 200–500 дружніх випромінювачів у бокс 50 км.
Розрив у продуктивності важливий, бо довіра оператора руйнується нижче 30 fps. Щойно панорамування починає сіпатися або символіка відстає від оновлень треків, оператор перестає довіряти всьому іншому на екрані — і починає звіряти картину з паперовою картою або другим монітором. Цей час перехресної перевірки — це саме та оперативна затримка, яку C2-система покликана усунути. C2-дашборд, що падає нижче 60 fps під реалістичним навантаженням, — це не повільний C2-дашборд. Це зламаний.
Ціль фіксована: стабільні 60 fps зі 100 000+ треків на типовій оперативній робочій станції середнього класу (8-ядерний CPU, dGPU середнього рівня, 1440p). Для досягнення цієї цілі потрібно, щоб кожен шар стека — геометрія, draw call'и, GPU-пам'ять, мережевий прийом, мутація стану — поважав бюджет кадру 16.67 мс.
Інстансинг WebGL
Перший важіль — це draw call. Видавати 100 000 окремих відмальовок на кадр на 60 fps на будь-якому сучасному GPU неможливо; лише накладні витрати драйвера на боці CPU з'їдають увесь бюджет. Інстансований рендеринг згортає тисячі символів в один draw call. Одна геометрія (меш символу), один шейдер і per-instance буфер атрибутів, що несе позицію, курс, приналежність та ID символу.
Стандартний патерн використовує ANGLE_instanced_arrays у WebGL 1 або нативний drawArraysInstanced у WebGL 2. Per-instance атрибути стрімлять із щільно упакованого буфера: типово 32 байти на трек (vec3 позиція, vec2 швидкість, uint32 запаковані прапорці). На 100k треків це 3.2 МБ даних атрибутів вершин — достатньо мало, щоб перезавантажувати щокадру за потреби, хоча часткові оновлення через bufferSubData дешевші.
Three.js надає інстансинг через InstancedMesh; deck.gl обробляє його нативно майже для кожного шару; API Primitive у Cesium підтримує його через масиви GeometryInstance. Три фреймворки потрапляють у той самий спектр — Three.js дає найбільше свободи й найменше готової геопросторової математики, deck.gl — найшвидший шлях до робочого щільного шару, Cesium надає семантику 3D-глобуса й оклюзію рельєфу, яких немає в інших двох.
Батчинг примітивів Cesium
API Entity у Cesium — неправильний інструмент за межами 5 000 треків. Сутності виділяють per-track JavaScript-об'єкти, виконують update-цикл на боці CPU й перебудовують геометрію при змінах властивостей. Вартість прийнятна на малих кількостях і катастрофічна на великих.
Для рендерингу масштабу 100k опускайтеся до API Primitive. Один PointPrimitiveCollection рендерить до ~1M точок одним draw call. Один BillboardCollection обробляє десятки тисяч іконкових спрайтів зі спільним текстурним атласом. Патерн GeometryInstance групує тисячі статичних геометрій (наприклад, кільця дальності, геозони) в один батч-примітив на момент створення.
Рендеринг міток асиметричний. PointPrimitiveCollection зі 100k рендериться невимушено; додавання 100k міток зверху миттєво ламає бюджет кадру. Мітки проходять через SDF-текст у Cesium, що коштує і пам'яті атласу гліфів, і окремої батч-відмальовки. Виправлення — LOD-залежна видимість міток: рендерте мітки лише для треків у залежному від зуму екранно-просторовому радіусі від курсора або для треків, помічених фьюжн-движком як "of interest". Типовому екрану оператора потрібно не більше 50–200 видимих міток одночасно.
Шари deck.gl для оборони
deck.gl сидить поверх базової карти MapboxGL або MapLibre і дає композовний стек шарів, спроєктований саме для цієї задачі. Релевантні шари для C2-дисплея:
ScatterplotLayer. Робочий кінь для сирих позицій треків. Рендерить мільйони точок на 60 fps, бо кожен атрибут (позиція, колір, радіус) GPU-bound. Використовуйте його для несимволізованих точок треків, кілець покриття сенсорів та щільних плоских шарів.
IconLayer. Рендерить символи MIL-STD-2525 із текстурного атласу. Продуктивність масштабується з розміром атласу; запакуйте символіку 2525 в один атлас 4096×4096 з усіма варіантами приналежності/ешелону, попередньо растеризованими. На 100k іконок із одним спільним атласом IconLayer комфортно тримає 60 fps на залізі середнього класу.
PathLayer. Для історій треків та проєкційних курсів. Вартість масштабується з кількістю вершин, а не з кількістю шляхів — віддавайте перевагу проріджуванню довгих історій (Douglas-Peucker з епсилоном, прив'язаним до рівня зуму), а не відкиданню шляхів.
Шари GPU-агрегації. ScreenGridLayer, HexagonLayer та HeatmapLayer агрегують мільйони точок у бін-сумовані візуалізації на GPU. Корисні як оверлеї щільності при віддалені — на низькому зумі ви не хочете бачити 100k символів, ви хочете бачити градієнт щільності загроз.
Стратегія Level-of-Detail (LOD)
Найефективніша оптимізація продуктивності — не рендерити те, чого оператор не може суттєво розрізнити. На рівні зуму шириною 2000 км дисплей зі 100k треків не може розрізнити окремі символи — кожен піксель екрана покриває кілька треків. Рендерити повну символіку MIL-STD-2525 на такому зумі — марна витрата часу GPU й нечитабельна картина.
Драбина LOD: на далекому зумі — агрегована щільність (GPU-бінована теплокарта або гекс-шар); на середньому — несимволізовані точки, забарвлені за приналежністю; на близькому — повна символіка 2525 з мітками для помічених треків; на максимальному — повна символіка з мітками, історичними слідами та проєкційними курсами для кожного видимого треку.
Кластеризація в екранному просторі — доповнення. Навіть на близькому зумі щільні кластери (парковка з машинами, порт із кораблями) дають перекриті символи, що ховають один одного. Прохід кластеризації через k-d дерево або grid-bin (виконується на воркер-потоці, не на головному) згортає перекриті символи в єдиний бейдж "N треків тут", поки оператор не наблизиться.
Профіль використання виправдовує агресивний LOD: оператор збільшує зум раз на 30–60 секунд і більшість часу сканує широку картину. Оптимізація бюджету кадру для широкої картини окуповується постійно; оптимізація для близького зуму — лише в моменти активного інтересу.
Бюджети GPU-пам'яті
Тиск на пам'ять — тихий вбивця щільних дисплеїв. Видима кількість треків — лише частина бюджету. Текстурні атласи (лист символів, тайли рельєфу, растрові тайли базової карти), вершинні буфери (per-instance атрибути, шляхи історії), uniform-буфери, прикріплення framebuffer та власний композитор браузера — усе тягне з того самого пулу.
Реальні бюджети, на які ми орієнтуємося: інтегрований 4 ГБ GPU (Intel Iris Xe, AMD Radeon 780M) на розгорнутому ноутбуці має приблизно 2–2.5 ГБ корисних для WebGL-контексту після того, як ОС, браузер та інші вкладки заберуть свою частку. 16 ГБ дискретний GPU (RTX 4070 / 5070 класу) має 12+ ГБ корисних. Багато консервативних розгортань — оперативні центри з тверднутими робочими станціями, придбаними роки тому, — досі працюють на iGPU-класі заліза. Проєктувати під iGPU-конверт — безпечніший дефолт.
Практичні числа для 100k-трекового C2-дашборду: per-instance буфери атрибутів ~5–10 МБ; атлас символів ~64 МБ (4096² RGBA); растровий кеш базової карти ~200–400 МБ; кеш тайлів рельєфу ~300–600 МБ; геометрія шляхів історії ~20–50 МБ залежно від ретенції. Разом ~600 МБ–1.2 ГБ. Це вкладається в iGPU-конверт із запасом, але лише якщо кожен шар дисципліновано підходить до розмірів текстур і росту буферів.
Затримка та темп кадрів
Бюджет 16.67 мс на кадр розбивається приблизно так: 2–3 мс на обробку вводу та мутацію стану, 4–6 мс на оновлення шару (на боці CPU, переважно перерахунок атрибутних буферів і culling), 6–8 мс на GPU-рендеринг, 1–2 мс на накладні витрати композитора. Усе, що споживає більше своєї частки, краде в наступної й видає скинутий кадр.
Найгірші стрибки ховаються там, де їх легко проґавити. Join'и кореляції треків на головному потоці — виконання проходу кореляції фьюжн-движка інлайн із рендерингом — дають 50–200 мс зупинки щоразу, коли надходить новий батч від сенсора. Виправлення — виконувати кореляцію на воркер-потоці й публікувати незмінні дельти треків у головний потік. Сплески оновлень із сервера (websocket, що скидає 5 000 оновлень треків за один тік) насичують JS event loop; обмежуйте швидкість і батчте дельти до одного буферизованого оновлення на кадр.
Збір сміття — третє джерело стрибків. Виділення нових об'єктів на трек на кадр дає пилкоподібні GC-паузи раз на кілька секунд. Використовуйте пули типізованих масивів і перевикористовуйте буфери; уникайте per-frame створення об'єктних літералів у гарячих шляхах. Очікування 60-fps-or-bust реальне, а одна 100 мс GC-пауза раз на 10 секунд — це саме той jank, що руйнує довіру оператора.
Тестування на масштабі
Не можна випустити 100k-трековий дисплей без тестової обв'язки, що генерує та відтворює навантаження на цьому масштабі. Три компоненти: синтетичний генератор треків, автоматизований пакет регресій FPS і записаний-відтворюваний еталонний джерело правди.
Синтетичний генератор продукує детерміновані сценарії на 100k треків — випадкові розподіли, щільні кластери, формації роїв БПЛА, масові рейди — кожен із сидом, щоб CI-запуск відтворював ту саму сцену щоразу. Кожен сценарій проводить headless-браузер скриптованим шляхом камери (панорама, наближення, віддалення, declutter, фільтр), а обв'язка зразкує гістограми дельт performance.now() і повідомляє часи кадрів p50/p95/p99.
Пакет регресій FPS виконується на кожному PR. Пороги явні: p95 час кадру нижче 18 мс, p99 нижче 25 мс, без скинутих кадрів понад 50 мс упродовж 60-секундного скриптованого прогону. Будь-який коміт, що штовхає числа за поріг, блокує merge. Це єдиний спосіб ловити регресії "смерті від тисячі порізів", де кожна окрема зміна коштує 0.2 мс.
Реальне записане відтворення сенсорних даних — джерело правди. Синтетичні навантаження ловлять обвали продуктивності, але пропускають асиметричні розподіли реальних даних — кластери, прогалини, патерни сплесків. Запис у стилі pcap живого фіду навчань, відтворений у швидкості "wall-clock" проти дашборду, — це найближче, що ви отримаєте до оперативного навантаження без оператора на стільці. Поєднайте синтетичну обв'язку з двома-трьома записаними сценами з верифікаційних навчань — і у вас є регресійна сітка, що тримає.