Porządki: jak wreszcie dobrze korzystamy z pamięci podręcznej GitHuba 🧹
W ostatnim wpisie technicznym zmierzyliśmy, ile naprawdę kosztuje nasz potok budowania i sprawdzania, i odkryliśmy, że nie pieniądze, tylko czas oczekiwania i miejsce. Artykuł kończył się słowami „porządki są zaplanowane, ale jeszcze nie zrobione”. Poszło szybciej, niż myśleliśmy: od południa dzisiaj jest to w głównej gałęzi. Oto liczby.
Najpierw wynik. Zajętość pamięci podręcznej naszego repozytorium spadła z 5,32 GB w 63 wpisach do 2,46 GB w 13. Analiza bezpieczeństwa C#, która kiedyś szła przy każdym pull requeście, trwa tam teraz 4 sekundy zamiast 4,5 minuty, a nadal sprawdza wszystko, co trafia do was w grze. Jedno i drugie bez rezygnacji z choćby jednej kontroli, która was chroni.
Krótko: do czego służy pamięć podręczna? Każde budowanie startuje na świeżo utworzonej, pustej maszynie. Żeby nie zaczynać za każdym razem od zera, może zapisywać wyniki pośrednie, a następnym razem je pobiera zamiast tworzyć od nowa. Dostajecie na to 10 GB na repozytorium. Gdy się zapełni, GitHub usuwa to, czego najdłużej nie używano. I właśnie to było naszym problemem: miejsce było zajęte, tylko nie tym, czym trzeba.
Błąd 1: klucz bez platformy. Unity przy budowaniu tworzy ogromny folder pośredni. Używają go cztery buildy, Windows, Linux, macOS i WebGL, i każdy potrzebuje własnego. Build dla Windows był jedynym, którego klucz pamięci nie miał oznaczenia platformy. Dopasowywał więc pierwszy dostępny wpis, którym był wpis WebGL: pobierane 1,7 GB, które Unity słusznie rozpoznawało jako „to tu nie pasuje” i importowało wszystko od zera. W logu: 257 sekund importu zamiast 85. Klucz brzmi teraz Library-Windows- i po strachu.
Błąd 2: nikt tego nigdy nie zapisywał. Bardziej żenujące odkrycie. Wszystkie cztery buildy sumiennie próbowały odtworzyć folder pośredni, ale żaden workflow go nigdy nie zapisywał. Jedyny istniejący wpis pochodził z narzędzia uruchamianego ręcznie. Najważniejsza pamięć podręczna projektu była więc utrzymywana przypadkiem. 🙈 Teraz zapisuje się ją celowo.
Decyzja, która oszczędza miejsce: liczy się tylko ścieżka krytyczna. Oczywistym ruchem byłoby zapisywanie pamięci dla wszystkich czterech platform. Rzut oka na czasy prawdziwego wydania pokazuje, czemu to byłby nonsens: wszystkie cztery buildy startują jednocześnie, Windows kończy po 11 minutach, Linux i macOS po 13,7, a WebGL po 31,6. Dopóki działa WebGL, pozostałe i tak czekają. Czekanie skraca więc tylko jego pamięć; pozostałe trzy kosztowałyby około 4,5 GB z naszych 10 GB i zaoszczędziłyby dokładnie zero minut. Zapisujemy więc dokładnie jedną.
Błąd 3: Docker zjadł resztę. Przy budowaniu obrazów serwera kiedyś każda warstwa pośrednia trafiała do pamięci podręcznej: mniej więcej 3,27 GB w 24 wpisach, 64% całego budżetu, odświeżane przy każdym pushu. Ta powódź regularnie wypychała jedną ważną pamięć Unity. Teraz zapisywany jest tylko etap końcowy, a każdy z naszych czterech obrazów ma własną przestrzeń nazw; wcześniej dzieliły jedną i ciągle się nawzajem wyrzucały. Z 3,27 GB zrobiło się około 150 MB.
Analiza bezpieczeństwa: sprawdzamy tam, gdzie to ma sens. CodeQL, narzędzie GitHuba do wyszukiwania luk w zabezpieczeniach, było naszym drugim największym pożeraczem czasu: 230 minut w osiem dni, prawie wszystko to część dla C#, która do analizy przebudowuje cały projekt, i to przy każdym pull requeście. Teraz działa w całości przy scalaniu z główną gałęzią i raz w tygodniu; pull requesty zachowują szybkie kontrole. Pomiar po zmianie: 4 sekundy w pull requeście i niezmienne dobre 4 minuty w głównej gałęzi. Każda linia, która naprawdę do was trafia, nadal jest sprawdzana, tylko nie trzy razy.
Drobiazgi, które się sumują. Pakiety .NET są teraz w pamięci podręcznej, ale tylko na ścieżce, na którą człowiek czeka. Emulator obcych architektur procesora konfigurujemy tylko wtedy, gdy naprawdę budujemy pod taką. Polecenie sprzątające, które na świeżo uruchomionej maszynie nie miało nic do roboty, a kosztowało do 1,4 minuty na build, zniknęło. A pliki pośrednie buildu wydania są czyszczone po 7 dniach zamiast po 90: nazbierało się ich 703, razem 7,4 GB.
Czego celowo nie zrobiliśmy. Dwie propozycje z naszej własnej analizy odrzuciliśmy po pomiarach, zamiast sprzedawać je jako ładne linijki w dzienniku zmian. Podział testów na dwa równoległe zadania zyskałby dokładnie cztery sekundy (jedna część trwa 2:54, druga 4 sekundy) i zmieniłby nazwę wymaganej kontroli, przez co każdy pull request wisiałby w „oczekiwaniu” w nieskończoność. A pominięcie wielkiej bramki testowej przed wydaniem też nic by nie dało, bo kończy się dawno, zanim WebGL skończy budowanie. Optymalizacja, która nic nie zyskuje, a zabiera siatkę bezpieczeństwa, nie jest optymalizacją.
A potem wypadł z tego prawdziwy błąd. W środku tych porządków każdy pull request nagle przekraczał limit czasu: jeden test trwał 212 sekund, a dozwolone jest 120. Drugi najwolniejszy w tym samym przebiegu: 10,5 sekundy. Przyczyną była ścieżka błędu w naszym serwerze, która zamiast czysto zamknąć uszkodzone żądanie HTTP, po prostu je ucinała. Na Linuksie system odzyskuje takie połączenia dopiero w przeglądzie co dwie minuty, a zamykanie cierpliwie na niego czekało. Na Windowsie nigdy się to nie ujawniało, dlatego gryzło tylko na serwerze budującym. Teraz serwer w takim przypadku odpowiada uczciwym kodem błędu i sam zamyka połączenie: test kończy się w dobre 20 sekund, a prawdziwi gracze, którzy trafią na taką czkawkę, dostaną zrozumiały błąd zamiast ściętego połączenia. Dzień porządków w CI, który znajduje błąd produkcyjny, to dobry dzień. 🐛
Nic z tego nie zmienia dla was niczego bezpośrednio: żadnego nowego bloku, żadnego nowego zwierzęcia. Po prostu skraca drogę między „skończyliśmy kodować” a „da się to zainstalować”. I potwierdza to samo zdanie co ostatnio: najpierw mierz, potem ruszaj. 🚀
Komentarze