posts - 562, comments - 2085, trackbacks - 203

My Links

News

Raffaele Rialdi website

Su questo sito si trovano i miei articoli, esempi, snippet, tools, etc.

Archives

Post Categories

Image Galleries

Blogs

Links

WPF, elaborazioni lunghe e presunti memory leak

Quando una elaborazione “pesante” viene eseguita in un contesto di una interfaccia utente, sia essa Winform, MFC o WPF, è necessario che questa venga “alimentata” servendo il thread sottostante:

- Windows Forms ed MFC, come anche altre librerie basate sulle Win32, hanno bisogno di alimentare la message pump. Si tratta di una coda associata ad uno specifico thread che accumula messaggi e li smista verso i controlli contenuti.

- WPF non ha message pump perché ha un meccanismo di rendering completamente differente. Nonostante questo esiste un meccanismo analogo alla message pump, basasto sul Dispatcher di WPF.

Fin qui tutto chiaro. Il developer attento conosce questi aspetti ma ci sono almeno due casi in cui queste precauzioni vengono tipicamente ignorate:

  1. Quando l’elaborazione viene eseguita in un thread differente, il developer è “tranquillo” di lasciare libero il dispatcher del thread principale e perciò di eseguire tutto nel rispetto delle regole
  2. Quando non esiste neppure una finestra di presentazione perché WPF viene usato per eseguire delle elaborazioni non visuali: creazione di XPS, rendering per la creazione di bitmap, etc.
    Questi casi sono tipici per elaborazioni dentro servizi WCF o applicazioni asp.net

Da un punto di vista di elaborazione grafica tutto questo non fa una grinza eppure c’è un problema.

Tutto nasce da un test per verificare una procedura complessa per la creazione di documenti XPS. Ad ogni loop viene creato un documento e salvato su file. Una volta eseguito il giro, tutti gli oggetti grafici che sono serviti per la creazione del documento vengono buttati via. Eppure la memoria (working set) continua a crescere ad ogni giro fino a dare una OutOfMemoryException.

Il primo pensiero va giustamente ad un classico memory leak e qui parte un intreccio di code review e misurazioni sull’uso della memoria eseguite con il profiler di Visual Studio. Quando si incontra un managed memory leak i maggiori sospetti sono tipicamente per:

  • Collection/hashtable/oggetti statici che accumulano un grosso numero di children senza rimuoverli
  • Oggetti singleton che fungono da “root” per interi tree di oggetti
  • Allocazioni native non rilasciate

Eppure non era nulla di tutto questo. Inoltre la traccia del profiler è decisamente poco generosa di informazioni utili. Si vede un esagerato numero di oggetti WPF referenziati internamente nelle classi di WPF e nulla di riconducibile al codice utente.

Il secondo tentativo è quello di forzare ad ogni ciclo una garbage collection con il classico pattern:

GC.Collect();
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Disclaimer: questo è solo codice per eseguire dei test e non dovrebbe mai essere usato in produzione.

I primi due Collect sono necessary per garantire che siano soggetti alla collection anche gli oggetti che hanno un finalizzatore. La WaitForPendingFinalizers serve ad attendere che la coda di finalizzazione sia vuota. L’ultima Collect garantisce che anche gli oggetti ormai finalizzati siano soggetti a collection.

Anche questo test non dà alcun esito, cioè la memoria non scende. A questo punto è sostanzialmente certa la presenza di un memory leak, cioè la presenza di un reference “da qualche parte” che non permetta al garbage collector di liberare la memoria.

Limitando il numero di cicli e forzando il garbage collector dopo un paio di secondi (tramite un pulsante) la memoria veniva liberata. Ma che razza di memory leak è questo?

E arriviamo al terzo tentativo. Mi dico: “come fa WPF a gestire oggetti grafici (i.e. risorse unmanaged di directx) senza aver mai bisogno di Dispose?”. Mi rispondo: “dovessi farlo io, implementerei un gestore di oggetti che li libera non appena non vengono più usati”.
Visto che l’apartment dei thread di WPF deve essere di tipo STA (Single Threading Apartment), se non permetto al dispatcher (che suppongo sia basato su una message pump di un oggetto COM di tipo STA) di svuotare la coda dei suoi messaggi, questo non potrà eseguire il metodo che presuppongo sia preposto al rilascio delle risorse e a ruota dei reference agli oggetti.

Per verificare la mia ipotesi, ad ogni ciclo faccio eseguire:

   1: Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new ThreadStart(delegate { }));

La priorità di tipo Background è fondamentale perché, come dice MSDN, il delegate vuoto verrà eseguito ... “Operations are processed after all other non-idle operations are completed”. In pratica il codice del delegate “{}” è una scusa per assicurarsi che il dispatcher svuoti prima la coda dei messaggi.

Bingo, funziona! Adesso ad ogni ciclo la memoria viene liberata senza alcuna necessità di invocare manualmente la collection.

Considerazioni finali.

1. Le motivazioni appena descritte sono solo una mia deduzione. Non ho trovato alcuna documentazione in merito sul funzionamento interno di WPF per quello che riguarda la disallocazione di oggetti e risorse.

2. Anche eseguendo il ciclo in un thread separato il problema si verifica comunque.
Il thread secondario deve essere aperto con apartment STA e come tale necessita comunque di alimentare il dispatcher in quel thread.

3. Se è effettivamente come ho descritto, si spiega il fatto che le chiamate alla GC.Collect non abbiano effetto.
Quello che posso dedurre è che WPF si comporti come un runtime che, in modo analogo a quanto viene fatto dal garbage collector, si accorge quando un reference non viene più usato. A quel punto, alla passata successiva, il dispatcher può rilasciare le risorse unmanaged sottostanti e finalmente rilasciare l’oggetto al GC che eseguirà il rilascio reale della memoria occupata.
Perciò fin tanto che WPF non ha processato questi oggetti, anche se il codice utente non ha più reference attivi, è WPF ad averli perché questi oggetti devono ancora essere soggetti al rilascio di risorse interno di WPF.
Ripeto: sono solo mie deduzioni e non ho alcuna conferma a sostegno del mio ragionamento, se non i test che ho eseguito.

Morale.

Il garbage collector funziona, e funziona molto bene.

WPF fa il suo dovere e se il funzionamento è quello che ho intuito osservando questo test, i vantaggi sono superiori al problema che ho incontrato. L’assenza di Dispose in tutto WPF è un vantaggio non da poco e da qualche parte le risorse unmanaged ci devono essere per forza visto che si tratta di oggetti grafici.

Perciò come al solito prima di pensare a malfunzionamenti e bug bisogna cercare di capire come funziona e mettersi nei panni di chi ha dovuto risolvere il problema del rilascio delle risorse.

Print | posted on venerdì 20 febbraio 2009 22.15 |

Feedback

Gravatar

# re: WPF, elaborazioni lunghe e presunti memory leak

Ciao Raffaele,
Grazie anzitutto per questo post illuminante: mi è capitato in alcune mie prove di scontrarmi con un problema simile, senza andare mai oltre il tuo "secondo tentativo". Non riuscivo veramente a capire per quale oscuro motivo non si liberasse memoria forzando la garbage collection. Ormai ci avevo rinunciato :P

Lette le tue considerazioni finali però mi sorgono delle preoccupazioni: se WPF si comporta da runtime come dici, non credi possano sorgere grossi rompicapi nel porting di applicazioni ad es. Windows Forms, che spesso e volentieri devono minimizzare lo spreco di memoria?

Inoltre,il fatto che una tale issue sia assolutamente sconosciuta ai più ( magari perché mal/non documentata ) non rischia di generare confusione e problemi soprattutto per la marea di sviluppatori abituata a non avere un "runtime sopra il runtime" per quanto riguarda la gestione delle risorse?

Un saluto

Dario
21/02/2009 12.08 | Dario Santarelli
Gravatar

# re: WPF, elaborazioni lunghe e presunti memory leak

SEI UN MITO. (e il maiuscolo è voluto, perchè lo urlo al mondo!). Trobbo bello questo tuo post, sei meglio di un microscopio elettronico.
22/02/2009 10.11 | Nicolò Cararandini
Gravatar

# re: WPF, elaborazioni lunghe e presunti memory leak

@Dario. Ciao e grazie a te per il commento. Io credo innanzitutto che migrare una app Winform in WPF senza ripensarla sia un grave errore. Detto questo, si tratti di migrazione o meno il problema può saltare fuori in modo viscido e molto pericoloso, ed è per questo che l'ho subito bloggato anche senza avere documentazione più solida sotto mano.
Adesso che WPF sta maturando spero che si cominci ad avere un po' più di informazioni sugli "inner details" così da poter capire meglio questi preziosi dettagli.
Più che open source vorrei tanto open documentation! E ripeto che i sorgenti hanno valore z e r o in confronto ad una buona documentazione.

@Nicolò. Grazie ma nessun mito, mi infilo in questi garbugli solo per tenere allenati i neuroni ;-)
23/02/2009 0.58 | Raffaele Rialdi
Gravatar

# re: WPF, elaborazioni lunghe e presunti memory leak

Complimenti Raffaele, ho aggiunto il post tra i preferiti, per quando mi si presenterà il problema.
adesso devo rileggerlo ancora una volta per capire tutto pienamento

ciao marco
23/02/2009 10.52 | Nostromo
Gravatar

# re: WPF, elaborazioni lunghe e presunti memory leak

Ciao Raffaele,

cosa intendi quando dici:

Per verificare la mia ipotesi, ad ogni ciclo faccio eseguire:

1: Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new ThreadStart(delegate { }));


Quando veve essere eseguita questa istruzione e cosa intendi per ciclo?

Grazie in anticipo

Andrea
26/05/2009 9.14 | Andrea
Gravatar

# re: WPF, elaborazioni lunghe e presunti memory leak

Ciao Andrea,
in pratica ho un loop dove ad ogni giro viene eseguita una elaborazione lunga su WPF.
Non essendoci interattività con la UI (è una elaborazione di conversione, quindi tipica da lato 'server') il dispatcher di WPF non ha mai 'respiro'.
In questo loop (ciclo) senza sosta ho inserito quella riga di codice per far si che al Dispatcher venga dato il tempo necessario per svolgere le sue cose interne (cioè disallocare e rilasciare risorse).
È più chiaro?
26/05/2009 18.27 | Raffaele Rialdi
Gravatar

# re: WPF, elaborazioni lunghe e presunti memory leak

Ciao Raffaele, non riesco a tradurre il tuo codice
Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new ThreadStart(delegate { }));
in vb.net.
Non comprendo l'argomento di ThreadStart.

Puoi aiutarmi?
Grazie
15/06/2009 18.22 | luciano.net
Gravatar

# re: WPF, elaborazioni lunghe e presunti memory leak

Ciao Luciano,
l'argomento di ThreadStart è un anonymous delegate. In pratica gli passi un delegate ad una funzione vuota. È un 'trucco' per far si che il thread del dispatcher sia servito.
Per vb.net puoi definire un metodo vuoto:
new ThreadStart(AddressOf Fake))
Dove Fake è ovviamente una sub vuota:
Sub Fake()
End Sub
06/07/2009 18.01 | Raffaele Rialdi

Post Comment

Title  
Name  
Email
Url
Comment   
Please add 6 and 8 and type the answer here:

Powered by: