JavaScript con memoria statica con pool di oggetti

Colt McAnlis
Colt McAnlis

Introduzione

Riceverai un'email che ti informa delle prestazioni scadenti del tuo gioco web / della tua app web dopo un certo periodo di tempo. Analizzi il codice e non vedi nulla di rilevante finché non apri gli strumenti per le prestazioni della memoria di Chrome e vedi quanto segue:

Uno snapshot dalla sequenza temporale della memoria

Uno dei tuoi colleghi ridacchia perché si rende conto che hai un problema di prestazioni legato alla memoria.

Nel grafico della memoria, questo schema a denti di sega indica un problema di prestazioni potenzialmente critico. Man mano che aumenta la memoria utilizzata, l'area del grafico crescerà anche nell'acquisizione della sequenza temporale. Quando il grafico cala improvvisamente, si tratta di un'istanza in cui è stato eseguito Garbage Collector che ha ripulito gli oggetti di memoria a cui viene fatto riferimento.

Significato dei denti seghe

In un grafico come questo, puoi vedere che si verificano molti eventi di garbage collection, che possono essere dannosi per le prestazioni delle tue app web. Questo articolo spiega come assumere il controllo dell'utilizzo della memoria, riducendo l'impatto sulle prestazioni.

Costi di garbage collection e prestazioni

Il modello di memoria di JavaScript si basa su una tecnologia nota come Garbage Collector. In molti linguaggi, il programmatore è direttamente responsabile di allocare e liberare memoria dall'heap di memoria del sistema. Un sistema Garbage Collector, tuttavia, gestisce questa attività per conto del programmatore, il che significa che gli oggetti non vengono liberati direttamente dalla memoria quando il programmatore li deriferisce, ma piuttosto in un momento successivo, quando l’euristica del GC decide che sarebbe utile farlo. Questo processo decisionale richiede che il GC esegua alcune analisi statistiche su oggetti attivi e inattivi, il che richiede un arco di tempo per l’esecuzione.

La garbage collection viene spesso rappresentata come l'opposto della gestione manuale della memoria, che richiede al programmatore di specificare quali oggetti distribuire e restituire al sistema di memoria

Il processo in cui un GC recupera memoria non è senza costi, di solito riduce le prestazioni disponibili impiegando un po' di tempo per svolgere il suo lavoro; inoltre, è il sistema stesso a decidere quando eseguire. Non hai alcun controllo su questa azione; durante l'esecuzione del codice può verificarsi un impulso GC in qualsiasi momento, che bloccherà l'esecuzione del codice fino al suo completamento. La durata di questo impulso è generalmente sconosciuta; l'esecuzione richiederà un po' di tempo, a seconda di come il tuo programma utilizza la memoria in un determinato punto.

Le applicazioni a prestazioni elevate si basano su limiti di prestazioni coerenti per garantire un'esperienza uniforme agli utenti. I sistemi di garbage collector possono causare un cortocircuito di questo obiettivo, poiché possono essere eseguiti in momenti casuali per durate casuali, consumando il tempo disponibile necessario all'applicazione per raggiungere gli obiettivi di prestazioni.

Riduci il tasso di abbandono della memoria, riduci le tasse sulla garbage collection

Come indicato, un battito di GC si verifica una volta che una serie di euristica determina la presenza di un numero sufficiente di oggetti inattivi per cui un battito potrebbe essere utile. Di conseguenza, la chiave per ridurre il tempo impiegato da Garbage Collector dall'applicazione sta nell'eliminare il maggior numero possibile di casi di creazione e rilascio di oggetti eccessivi. Questo processo di creazione/liberazione frequente di oggetti è chiamato "abbandono della memoria". Se puoi ridurre il tasso di abbandono della memoria durante il ciclo di vita dell'applicazione, riduci anche il tempo impiegato da GC per l'esecuzione. Ciò significa che devi rimuovere / ridurre il numero di oggetti creati ed eliminati, di fatto, devi interrompere l'allocazione della memoria.

Questa procedura sposterà il tuo grafico della memoria da :

Uno snapshot dalla sequenza temporale della memoria

a questo:

JavaScript statico memoria

Come puoi vedere da questo modello, il grafico non presenta più uno schema a dente di sega, ma cresce molto all'inizio per poi aumentare lentamente nel tempo. Se stai riscontrando problemi di prestazioni a causa del tasso di abbandono della memoria, questo è il tipo di grafico che ti conviene creare.

Passaggio a JavaScript con memoria statica

Static Memory JavaScript è una tecnica che prevede la pre-allocazione, all'avvio dell'app, di tutta la memoria necessaria per il suo ciclo di vita e la gestione di questa memoria durante l'esecuzione poiché gli oggetti non sono più necessari. Possiamo raggiungere questo obiettivo in pochi semplici passaggi:

  1. Instrumenta la tua applicazione per determinare il numero massimo di oggetti di memoria attiva richiesti (per tipo) per una serie di scenari di utilizzo
  2. Implementa nuovamente il codice per pre-allocare la quantità massima, quindi recupera/rilascia manualmente anziché andare alla memoria principale.

In realtà, per raggiungere la posizione n. 1 è necessario passare dalla fase 2 alla fase 2, quindi iniziamo da lì.

Pool di oggetti

In termini semplici, il pool di oggetti è il processo di conservazione di un insieme di oggetti inutilizzati che condividono un tipo. Quando hai bisogno di un nuovo oggetto per il tuo codice, anziché allocarne uno nuovo dal heap di memoria di sistema, ricicla uno degli oggetti non utilizzati del pool. Una volta che il codice esterno è stato creato con l'oggetto, invece di rilasciarlo nella memoria principale, viene restituito al pool. Poiché l'oggetto non viene mai dereferenziato (ovvero eliminato) dal codice, non sarà garbage collection. L'utilizzo dei pool di oggetti riporta il controllo della memoria nelle mani del programmatore, riducendo l'influenza del garbage collector sulle prestazioni.

Poiché l'applicazione gestisce un insieme eterogeneo di tipi di oggetti, un uso corretto dei pool di oggetti richiede di avere un pool per tipo che presenti un tasso di abbandono elevato durante il runtime dell'applicazione.

var newEntity = gEntityObjectPool.allocate();
newEntity.pos = {x: 215, y: 88};

//..... do some stuff with the object that we need to do

gEntityObjectPool.free(newEntity); //free the object when we're done
newEntity = null; //free this object reference

Per la maggior parte delle applicazioni, arriverai a un certo livello in termini di allocazione di nuovi oggetti. Durante più esecuzioni dell'applicazione, dovresti essere in grado di avere un'idea chiara di cosa sia questo limite superiore e puoi preallocare quel numero di oggetti all'avvio dell'applicazione.

Preallocazione degli oggetti

L'implementazione del pool di oggetti nel progetto ti fornirà un massimo teorico per il numero di oggetti richiesti durante il runtime dell'applicazione. Dopo aver eseguito il tuo sito attraverso vari scenari di test, puoi avere un'idea dei tipi di requisiti di memoria che saranno necessari e puoi catalogare quei dati da qualche parte e analizzarli per comprendere quali sono i limiti massimi dei requisiti di memoria per la tua applicazione.

Quindi, nella versione di spedizione dell'app, puoi impostare la fase di inizializzazione in modo da precompilare tutti i pool di oggetti fino a un determinato importo. Questa operazione eseguirà il push di tutta l'inizializzazione degli oggetti in primo piano nella tua app e ridurrà il numero di allocazioni che si verificano in modo dinamico durante la sua esecuzione.

function init() {
  //preallocate all our pools. 
  //Note that we keep each pool homogeneous wrt object types
  gEntityObjectPool.preAllocate(256);
  gDomObjectPool.preAllocate(888);
}

La quantità che scegli ha molto a che fare con il comportamento della tua applicazione; a volte il massimo teorico non è l'opzione migliore. Ad esempio, se scegli la media massima, potresti avere una quantità di memoria ridotta per i non utenti esperti.

Lontano da un proiettile d'argento

Esiste un'intera classificazione di app in cui un modello di crescita della memoria statico può essere vincente. Come fa notare Renato Mangini dal collega Chrome DevRel, però, esistono alcuni svantaggi.

Conclusione

Uno dei motivi per cui JavaScript è ideale per il web dipende dal fatto che è un linguaggio veloce, divertente e facile da utilizzare. Ciò è dovuto principalmente alla sua bassa barriera alle restrizioni di sintassi e alla sua gestione dei problemi di memoria per tuo conto. Puoi scrivere codice e lasciare che si occupi delle attività più sporche. Tuttavia, per le applicazioni web ad alte prestazioni, come i giochi HTML5, GC può spesso disperdere la frequenza frame necessaria, riducendo l'esperienza dell'utente finale. Con un po' di strumentazione attenta e l'adozione di pool di oggetti, puoi ridurre questo carico sulla frequenza fotogrammi e recuperare quel tempo per cose più fantastiche.

Codice sorgente

Esistono molte implementazioni di pool di oggetti in giro per il Web, quindi non ti annoierò con un'altra. Ti indirizzerò invece a questi, ognuno dei quali presenta specifiche sfumature di implementazione; il che è importante, considerando che ogni utilizzo dell'applicazione può avere esigenze di implementazione specifiche.

Riferimenti