Messo in ordine: come usiamo finalmente bene la cache di GitHub 🧹
Nell'ultimo articolo tecnico abbiamo misurato quanto costa davvero la nostra pipeline di build e controlli, e abbiamo scoperto che non sono soldi, ma tempo di attesa e spazio. Quell'articolo finiva con «il lavoro di pulizia è pianificato ma non ancora fatto». È andata più in fretta del previsto: da oggi a mezzogiorno è sul branch principale. Ecco i numeri.
Prima il risultato. L'uso della cache del nostro repository è sceso da 5,32 GB in 63 voci a 2,46 GB in 13. L'analisi di sicurezza del C# che prima girava a ogni pull request ora richiede 4 secondi invece di 4 minuti e mezzo, e continua a controllare tutto ciò che viene distribuito. Il tutto senza rinunciare a un solo controllo che vi protegge.
In breve: a cosa serve una cache? Ogni build parte su una macchina nuova e vuota. Per non ricominciare ogni volta da zero, può salvare dei risultati intermedi: la volta dopo li scarica invece di rifarli. Per questo hai a disposizione 10 GB per repository. Quando è pieno, GitHub elimina ciò che non è stato usato da più tempo. Ed era proprio questo il nostro problema: lo spazio era occupato, ma dalle cose sbagliate.
Errore 1: una chiave senza piattaforma. Unity durante la build crea una cartella intermedia enorme. La usano quattro build (Windows, Linux, macOS, WebGL) e ognuna ne ha bisogno di una propria. La build per Windows era l'unica la cui chiave di cache non portava il marcatore della piattaforma. Così trovava la prima voce disponibile, che era quella di WebGL: 1,7 GB scaricati che Unity riconosceva correttamente come «non appartiene a qui» e reimportava da zero. Nel log: 257 secondi di importazione invece di 85. Ora la chiave è Library-Windows-, e l'incubo è finito.
Errore 2: nessuno l'aveva mai salvata. La scoperta più imbarazzante. Tutte e quattro le build cercavano diligentemente di ripristinare la cartella intermedia, ma nessun workflow l'aveva mai scritta. L'unica voce che esisteva veniva da uno strumento che si avvia a mano. Quindi la cache più importante del progetto veniva mantenuta per caso. 🙈 Ora viene scritta di proposito.
La decisione che fa risparmiare spazio: conta solo il percorso critico. La mossa ovvia sarebbe stata mettere in cache tutte e quattro le piattaforme. Basta guardare i tempi di una vera release per capire perché sarebbe una sciocchezza: le quattro build partono insieme, Windows finisce dopo 11 minuti, Linux e macOS dopo 13,7 e WebGL dopo 31,6. Finché WebGL è in corso, gli altri aspettano comunque. Quindi solo la sua cache accorcia l'attesa; le altre tre sarebbero costate circa 4,5 GB dei nostri 10 GB e avrebbero fatto risparmiare esattamente zero minuti. Perciò ne mettiamo in cache una sola.
Errore 3: Docker si è mangiato il resto. Quando costruivamo le immagini dei nostri server, ogni livello intermedio veniva salvato come cache: circa 3,27 GB in 24 voci, il 64% dell'intero budget, aggiornato a ogni push. Questa inondazione espelleva con regolarità l'unica cache di Unity che conta. Ora viene salvata solo la fase finale, e ognuna delle nostre quattro immagini ha un proprio spazio dei nomi: prima ne condividevano uno solo e si cacciavano a vicenda. I 3,27 GB sono diventati circa 150 MB.
Analisi di sicurezza: controllare dove conta. CodeQL, lo strumento di GitHub per trovare falle di sicurezza, era il nostro secondo maggior consumo di tempo: 230 minuti in otto giorni, quasi tutti per la parte C#, che ricostruisce l'intero progetto per la propria analisi, a ogni pull request. Ora gira per intero quando si fa il merge nel branch principale e una volta alla settimana; le pull request mantengono i controlli rapidi. Misurato dopo la modifica: 4 secondi in una pull request, e sul branch principale i buoni 4 minuti di sempre. Quindi ogni riga che arriva davvero a voi viene comunque controllata, solo non tre volte.
Piccole cose che si sommano. I pacchetti .NET ora sono in cache, ma solo sul percorso su cui un essere umano aspetta. Un emulatore per architetture di processore estranee viene configurato solo quando costruiamo davvero per una di esse. Un comando di pulizia che su una macchina appena avviata non aveva nulla da fare, ma costava fino a 1,4 minuti a build, è sparito. E i file intermedi di una build di release vengono cancellati dopo 7 giorni invece di 90: se ne erano accumulati 703, per un totale di 7,4 GB.
Cosa abbiamo deliberatamente non fatto. Due proposte della nostra stessa analisi sono state scartate dopo averle misurate, invece di venderle come belle righe in un changelog. Dividere i test su due job paralleli avrebbe fatto guadagnare esattamente quattro secondi (una parte richiede 2:54, l'altra 4 secondi) e avrebbe rinominato un controllo obbligatorio, lasciando ogni pull request bloccata «in attesa» per sempre. E saltare il grande controllo dei test prima di una release non avrebbe fatto risparmiare niente, perché è finito da un pezzo mentre WebGL è ancora in costruzione. Un'ottimizzazione che non guadagna nulla ma toglie una rete di sicurezza non è un'ottimizzazione.
E poi ne è uscito un vero bug. Nel bel mezzo di questa pulizia, a un tratto ogni pull request falliva per un limite di tempo: un singolo test impiegava 212 secondi dove ne sono consentiti 120. Il secondo più lento della stessa esecuzione: 10,5 secondi. La causa era un percorso di errore del nostro server che, davanti a una richiesta HTTP difettosa, si limitava a tagliare la connessione invece di chiuderla in modo pulito. Su Linux il sistema recupera tali connessioni solo in un passaggio che avviene ogni due minuti, e lo spegnimento aspettava pazientemente. Su Windows non si vedeva mai, ed è per questo che morsicava solo sul server di build. Ora il server risponde a un caso simile con un onesto codice di errore e chiude lui stesso la connessione: il test finisce in venti secondi buoni, e i giocatori veri che si imbattono in un intoppo del genere riceveranno un errore comprensibile invece di una connessione tranciata. Una giornata di pulizia della CI che trova un bug in produzione è una bella giornata. 🐛
Niente di tutto questo cambia direttamente qualcosa per voi: nessun blocco nuovo, nessun animale nuovo. Accorcia semplicemente la strada tra «programmazione finita» e «installabile per voi». E conferma la stessa frase dell'ultima volta: prima misurare, poi toccare. 🚀
Commenti