Player di Flash (actionScript) mono-thread

AS3, Flex oscar Commenta l'articolo

In un player di Flash l’esecuzione di codice ActionScript e il rendering dello screen è gestito da un singolo thread, quindi quando viene realizzato il render dello schermo tutto il codice in esecuzione deve essere terminato.

Se abbiamo un pezzo di codice che richiede molti calcoli, allora il render dello schermo tarderà ad avvenire e questo porterà l’utente a vedere un blocco delle immagini.
Una soluzione all’inconveniente è quello di spezzare il codice che contiene calcoli onerosi, come può essere un ciclo molto lungo. Spezzare il codice nelcaso di un loop molto lungo significa “freezzare” lo stato del “loop” per poi riprenderlo in un secondo momento e ripartire dallo stato freezzato. Per esempio il loop seguente:

// pseudo code
var i, n = array.length;
for (i=0; i < n; i++){
	process(array[i]);
}

può essere convertito in :

// pseudo code
var savedIndex = 0;
var i, n = array.length;
for (i=savedIndex; i<n; i++){

	if (needToExit()){
		savedIndex = i;
		break;
	}

	process(array[i]);
}

dove “savedIndex” memorizza lo stato e “needToExit()” decide quante volte eseguire il loop prima di cedere il controllo al render.
L’esecuzione di ogni pezzo di codice sarà gestito in un Enter_Frame:

// pseudo code
var savedIndex = 0;
function enterFrame() {

	var i, n = array.length;
	for (i=savedIndex; i<n; i++){

		if (needToExit()){
			savedIndex = i;
			return;
		}

		process(array[i]);
	}

	complete();
}

Mentre se vogliamo gestire il freeze del loop con un Timer possiamo adottare la soluzione successiva:

//pseudo code
var allowedTime = 1000/fps - renderTime - otherScriptsTime;
var startTime = 0;
var savedIndex = 0;
function enterFrame() {
	startTime = getTimer();
	
	var i, n = array.length;
	for (i=savedIndex; i < n; i++){

		if (getTimer() - startTime > allowedTime){
			savedIndex = i;
			return;
		}

		process(array[i]);
	}

	complete();
}

Nel caso di loop annidati:

// pseudo code
for (i=savedIndexI; i<n; j++){

	for (j=savedIndexJ; j<m; j++){

		process(array[i][j]);
	}
		
	savedIndexJ = 0;
}

Nel caso di loop “for..in” e “for..each” bisogna necessariamente passare a loop con indice come quelli precedenti, quindi bisogna ricostruire l’array:

// pseudo code
var indexedList = [];
for each(value in object){
	indexedList.push(value);
}

Un classico esempio è quello di far vedere un progress bar animato mentre ci sono dei calcoli da portare avanti.

This movie requires Flash Player 9


L’esempio prevede di inserire del testo nel campo di input che sarà convertito in un codice premendo il bottone “Cipher”.
Il codice, che si trova qui va usato come:

var text:String = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
var cipher:CaesarCipher = new CaesarCipher(text, 3);
cipher.addEventListener(Event.COMPLETE, complete);
cipher.run();

function complete(event:Event):void {
	trace("Result: " + cipher.result);
	// Result: Oruhp lsvxp groru vlw dphw, frqvhfwhwxu dglslvflqj holw.
}

Non sempre la complessità dei calcolo deriva dai loop, quindi se abbiamo un codice molto complesso siamo costretti a dividerlo in pezzi più piccoli e inserirlo in nuovi metodi, per esempio processA, processB e processC:

// pseudo code
function processA(){
	// time consuming process 1
}
function processB(){
	// time consuming process 2
}
function processC(){
	// time consuming process 3
}

var sequence:Array = [processA, processB, processC];
// Event.ENTER_FRAME loop attraverso un array ...

Un’alternativa è quella di assegnare la sequenza dell’esecuzione dei processi non ad un array, ma ai processi stessi, in questo modo non è necessario una gestione del tempo per eseguire ogni processo.
Sicuramente questa gestione è più difficile delle precedenti, ma creare molti piccoli processi potrebbe permettere di eseguire più processi contemporaneamente:

// pseudo code
function processA(){
	// time consuming process
	next = processB;
}
function processB(){
	// time consuming process
	next = processC;
}
function processC(){
	// time consuming process
	enterFrameDispatcher.removeEventListener(Event.ENTER_FRAME, handlerEnterFrame, false);
        dispatchEvent(new Event(Event.COMPLETE));
}

var next = processA;
// Event.ENTER_FRAME call next while non-null ...

Ovviamente dentro il metodo che gestisce l’evento ENTER_FRAME bisogna eseguire il processo contenuto nell’istanza “next”.

Qualche volta necessiti creare una operazione che non è limitata dalla velocità del processo, ma da un’altra operazione asincrona. Se un’operazione dipende dal risultato di un’altra operazione asinocrona allora questa operazione è asincrona. Qualunque operazione di rete, per esempio, che carica dati da un server è necessariamente un’operazione asincrona.
Un’operazione asincrona viene uniformata ad un evento infatti come esso può diventare un’operazione completa o fallire.
La difficoltà inizia quando hai una serie di operazioni che possono o non possono essere sincrone. Quando più operazioni sono in esecuzione in serie, nel gestore dell’evento "complete", un’altra operazione può partire.Quando questa operazione è completata, lo stesso gestore verrebbe usato per ripetere il processo attraverso ogni operazione della serie. Se tutte queste operazioni finiscono col diventare sincrone, tu stai creando un loop ricorsivo all’interno del gestore dell’evento. Con sufficienti operazioni in una serie, questo potrebbe creare uno stack in overflow, con molte funzioni nello stack. Veramente le operazioni asincrone non soffrirebbero di ciò perché il loro completamento, che è stato originato all’interno di una chiamat,a è separato dalla serie di loop.
La soluzione a questo problema, semplicemente, è evitare la ricorsione. Questo può significare aspettare un frame (o usare qualche altro evento) per "restartartare" la successiva serie di operazioni. Con sufficienti operazioni in una serie, questo potrebbe consumare del tempo comparato ad un singolo frame per un lungo insieme di variazioni sincrone.
Un approccio alternativo potrebbe essere usare un loop per gestire la ricorsione. Un semplice flag in un gestore dell’evento "complete" può essere usato per identificare il risultato di un’operazione asincrona che può essere usato in una serie principale di loop, per sapere se o meno il loop può continuare in maniera sincrona. Se asincrono, lo stesso flag può essere usato all’interno del gestore che sa se dover ristartare la serie nella sua nuova chiamata.

// pseudo code
var savedIndex = 0;
var resume = false;
function loop(){
	var i, n = array.length;
	for (i=savedIndex; i<n; i++){
		
		resume = false;
		process(array[i]);
		
		if (!resume){
			resume = true;
			savedIndex = i + 1;
			return;
		}
	}
}
function processComplete(){
	if (resume){
		loop();
	}else{
		resume = true;
	}
}

La variabile "resume" indica, quando si è verificato l’evento "complete", se sia necessario o meno chiamare la funzione "loop", permettendo al ciclo di continuare in maniera sincrona. Il loop controlla la variabile resume per verificare che l’evento si sia verificato, in modo da uscire dal ciclo ed aspettare fino a che l’evento accada.
Il gestore della varibile resume potrebbe anche essere stato gestito da un return dal processo, indicando se o meno l’operazione era sincrona. Ma questo significherebbe che l’operazione era disegnata con quel comportamento. Il precedente esempio non è prettamente asincrono, ma potrebbe essere sincrono se le operazioni non porteranno mai a ritardo e quindi calcolate in un altro frame.

Parliamo della tecnica CallLater
Possiamo usare il metodo UIComponent.callLater() per lanciare un loop:

public function doLongWork( arg1 : String, i : int, total : int ) : void {
// do work
if( i < total ) {
uicomponent.callLater( doLongWork, [ arg1, i + 1, total ] );
}
}

In questa tecnica sostituiamo il loop con una chiamata ricorsiva, ma effettivamente ciò che facciamo è una iterazione del loop e scheduliamo l’evento nella coda delle chiamate in modo da essere ripreso nell’iterazione successiva. Faremo questo fino a che i==total infatti dopo di ciò stoppiamo le chiamate.

Consideriamo ora l’evento "enter_frame":

public function start() : void {
i = 0; total = 1000000;
Application.application.addEventListener( Event.FRAME_ENTER, doLongWork );
}

public function doLongWork( event : Event ) : void {
// do some work
i++;
if( i >= total ) {
Application.application.removeEventListener( Event.FRAME_ENTER, doLongWork );
}
}

La cosa da notare è che a livello di performace questa tecnica equivale a quella CallLater ed entrambe spendono più tempo per aspettare il successivo frame piuttosto di fare i calcoli delle nostre operazioni.

Con un getTime possiamo realizzare più calcoli prima di passare al prossimo frame in modo da eliminare molti tempi morti. Soluzione già adottata all’inizio dell’articolo, ma ora riformulata per dare una struttura riutilizzabile con maggiore organizzazione.

public function start() : void {
Application.application.addEventListener( Event.FRAME_ENTER, onCycle );
}

private function onCycle( event : Event ) : void {
var cycle : Boolean = true;
var start : Number = getTimer();
var milliseconds = 1000 / Application.application.stage.frameRate - DELTA;
while( cycle && (getTimer() - start) < milliseconds ) {
cycle = doLongWork();
}

if( cycle == false ) {
Application.application.removeEventListener( Event.FRAME_ENTER, doLongWork );
}
}

public function doLongWork() : Boolean {
// do some work
i++;
return i < total;
}

Il metodo start è quello che inizia il processo, il metoro onCycle calcola quanto tempo impiega un frame al secondo. Il ciclo continua fino quando il metodo doLongWork ritorna "false" oppure terminaniamo il tempo assegnato al calcolo. Notare che la costante DELTA tiene il tempo dell’intero frame. Necessitiamo dare un po’ di respiro a Flash per svuotare la coda. Il metodo doLongWork è il codice pertinenete a fare il nostro calcolo. Questa parte di codice è facilemte riutilizzabile.

Per approfondire l’argomento vai qui.

Scrivi un Commento

Home | Graffiti e Disegni | Educazione | Chi siamo | Blog | Progetti | Contatti
RSS Feed Comments RSS Accedi