Profilazione del gioco WebGL con il flag about:tracing

Lilli Thompson
Lilli Thompson

Se non puoi misurarli, non puoi migliorarli.

Lord Kelvin

Per un'esecuzione più rapida dei giochi HTML5, devi prima individuare i colli di bottiglia delle prestazioni, ma questo può essere difficile. La valutazione dei dati relativi ai frame al secondo (f/s) è un punto di partenza, ma per avere un quadro completo devi comprendere le sfumature delle attività di Chrome.

Lo strumento about:tracing fornisce le informazioni che ti aiutano a evitare soluzioni alternative frettolose volte a migliorare il rendimento, ma che in sostanza si traducono in congetture ben intenzionali. Potrai risparmiare molto tempo ed energia, ottenere un quadro più chiaro di cosa fa Chrome con ogni frame e utilizzare queste informazioni per ottimizzare il tuo gioco.

Introduzione al tracciamento

Lo strumento about:tracciamento di Chrome ti offre una finestra su tutte le attività di Chrome in un arco di tempo con una granularità così alta che all'inizio potrebbe sembrare molto complesso. Molte delle funzioni di Chrome sono pronte all'uso per essere subito tracciate, quindi senza utilizzare alcuna strumentazione manuale puoi comunque usare la funzionalità about:tracing per monitorare le tue prestazioni. (consulta una sezione successiva sulla strumentazione manuale di JS).

Per vedere la visualizzazione di tracciamento, digita "about:tracing" nella omnibox (barra degli indirizzi) di Chrome.

omnibox di Chrome
Digita "about:tracing" nella omnibox di Chrome

Dallo strumento di tracciamento, puoi iniziare a registrare, eseguire il gioco per alcuni secondi e visualizzare i dati di traccia. Ecco un esempio di come potrebbero presentarsi i dati:

Risultato di tracciamento semplice
Risultato di tracciamento semplice

Sì, è confuso, va bene. Parliamo di come leggerlo.

Ogni riga rappresenta un processo che viene profilato, l'asse sinistro-destro indica il tempo e ogni casella colorata è una chiamata di funzione instrumentata. Ci sono righe per una serie di tipi diversi di risorse. Quelli più interessanti per la profilazione dei giochi sono CrGpuMain, che mostra le operazioni della GPU (Graphics Processing Unit) e CrRendererMain. Ogni traccia contiene righe CrRendererMain per ogni scheda aperta durante il periodo della traccia (inclusa la scheda about:tracing stessa).

Durante la lettura dei dati di traccia, la prima attività è determinare quale riga CrRendererMain corrisponde al tuo gioco.

Risultato di tracciamento semplice evidenziato
Risultato di tracciamento semplice evidenziato

In questo esempio i due candidati sono 2216 e 6516. Purtroppo al momento non esiste un modo corretto per scegliere la tua applicazione, se non per cercare la riga che esegue molti aggiornamenti periodici (oppure, se hai instrumentato manualmente il codice con i punti di traccia, per cercare la riga contenente i dati di traccia). In questo esempio, sembra che il 6516 stia eseguendo un loop principale dalla frequenza degli aggiornamenti. Se chiudi tutte le altre schede prima di iniziare la traccia, sarà più facile trovare il CrRendererMain corretto. Tuttavia, potrebbero esserci ancora righe CrRendererMain per processi diversi dal gioco.

Ricerca della tua cornice

Dopo aver individuato la riga corretta nello strumento di tracciamento per il tuo gioco, il passaggio successivo consiste nell'individuare il loop principale. Il loop principale appare come uno schema ripetuto nei dati di tracciamento. Puoi spostarti tra i dati di tracciamento utilizzando i tasti W, A, S e D: A e D per spostarti a sinistra o a destra (avanti e indietro nel tempo) e W e S per aumentare e diminuire lo zoom dei dati. Se il gioco è in esecuzione a 60 Hz, il tuo loop principale è un pattern che si ripete ogni 16 millisecondi.

Sembra che tre frame di esecuzione
Sembra tre frame di esecuzione

Una volta individuato il battito del tuo gioco, puoi analizzare in dettaglio cosa sta facendo esattamente il tuo codice in ogni frame. Utilizza i tasti W, A, S, D per aumentare lo zoom fino a quando riesci a leggere il testo nelle caselle delle funzioni.

Esplorare un frame di esecuzione
Presentazione di un frame di esecuzione

Questa raccolta di riquadri mostra una serie di chiamate di funzione, dove ogni chiamata è rappresentata da un riquadro colorato. Ogni funzione è stata chiamata dalla casella sopra di essa, quindi in questo caso si può vedere quel MessageLoop::RunTask denominato RenderWidget::OnExchangeBuffersComplete, che a sua volta ha chiamato RenderWidget::DoDeferredUpdate e così via. Leggendo questi dati, è possibile ottenere una visione completa del cosiddetto "cosa" e della durata di ogni esecuzione.

Ma qui è un po' appiccicoso. Le informazioni esposte da about:tracing sono le chiamate di funzione non elaborate dal codice sorgente di Chrome. Puoi formulare ipotesi oculate sul comportamento di ciascuna funzione partendo dal nome, ma le informazioni non sono esattamente facili da usare. È utile vedere il flusso complessivo del frame, ma è necessario qualcosa di un po' più leggibile per capire davvero cosa sta succedendo.

Aggiunta di tag di traccia

Fortunatamente, esiste un modo semplice per aggiungere la strumentazione manuale al codice al fine di 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 vista di tracciamento con i tag specificati, quindi se riesegui l'app vedrai "update" e "rendering" che mostrano il tempo trascorso tra le chiamate di inizio e di fine di ogni tag.

Tag aggiunti manualmente
Tag aggiunti manualmente

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

GPU o CPU?

Nel caso della grafica con accelerazione hardware, una delle domande più importanti che puoi porti durante la profilazione è: questo codice è vincolato alla GPU o alla CPU? Con ogni frame eseguirai un lavoro di rendering sulla GPU e di una logica sulla CPU; per capire cosa rallenta il tuo gioco, devi capire come viene bilanciato il lavoro tra le due risorse.

Innanzitutto, trova nella vista di tracciamento la linea CrGPUMain, che indica se la GPU è occupata in un determinato momento.

Tracce GPU e CPU

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

La visualizzazione di tracciamento è davvero utile quando si ha un gioco che funziona lentamente e non si è certi della risorsa che si sta sfruttando al massimo. Il segreto per il debug è osservare la relazione tra le linee GPU e CPU. Prendi lo stesso esempio di prima, ma aggiungi un po' di lavoro in più 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

Che cosa ci dice questa traccia? Possiamo vedere che il fotogramma mostrato va da circa 2270 ms a 2320 ms, il che significa che ogni fotogramma richiede circa 50 ms (un frame rate di 20Hz). Puoi vedere frammenti di riquadri colorati che rappresentano la funzione di rendering accanto alla finestra di aggiornamento, ma il frame è interamente dominato dall'aggiornamento stesso.

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

E quando il codice dello shaker è lento e la GPU è sovraccarica? E se rimuoviamo il lavoro non necessario dalla CPU e aggiungiamo invece del lavoro nel codice del Snippet Shader. Ecco uno shaker di frammenti 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 usa quello shaker?

Tracce GPU e CPU quando viene utilizzato un codice GPU lento
Tracce di GPU e CPU quando viene utilizzato un codice GPU lento

Anche in questo caso, prendi nota della durata di un frame. In questo caso il pattern ripetuto va da circa 2750 ms a 2950 ms, una durata di 200 ms (frame rate di circa 5Hz). La riga CrRendererMain è quasi completamente vuota, il che significa che la CPU è inattiva la maggior parte delle volte, mentre la GPU è sovraccarica. Questo è un segnale sicuro che i tuoi Shader sono troppo pesanti.

Se non hai visibilità su cosa sta causando la bassa frequenza fotogrammi, potresti osservare l'aggiornamento a 5 Hz e provare ad accedere al codice del gioco e iniziare a ottimizzare o rimuovere la logica del gioco. In questo caso non sarebbe affatto positivo, perché la logica nel ciclo di gioco non è ciò che fa mangiare il tempo. In effetti, ciò che questa traccia indica è che fare più lavoro della CPU per ogni frame sarebbe essenzialmente "senza costi" in quanto la CPU è inattiva, quindi aumentare il lavoro non influisce sulla durata del frame.

Esempi reali

Ora diamo un'occhiata ai dati di tracciamento di un gioco reale. Una delle cose più interessanti dei giochi realizzati con tecnologie open web è che ti permette di vedere cosa succede nei tuoi prodotti preferiti. Se desideri provare gli strumenti di profilazione, puoi scegliere il tuo titolo WebGL preferito dal Chrome Web Store e profilarlo con about:tracing. Questa è una traccia di esempio tratta dall'eccellente gioco WebGL Skid Racer.

Tracciare un gioco reale
Tracciare una partita reale

Sembra che ogni frame 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 vedere com'è creare una presentazione di esempi reali di giochi WebGL, prova a sperimentare con alcuni dei titoli del Chrome Web Store realizzati con WebGL, tra cui:

Conclusione

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

Passaggi successivi

Oltre alla GPU, puoi tracciare anche altre parti del runtime di Chrome. Chrome Canary, la versione in fase iniziale di Chrome, è in grado di tracciare IO, IndexedDB e diverse altre attività. Leggi questo articolo di Chromium per una comprensione più approfondita dello stato attuale del tracciamento degli eventi.

Se sei uno sviluppatore di giochi web, assicurati di guardare il seguente video. È una presentazione del team Game Developer Advocate di Google alla GDC 2012 sull'ottimizzazione delle prestazioni dei giochi di Chrome: