Nowoczesne dashboardy C2 nie renderują już setek śladów. Renderują dziesiątki tysięcy. Obraz powietrzny z federacyjnych sieci radarowych, morskie kanały AIS, telemetria rojów dronów, pingi GPS jednostek naziemnych oraz scalone ślady z wielu czujników — wszystko trafia na ten sam ekran operatora. Działający wspólny obraz operacyjny w 2026 roku regularnie przekracza 100 000 jednoczesnych śladów. Warstwa wizualizacji albo utrzymuje 60 fps pod tym obciążeniem, albo operator traci zaufanie do systemu.

Ten artykuł to inżynierski przewodnik, jak zachować uczciwe budżety klatek przy tej skali. Omawiamy instancing WebGL, batching prymitywów Cesium, kompozycję warstw deck.gl, strategię LOD, budżety pamięci GPU, arytmetykę tempa klatek i framework testowy potrzebny do wychwytywania regresji przed operatorem.

Rzeczywistość 100 000 śladów

Stara zasada — „taktyczny wyświetlacz musi obsłużyć kilka tysięcy śladów" — była poprawna w 2010 roku i niebezpiecznie błędna w 2026. Wielosensorowe kanały fuzji NATO, niskopułapowe siatki radarowe oraz komercyjne agregatory ADS-B/AIS regularnie produkują sześciocyfrowe liczby śladów podczas ćwiczeń i aktywnych operacji. Sama doktryna roju dronów może wstawić 200–500 przyjaznych emiterów w pudełko 50 km.

Luka wydajnościowa ma znaczenie, ponieważ zaufanie operatora załamuje się poniżej 30 fps. Gdy panoramowanie się zacina lub symbolika opóźnia się względem aktualizacji śladów, operator przestaje ufać wszystkiemu innemu na wyświetlaczu — i zaczyna sprawdzać obraz krzyżowo na papierowej mapie lub na drugim ekranie. Ten czas sprawdzania krzyżowego to dokładnie to opóźnienie operacyjne, do którego eliminacji system C2 został zbudowany. Dashboard C2, który spada poniżej 60 fps pod realistycznym obciążeniem, nie jest powolnym dashboardem C2. Jest zepsuty.

Cel jest stały: stałe 60 fps, z ponad 100 000 śladów, na typowej stacji roboczej operacyjnej średniej klasy (8-rdzeniowy CPU, średniej klasy dGPU, 1440p). Osiągnięcie tego celu wymaga, aby każda warstwa stosu — geometria, draw calls, pamięć GPU, ingest sieciowy, mutacja stanu — respektowała budżet klatki 16,67 ms.

Instancing WebGL

Pierwszą dźwignią jest draw call. Wysyłanie 100 000 indywidualnych draw calls na klatkę jest niemożliwe przy 60 fps na żadnym obecnym GPU; sam narzut sterownika po stronie CPU spala cały budżet. Renderowanie instancjonowane łączy tysiące symboli w pojedynczy draw call. Jedna geometria (mesh symbolu), jeden shader i bufor atrybutów per instancja niosący pozycję, kurs, przynależność i ID symbolu.

Standardowy wzorzec używa ANGLE_instanced_arrays w WebGL 1 lub natywnego drawArraysInstanced w WebGL 2. Atrybuty per instancja przepływają z ciasno upakowanego bufora: zazwyczaj 32 bajty na ślad (vec3 pozycja, vec2 prędkość, uint32 spakowane flagi). Przy 100 tys. śladów to 3,2 MB danych atrybutów wierzchołków — wystarczająco mało, aby przesłać ponownie co klatkę, choć częściowe aktualizacje przez bufferSubData są tańsze.

Three.js udostępnia instancing przez InstancedMesh; deck.gl obsługuje go natywnie dla niemal każdej warstwy; API Primitive Cesium wspiera go przez tablice GeometryInstance. Trzy frameworki lądują na tym samym spektrum — Three.js daje najwięcej swobody i najmniej wbudowanej matematyki geoprzestrzennej, deck.gl to najszybsza droga do działającej warstwy o wysokiej gęstości, Cesium zapewnia semantykę globu 3D i okluzję terenu, których pozostałe dwa nie mają.

Batching prymitywów Cesium

API Entity w Cesium to złe narzędzie powyżej 5 000 śladów. Entities alokują obiekty JavaScript per ślad, uruchamiają pętlę aktualizacji po stronie CPU i przebudowują geometrię przy zmianach właściwości. Koszt jest amortyzowany dobrze przy małych liczbach i katastrofalny przy dużych.

Dla renderowania w skali 100 tys. zejdź do API Primitive. PointPrimitiveCollection renderuje do ~1 mln punktów w pojedynczym draw call. BillboardCollection obsługuje dziesiątki tysięcy sprite'ów teksturowanych ikonami ze wspólnym atlasem tekstur. Wzorzec GeometryInstance grupuje tysiące statycznych geometrii (np. pierścienie zasięgu, geofences) w jeden zbatchowany prymityw w momencie utworzenia.

Renderowanie etykiet jest asymetryczne. PointPrimitiveCollection na 100 tys. renderuje bez wysiłku; dodanie 100 tys. etykiet na wierzchu natychmiast łamie budżet klatki. Etykiety przechodzą przez ścieżkę tekstu SDF Cesium, która kosztuje zarówno pamięć atlasu glifów, jak i osobny zbatchowany draw. Rozwiązaniem jest widoczność etykiet sterowana LOD: renderuj etykiety tylko dla śladów w zależnym od zoomu promieniu screen-space od kursora lub dla śladów oznaczonych jako „interesujące" przez silnik fuzji. Typowy ekran operatora potrzebuje nie więcej niż 50–200 widocznych etykiet naraz.

Warstwy deck.gl dla obronności

deck.gl siedzi na mapie bazowej MapboxGL lub MapLibre i daje komponowalny stos warstw zaprojektowany dokładnie do tego problemu. Istotne warstwy dla wyświetlacza C2:

ScatterplotLayer. Koń roboczy dla surowych pozycji śladów. Renderuje miliony punktów przy 60 fps, ponieważ każdy atrybut (pozycja, kolor, promień) jest GPU-bound. Używaj jej dla niesymbolizowanych kropek śladów, pierścieni pokrycia czujników i płaskich warstw o wysokiej gęstości.

IconLayer. Renderuje symbole MIL-STD-2525 z atlasu tekstur. Wydajność skaluje się z rozmiarem atlasu; spakuj symbolikę 2525 w jeden atlas 4096×4096 z prerasteryzowanymi wszystkimi wariantami przynależności/szczebla. Przy 100 tys. ikon z jednym wspólnym atlasem IconLayer utrzymuje 60 fps komfortowo na sprzęcie średniej klasy.

PathLayer. Dla historii śladów i prognozowanych kursów. Koszt skaluje się z liczbą wierzchołków, nie z liczbą ścieżek — preferuj decymację długich historii (Douglas-Peucker, z epsilonem powiązanym z poziomem zoomu) zamiast porzucania ścieżek.

Warstwy agregacji GPU. ScreenGridLayer, HexagonLayer i HeatmapLayer agregują miliony punktów w wizualizacje sumowane w binach na GPU. Przydatne jako nakładki gęstości przy oddaleniu — przy niskim zoomie nie chcesz widzieć 100 tys. symboli, chcesz widzieć gradient gęstości zagrożenia.

Strategia poziomu szczegółowości (LOD)

Najefektywniejszą optymalizacją wydajności jest nierenderowanie tego, czego operator nie może sensownie zobaczyć. Przy poziomie zoomu 2000 km szerokości wyświetlacz 100 tys. śladów nie może rozróżnić indywidualnych symboli — każdy piksel ekranu obejmuje wiele śladów. Renderowanie pełnej symboliki MIL-STD-2525 przy tym zoomie to zmarnowany czas GPU i produkuje nieczytelny obraz.

Drabina LOD: przy dalekim zoomie renderuj zagregowaną gęstość (heatmap binowany na GPU lub warstwa heksagonalna); przy średnim zoomie renderuj niesymbolizowane kropki kolorowane według przynależności; przy bliskim zoomie renderuj pełną symbolikę 2525 z etykietami dla oznaczonych śladów; przy maksymalnym zoomie renderuj pełną symbolikę z etykietami, śladami historii i prognozowanymi kursami dla każdego widocznego śladu.

Klastrowanie screen-space jest uzupełnieniem. Nawet przy bliskim zoomie gęste klastry (parking pełen pojazdów, port pełen statków) produkują nakładające się symbole, które wzajemnie się ukrywają. Przebieg klastrowania k-d tree lub grid-bin (uruchamiany w wątku worker, nie głównym) zwija nakładające się symbole w jedną odznakę „N śladów tutaj", dopóki operator nie powiększy.

Profil użycia uzasadnia agresywny LOD: operator powiększa raz na 30–60 sekund i większość czasu spędza skanując szeroki obraz. Optymalizacja budżetu klatki szerokiego obrazu zwraca się stale; optymalizacja budżetu klatki bliskiego zoomu zwraca się tylko w momentach aktywnego zainteresowania.

Budżety pamięci GPU

Presja pamięci to cichy zabójca wyświetlaczy o wysokiej gęstości. Liczba widocznych śladów to tylko część budżetu. Atlasy tekstur (arkusz symboli, kafle terenu, kafle rastrowe mapy bazowej), bufory wierzchołków (atrybuty per instancja, ścieżki historii), bufory uniform, załączniki framebuffer i sam kompozytor przeglądarki — wszystko czerpie z tej samej puli.

Realne budżety, pod które planujemy: zintegrowany GPU 4 GB (Intel Iris Xe, AMD Radeon 780M) na wdrożonym laptopie ma około 2–2,5 GB użytecznej pamięci dla kontekstu WebGL po tym, jak OS, przeglądarka i inne karty zabiorą swój udział. Dyskretne GPU 16 GB (klasa RTX 4070/5070) ma 12+ GB użytecznej. Wiele konserwatywnych headless wdrożeń — centra operacyjne ze utwardzonymi stacjami roboczymi zakupionymi lata temu — wciąż działa na sprzęcie klasy iGPU. Projektowanie pod kopertę iGPU jest bezpieczniejszym domyślnym wyborem.

Praktyczne liczby dla dashboardu C2 na 100 tys. śladów: bufory atrybutów per instancja ~5–10 MB; atlas symboli ~64 MB (4096² RGBA); cache rastrowy mapy bazowej ~200–400 MB; cache kafli terenu ~300–600 MB; geometria ścieżek historii ~20–50 MB w zależności od retencji. Razem ~600 MB–1,2 GB. Mieści się to w kopercie iGPU z marginesem, ale tylko jeśli każda warstwa jest zdyscyplinowana co do rozmiarów tekstur i wzrostu buforów.

Opóźnienie i tempo klatek

Budżet klatki 16,67 ms rozkłada się z grubsza: 2–3 ms na obsługę wejścia i mutację stanu, 4–6 ms na aktualizację warstw (po stronie CPU, głównie ponowne obliczanie buforów atrybutów i culling), 6–8 ms na renderowanie GPU, 1–2 ms na narzut kompozytora. Cokolwiek konsumuje więcej niż swoją porcję, kradnie z następnej porcji i produkuje porzuconą klatkę.

Najgorsze szczyty ukrywają się w miejscach łatwych do przeoczenia. Join'y korelacji śladów na głównym wątku — uruchamianie przebiegu korelacji silnika fuzji inline z renderowaniem — produkują 50–200 ms zatrzymań za każdym razem, gdy przybywa nowa partia z czujnika. Rozwiązaniem jest uruchamianie korelacji na wątku worker i wysyłanie niezmiennych delt śladów do głównego wątku. Wybuchy aktualizacji push z serwera (websocket zrzucający 5 000 aktualizacji śladów w jednym ticku) nasycają pętlę zdarzeń JS; ogranicz tempo i grupuj delty do jednej buforowanej aktualizacji na klatkę.

Garbage collection to trzecie źródło szczytów. Alokowanie nowych obiektów per ślad per klatka produkuje piłokształtne pauzy GC co kilka sekund. Używaj pul typowanych tablic i wykorzystuj bufory ponownie; unikaj tworzenia literałów obiektowych per klatka w gorących ścieżkach. Oczekiwanie 60-fps-albo-nic jest realne, a pojedyncza pauza GC 100 ms co 10 sekund to dokładnie ten rodzaj jank, który niszczy zaufanie operatora.

Testowanie na skalę

Nie można wysłać wyświetlacza 100 tys. śladów bez frameworku testowego, który generuje i odtwarza obciążenia w tej skali. Trzy komponenty: syntetyczny generator śladów, automatyczny pakiet regresji FPS i zarejestrowane źródło prawdy do odtwarzania.

Generator syntetyczny produkuje deterministyczne scenariusze 100 tys. śladów — losowe rozkłady, gęste klastry, formacje rojów dronów, scenariusze masowego nalotu — każdy zsiedlony tak, że uruchomienie CI odtwarza tę samą scenę za każdym razem. Każdy scenariusz prowadzi headless przeglądarkę przez skryptowaną ścieżkę kamery (pan, zoom in, zoom out, declutter, filtr), podczas gdy framework próbkuje histogramy delty performance.now() i raportuje czasy klatek p50/p95/p99.

Pakiet regresji FPS uruchamia się przy każdym PR. Progi są jawne: czas klatki p95 poniżej 18 ms, p99 poniżej 25 ms, brak porzuconych klatek przekraczających 50 ms w 60-sekundowym skryptowanym uruchomieniu. Każdy commit, który przesuwa liczby ponad próg, blokuje merge. To jedyny sposób na wychwytywanie regresji śmierci-przez-tysiąc-cięć, gdzie każda indywidualna zmiana kosztuje 0,2 ms.

Odtwarzanie zarejestrowanych rzeczywistych danych z czujników to źródło prawdy. Syntetyczne obciążenia łapią klify wydajnościowe, ale przegapiają asymetryczne rozkłady prawdziwych danych — klastry, luki, wzorce wybuchów. Nagranie w stylu pcap z kanału żywego ćwiczenia, odtwarzane z prędkością wall-clock przeciwko dashboardowi, jest najbliżej obciążenia operacyjnego bez operatora w fotelu. Sparuj framework syntetyczny z dwoma lub trzema zarejestrowanymi scenami z ćwiczeń weryfikacyjnych, a otrzymasz sieć regresji, która się trzyma.