Profilazione del gioco WebGL con il flag about:tracing

Lilli Thompson
Lilli Thompson

Se non puoi misurarlo, non puoi migliorarlo.

Lord Kelvin

Per far funzionare i tuoi giochi HTML5 più velocemente, devi prima individuare i colli di bottiglia delle prestazioni, ma può essere difficile. La valutazione dei dati relativi ai fotogrammi al secondo (FPS) è un buon inizio, ma per avere un quadro completo, devi comprendere le sfumature delle attività di Chrome.

Lo strumento about:tracing fornisce informazioni che ti aiutano a evitare soluzioni rapide volte a migliorare il rendimento, ma che sono essenzialmente supposizioni ben intenzionate. Risparmierai molto tempo ed energie, avrai un quadro più chiaro di cosa fa Chrome con ogni frame e potrai utilizzare queste informazioni per ottimizzare il tuo gioco.

Ciao about:tracing

Lo strumento about:tracing di Chrome ti offre una panoramica di tutte le attività di Chrome in un determinato periodo di tempo con una granularità così elevata che all'inizio potrebbe risultare un po' scoraggiante. Molte delle funzioni di Chrome sono strumentate per il monitoraggio immediatamente, quindi senza alcuna strumentazione manuale puoi comunque utilizzare about:tracing per monitorare le prestazioni. (vedi una sezione successiva sull'instrumentazione manuale del codice JS)

Per visualizzare la visualizzazione di monitoraggio, digita "about:tracing" nell'omnibox (barra degli indirizzi) di Chrome.

Omnibox di Chrome
Digita "about:tracing" nell'omnibox di Chrome

Nello strumento di tracciamento puoi avviare la registrazione, eseguire il gioco per alcuni secondi e visualizzare i dati della traccia. Ecco un esempio di come potrebbero essere i dati:

Risultato di tracciamento semplice
Risultato del rilevamento semplice

Sì, è davvero complicato. Vediamo come leggerlo.

Ogni riga rappresenta un processo di cui viene eseguito il profiling, l'asse sinistro-destro indica il tempo e ogni casella colorata è una chiamata di funzione strumentata. Esistono righe per diversi tipi di risorse. I più interessanti per il profiling dei giochi sono CrGpuMain, che mostra cosa sta facendo l'unità di elaborazione grafica (GPU), e CrRendererMain. Ogni traccia contiene righe CrRendererMain per ogni scheda aperta durante il periodo di tracciamento (inclusa la scheda about:tracing stessa).

Quando leggi i dati traccia, la prima operazione da svolgere è determinare quale riga CrRendererMain corrisponde al tuo gioco.

Risultato del monitoraggio semplice evidenziato
Risultato del rilevamento semplice evidenziato

In questo esempio, i due candidati sono 2216 e 6516. Purtroppo al momento non esiste un modo raffinato per individuare l'applicazione, tranne cercare la riga che esegue molti aggiornamenti periodici (o, se hai strumentato manualmente il codice con punti di traccia, cercare la riga contenente i dati di traccia). In questo esempio, dalla frequenza degli aggiornamenti sembra che il processo 6516 stia eseguendo un ciclo principale. Se chiudi tutte le altre schede prima di avviare la traccia, sarà più facile trovare CrRendererMain corretto. Tuttavia, potrebbero essere presenti righe CrRendererMain per processi diversi dal tuo gioco.

Trovare la tua inquadratura

Una volta individuata la riga corretta nello strumento di tracciamento per il tuo gioco, il passaggio successivo consiste nel trovare il loop principale. Il loop principale ha la forma di un pattern ripetuto nei dati di monitoraggio. Puoi spostarti nei dati di tracciamento utilizzando i tasti W, A, S e D: A e D per spostarti verso sinistra o destra (avanti e indietro nel tempo) e W e S per aumentare e diminuire lo zoom sui dati. Se il gioco viene eseguito a 60 Hz, dovresti aspettarti che il loop principale sia un pattern che si ripete ogni 16 millisecondi.

Sembra che ci siano tre frame di esecuzione
Sembrano tre frame di esecuzione

Una volta individuato il battito cardiaco del gioco, puoi esaminare esattamente cosa fa il codice in ogni frame. Usa W, A, S, D per aumentare lo zoom finché non riesci a leggere il testo nelle caselle delle funzioni.

Analisi approfondita di un frame di esecuzione
Approfondimento di un frame di esecuzione

Questa raccolta di riquadri mostra una serie di chiamate di funzione, con ogni chiamata rappresentata da un riquadro colorato. Ogni funzione è stata chiamata dalla casella sopra, quindi in questo caso puoi vedere che MessageLoop::RunTask ha chiamato RenderWidget::OnSwapBuffersComplete, che a sua volta ha chiamato RenderWidget::DoDeferredUpdate e così via. Leggendo questi dati, puoi avere una visione completa di cosa ha chiamato cosa e del tempo impiegato per ogni esecuzione.

Ma qui è dove le cose si complicano un po'. Le informazioni esposte da about:tracing sono le chiamate di funzioni non elaborate dal codice sorgente di Chrome. Puoi fare supposizioni ragionevoli su cosa fa ogni funzione dal nome, ma le informazioni non sono esattamente user-friendly. È utile per vedere il flusso complessivo del frame, ma per capire cosa sta succedendo serve qualcosa di più leggibile.

Aggiunta di tag traccia

Fortunatamente, esiste un modo semplice per aggiungere la misurazione manuale al codice per creare dati di traccia: console.time e console.timeEnd.

console.time("update");
update
();
console
.timeEnd("update");
console
.time("render");
update
();
console
.timeEnd("render");

Il codice riportato sopra crea nuove caselle nel nome della visualizzazione di monitoraggio con i tag specificati, quindi se esegui di nuovo l'app vedrai le caselle "update" e "render" che mostrano il tempo trascorso tra le chiamate di inizio e di fine per ogni tag.

Tag aggiunti manualmente
Tag aggiunti manualmente

In questo modo, puoi creare dati di tracciamento leggibili da persone per monitorare gli hotspot nel codice.

GPU o CPU?

Con la grafica con accelerazione hardware, una delle domande più importanti che puoi porti durante il profiling è: questo codice è vincolato alla GPU o alla CPU? Con ogni frame esegui un po' di lavoro di rendering sulla GPU e un po' di logica sulla CPU. Per capire cosa rallenta il tuo gioco, devi vedere come il lavoro è bilanciato tra le due risorse.

Innanzitutto, individua la riga nella visualizzazione del tracciamento denominata CrGPUMain, che indica se la GPU è occupata in un determinato momento.

Tracce GPU e CPU

Puoi vedere che ogni frame del gioco causa un utilizzo della CPU in CrRendererMain e sulla GPU. La traccia riportata sopra mostra un caso d'uso molto semplice in cui sia la CPU sia la GPU sono inattive per la maggior parte di ogni frame di 16 ms.

La visualizzazione del monitoraggio diventa davvero utile quando un gioco funziona lentamente e non sai quale risorsa stai utilizzando al massimo. Esaminare la relazione tra le righe GPU e CPU è la chiave per il debug. Prendi lo stesso esempio di prima, ma aggiungi un po' di lavoro extra nel loop di aggiornamento.

console.time("update");
doExtraWork
();
update
(Math.min(50, now - time));
console
.timeEnd("update");

console
.time("render");
render
();
console
.timeEnd("render");

Ora vedrai una traccia simile alla seguente:

Tracce GPU e CPU

Cosa ci dice questa traccia? Possiamo vedere che il frame mostrato va da circa 2270 ms a 2320 ms, il che significa che ogni frame richiede circa 50 ms (una frequenza frame di 20 Hz). Puoi vedere frammenti di caselle colorate che rappresentano la funzione di rendering accanto alla casella di aggiornamento, ma il frame è interamente dominato dall'aggiornamento stesso.

A differenza di quanto accade sulla CPU, puoi vedere che la GPU è ancora inattiva per la maggior parte di ogni frame. Per ottimizzare questo codice, puoi cercare le operazioni che possono essere eseguite nel codice shader e spostarle sulla GPU per utilizzare al meglio le risorse.

Che cosa succede quando il codice shader stesso è lento e la GPU è sottoutilizzata? E se rimuovessimo il lavoro non necessario dalla CPU e aggiungessimo un po' di lavoro nel codice dello shader del frammento? Ecco un frammento shader inutilmente costoso:

#ifdef GL_ES
precision highp
float;
#endif
void main(void) {
 
for(int i=0; i<9999; i++) {
    gl_FragColor
= vec4(1.0, 0, 0, 1.0);
 
}
}

Che aspetto ha una traccia di codice che utilizza lo shader?

Tracce GPU e CPU quando si utilizza codice GPU lento
Tracce GPU e CPU quando si utilizza codice GPU lento

Prendi di nuovo nota della durata di un fotogramma. Qui il pattern ripetuto va da circa 2750 ms a 2950 ms, con una durata di 200 ms (frequenza frame di circa 5 Hz). La riga CrRendererMain è quasi completamente vuota, il che significa che la CPU è inattiva per la maggior parte del tempo, mentre la GPU è sovraccaricata. Questo è un segno sicuro che gli shader sono troppo pesanti.

Se non avevi la visibilità esatta di cosa causava il frame rate basso, potresti osservare l'aggiornamento a 5 Hz e avere la tentazione di accedere al codice del gioco e iniziare a provare a ottimizzare o rimuovere la logica di gioco. In questo caso, non servirebbe a nulla, perché non è la logica del loop di gioco a occupare tempo. In effetti, questa traccia indica che un maggiore utilizzo della CPU per ogni frame sarebbe essenzialmente "senza costi" in quanto la CPU è inutilizzata, quindi assegnarle più lavoro non influisce sul tempo necessario per il frame.

Esempi reali

Ora vediamo come sono i dati di monitoraggio di una partita reale. Uno dei vantaggi dei giochi creati con tecnologie web aperte è che puoi vedere cosa succede nei tuoi prodotti preferiti. Se vuoi provare gli strumenti di profilazione, puoi scegliere il tuo titolo WebGL preferito dal Chrome Web Store e crearne il profilo con about:tracing. Questo è un esempio di traccia presa dall'eccellente gioco WebGL Skid Racer.

Tracciare una partita reale
Tracciare una partita reale

Sembra che ogni fotogramma richieda circa 20 ms, il che significa che la frequenza fotogrammi è di circa 50 FPS. Puoi vedere che il lavoro è bilanciato tra CPU e GPU, ma la GPU è la risorsa più richiesta. Se vuoi scoprire come creare il profilo di esempi reali di giochi WebGL, prova alcuni dei titoli del Chrome Web Store creati con WebGL, tra cui:

Conclusione

Se vuoi che il gioco venga eseguito a 60 Hz, per ogni frame tutte le operazioni devono rientrare in 16 ms di CPU e 16 ms di GPU. Hai due risorse che possono essere utilizzate in parallelo e puoi spostare il lavoro da una all'altra per massimizzare il rendimento. La visualizzazione about:tracing di Chrome è uno strumento inestimabile per ottenere informazioni su cosa fa effettivamente il codice e ti aiuterà a massimizzare il tempo di sviluppo affrontando i problemi giusti.

Passaggi successivi

Oltre alla GPU, puoi anche tracciare altre parti del runtime di Chrome. Chrome Canary, la versione in fase iniziale di Chrome, è dotata di strumenti per monitorare I/O, IndexedDB e diverse altre attività. Per una comprensione più approfondita dello stato attuale degli eventi di monitoraggio, leggi questo articolo di Chromium.

Se sei uno sviluppatore di giochi web, assicurati di guardare il video di seguito. Si tratta di una presentazione del team Game Developer Advocate di Google alla GDC 2012 sull'ottimizzazione delle prestazioni per i giochi di Chrome: