Opgeruimd: zo gebruiken we de cache van GitHub eindelijk goed 🧹
In de vorige techniekpost hebben we gemeten wat onze bouw-en-controle-pijplijn echt kost, en we ontdekten: geen geld, maar wachttijd en opslagruimte. Dat artikel eindigde met “het opruimwerk is gepland maar nog niet gedaan”. Het ging sneller dan verwacht: sinds vanmiddag staat het op de hoofdtak. Hier zijn de cijfers.
Eerst het resultaat. Het cachegebruik van onze repository is gedaald van 5,32 GB in 63 items naar 2,46 GB in 13. De C#-beveiligingsanalyse die bij elke pull request draaide, duurt daar nu 4 seconden in plaats van 4½ minuut, terwijl alles wat wordt uitgeleverd nog steeds gecontroleerd wordt. Beide zonder dat we één controle hebben laten vallen die jou beschermt.
Kort: waar is een cache voor? Elke build start op een vers aangemaakte, lege machine. Om niet elke keer vanaf nul te beginnen, mag die tussenresultaten opslaan: de volgende keer downloadt ze die in plaats van ze opnieuw te maken. Daarvoor krijg je 10 GB per repository. Is die vol, dan gooit GitHub weg wat het langst ongebruikt bleef. En dat was precies ons probleem: de ruimte was bezet, maar door de verkeerde dingen.
Fout 1: een sleutel zonder platform. Unity maakt bij het bouwen een enorme tussenmap aan. Vier builds gebruiken die (Windows, Linux, macOS, WebGL) en elk heeft een eigen map nodig. De Windows-build was de enige waarvan de cachesleutel geen platformmarkering had. Daardoor pakte hij het eerste beschikbare item, en dat was dat van WebGL: 1,7 GB gedownload, dat Unity vervolgens terecht als “hoort hier niet” herkende en helemaal opnieuw importeerde. In het log: 257 seconden import in plaats van 85. De sleutel is nu Library-Windows-, en de spooksensatie is voorbij.
Fout 2: niemand heeft hem ooit opgeslagen. De pijnlijkere vondst. Alle vier de builds probeerden braaf de tussenmap te herstellen, maar geen enkele workflow schreef hem ooit weg. Het ene item dat er wel was, kwam van een hulpmiddel dat je met de hand start. De belangrijkste cache van het project werd dus bij toeval bijgehouden. 🙈 Nu wordt hij met opzet geschreven.
De beslissing die ruimte bespaart: alleen het kritieke pad telt. De voor de hand liggende zet was geweest om alle vier de platformen te cachen. Een blik op de timing van een echte release laat zien waarom dat onzin zou zijn: alle vier de builds starten tegelijk, Windows is na 11 minuten klaar, Linux en macOS na 13,7 en WebGL na 31,6. Zolang WebGL draait, wachten de andere toch al. Alleen zijn cache verkort dus de wachttijd; de andere drie hadden zo'n 4,5 GB van onze 10 GB budget gekost en precies nul minuten bespaard. We cachen dus precies één.
Fout 3: Docker at de rest op. Bij het bouwen van onze serverimages werd elke tussenlaag als cache opgeslagen: ongeveer 3,27 GB in 24 items, 64% van het hele budget, bij elke push ververst. Die vloed verdrong betrouwbaar de ene Unity-cache die ertoe doet. Nu wordt alleen de eindfase opgeslagen en krijgt elk van onze vier images een eigen naamruimte; eerder deelden ze er één en gooiden ze elkaar steeds eruit. Van 3,27 GB is ongeveer 150 MB geworden.
Beveiligingsanalyse: controleer waar het telt. CodeQL, het hulpmiddel van GitHub om beveiligingslekken te vinden, was onze op een na grootste tijdvreter: 230 minuten over acht dagen, bijna allemaal het C#-deel, dat voor zijn analyse het hele project opnieuw bouwt, bij elke pull request. Dat draait nu volledig bij het samenvoegen in de hoofdtak en eens per week; pull requests houden de snelle controles. Gemeten na de wijziging: 4 seconden in een pull request, een ongewijzigde goede 4 minuten op de hoofdtak. Elke regel die jou echt bereikt wordt dus nog steeds gecontroleerd, alleen niet driemaal.
Kleinigheden die zich opstapelen. De .NET-pakketten worden nu gecachet, maar alleen op het pad waarop een mens wacht. Een emulator voor vreemde processorarchitecturen wordt alleen ingesteld als we er echt voor bouwen. Een opruimcommando dat op een vers opgestarte machine niets te doen had, maar per build tot 1,4 minuut kostte, is weg. En de tussenbestanden van een releasebuild worden na 7 dagen gewist in plaats van na 90: er hadden zich 703 opgestapeld, samen 7,4 GB.
Wat we bewust niet hebben gedaan. Twee voorstellen uit onze eigen analyse zijn na het meten geschrapt, in plaats van als mooie regels in een changelog te worden verkocht. De tests over twee parallelle jobs verdelen had precies vier seconden opgeleverd (het ene deel duurt 2:54, het andere 4 seconden) en had een verplichte controle hernoemd, waardoor elke pull request voor eeuwig op “wachten” was blijven hangen. En de grote testpoort voor een release overslaan had ook niets bespaard, want die is allang klaar terwijl WebGL nog bouwt. Een optimalisatie die niets oplevert maar een vangnet weghaalt, is geen optimalisatie.
En toen viel er een echte bug uit. Midden in deze opruimactie faalde plotseling elke pull request op een tijdslimiet: één enkele test duurde 212 seconden terwijl er 120 zijn toegestaan. De op een na traagste in dezelfde run: 10,5 seconden. De oorzaak was een foutpad in onze server dat een kapot HTTP-verzoek gewoon afkapte in plaats van het netjes te sluiten. Op Linux haalt het systeem zulke verbindingen alleen terug in een veegronde die elke twee minuten draait, en het afsluiten wachtte er geduldig op. Op Windows zag je het nooit, daarom beet het alleen op de buildserver. Nu beantwoordt de server zo'n geval met een eerlijke foutcode en sluit hij de verbinding zelf: de test is in ruim 20 seconden klaar, en echte spelers die zo'n hapering tegenkomen krijgen een begrijpelijke foutmelding in plaats van een afgekapte verbinding. Een CI-opruimdag die een productiebug vindt, is een goede dag. 🐛
Niets hiervan verandert rechtstreeks iets voor jou: geen nieuw blok, geen nieuw dier. Het verkort gewoon de weg tussen “klaar met programmeren” en “installeerbaar voor jou”. En het bewijst dezelfde zin als de vorige keer: eerst meten, dan pas aanraken. 🚀
Opmerkingen