Gestione della Memoria nella .NET Compact Framework e in Windows Mobile (Parte 2 – Troubleshooting di Memory Leak)

Come fatto nel post precedente, mi piacerebbe approcciare il problema in un modo diverso dal semplicemente spiegare come funzionano le cose, lasciando questo compito alla documentazione e ai vari blog – uno fra tutti quello di Abhinaba Basu, che all’interno del Dev Team della NETCF si occupa esattamente del Garbage Collector: Back to basic: Series on dynamic memory management. L’approccio che mi piacerebbe seguire è quello del troubleshooting a posteriori, in cui mi ritrovo ogni volta che si presenta una richiesta di Supporto Tecnico relativa ad esempio a:

  • OutOfMemoryException
  • SqlCeException: “Not enough memory to complete this operation”
  • L’applicazione non riesce a caricare una DLL
  • Altri malfunzionamenti che facciano pensare ad un problema di memoria

Anzitutto, bisogna verificare se il problema sia specifico ad un OEM. L’approccio migliore, quando possibile, è verificare se l’errore si presenta anche sugli emulatori contenuti nei diversi Windows Mobile SDK. Se così non è, l’aiuto che il Supporto Tecnico Microsoft può dare è limitato, poichè è possibile che l’errore sia dovuto ad una personalizzazione della piattaforma Windows Mobile da parte dell’OEM (per dettagli a proposito di piattaforme e OEM vi rimando a questo post). In questo caso, può tornare utile il discorso relativo ai driver che ho fatto in quest’altro post, ed eventualmente chiedere al particolare OEM se esista il modo di disabilitarne qualcuno programmaticamente.

Un altro passo iniziale, nel caso di applicazioni NETCF v2 SP2, è verificare se semplicemente far girare l’applicazione sulla NETCF v3.5 migliori le cose. Non è necessario ricompilare l’applicazione con il Visual Studio 2008 – è sufficiente, esattamente come per le applicazioni .NET Desktop, inserire un file XML di configurazione nella stessa cartella che contiene il file Applicazione.exe, nominato Applicazione.exe.config ed il cui contenuto sia semplicemente (ne ho parlato qui):

 <configuration>
  <startup>
    <supportedRuntime version="v3.5.7283"/>
  </startup>
</configuration>

Eliminate le cause “banali”, si può procedere all’analisi… Thinking Storicamente gli sviluppatori NETCF non hanno avuto vita facile nel troubleshooting, a causa della mancanza di tool adeguati – a differenza dei cugini di Desktop! – ma nel corso degli anni Microsoft ha rilasciato strumenti che si sono via via evoluti negli attuali Power Toys for .NET Compact Framework 3.5. A questi vanno sicuramente aggiunti quelli (freeware!) della EQATEC (Tracer e Profiler) e recentemente un tool su CodeProject che ho già menzionato che visualizza lo stato della memoria virtuale (VirtualMemory, con source code).

Sono 2 i Power Toys che aiutano quando ci si trova alle prese con un problema di memoria: il “CLR Profiler” ed il “Remote Performance Monitor” (RPM). Il primo è utile nel rendere visivamente quasi immediati eventuali problemi nell’allocazione di oggetti e permette di notare il problema in modo visuale. Info su come usarlo sono state descritte su The CLR Profiler for the .Net Compact Framework Series Index. Il secondo fornisce, sia in tempo reale sia analizzandolo a posteriori, contatori legati all’utilizzo della memoria MANAGED; inoltre, attraverso il “GC Heap Viewer” permette non solo di studiare l’esatto contenuto del MANAGED HEAP, ma addirittura permette di confrontare i contenuti dello heap in diversi istanti, in modo da far risaltare una eventuale crescita inaspettata di un certo tipo di oggetti. Alcune immagini sono disponibili su Finding Managed Memory leaks using the .Net CF Remote Performance Monitor, utile anche solo per avere un’idea sui contatori a disposizione, mentre una lista e e relative spiegazioni è fornita su Monitoring Application Performance on the .NET Compact Framework - Table of Contents and Index . Quello che mi piacerebbe fare qui non è riproporre le stesse spiegazioni, già dettagliate nei link suddetti, ma condividere alcune esperienze pratiche

Ad esempio, nella stragrande maggioranza dei casi che ho gestito a proposito di memory leak, il problema era dovuto a Form (o controlli) che inaspettatamente NON venivano rimossi dal Garbage Collector. Le instanze delle classi Form dell’applicazione sono quindi la prima cosa da controllare attraverso il Remote Performance Monitor e il GC Heap Viewer. Per questo motivo, laddove opportuno (ad esempio se le form totali sono relativamente “poche”), per evitare problemi di memoria su applicazioni NETCF è utile il cosiddetto “Singletone Pattern”: in questo modo nel managed heap esisterà una singola instanza di una determinata form durante tutta il ciclo di vita dell’applicazione.

Quindi, supponiamo di trovarci in questa situazione, ovvero: utilizzo il Remote Performance Monitor salvando diversi .GCLOG durante l’utilizzo normale dell’applicazione, e grazie al GC Heap Viewer noto che rimangono in memoria un numero inaspettato di form, e che questi aumenta nel corso della vita dell’applicazione, nonostante vi siano state diverse Garbage Collections. Perchè la memoria di un oggetto Form non è ripulita dal Garbage Collector? Grazie al GC Heap Viewer è possibile sapere esattamente chi mantiene un riferimento a cosa, nella “Root View” a destra. Ovviamente è necessario conoscere l’architettura dell’applicazione, e questo aiuterà nell’individuare link inaspettati.

Tra l’altro, una particolarità relativa alle form in .NET riguarda le form MODALI (le dialog, quelle che su Windows Mobile hanno il pulsante di chiusura “Ok” anzichè la “X” e che permettono di impedire all’utente di tornare alla form precedente). In molti casi che ho gestito, il problema era semplicemente nato dal fatto che il codice non invocava .Close() (o .Dispose()) dopo lo .ShowDialog():

 Form2 f2 = new Form2();
f2.ShowDialog();
f2.Close();

Perchè dovrebbe essere un problema? Perchè spesso (non sempre, ad esempio non nel caso in cui ci si aspetta un DialogResult) su Windows Mobile l’utente clicca sull’ “Ok” in alto a destra “chiudendo” la dialog. Anche su Desktop, quando una dialog viene “chiusa” in questo modo la finestra non è chiusa ma “nascosta”! E potrebbe capitare che il codice crei una nuova instanza della form, senza aver rimosso la precedente dalla memoria. E’ documentato in “Form..::.ShowDialog Method” (in documentazione si parla di “X” ma ovviamente per Windows Mobile fa riferimento all’ “Ok” di cui sopra):

[…] When a form is displayed as a modal dialog box, clicking the Close button (the button with an X at the upper-right corner of the form) causes the form to be hidden and the DialogResult property to be set to DialogResult.Cancel. Unlike modeless forms, the Close method is not called by the .NET Framework when the user clicks the close form button of a dialog box or sets the value of the DialogResult property. Instead the form is hidden and can be shown again without creating a new instance of the dialog box. Because a form displayed as a dialog box is not closed, you must call the Dispose method of the form when the form is no longer needed by your application.

Tra l’altro, abbiamo dato per scontato che il leak sia di memoria MANAGED, ma potrebbe darsi che in realtà a leakare siano le risorse NATIVE che sono utilizzate da un’instanza .NET, che non siano state rilasciate implementando correttamente il cosiddetto “ IDisposable Pattern ”. E qui per NETCF esistono alcune particolarità di cui gli sviluppatori .NET Desktop non devono preoccuparsi, in particolare relativamente ad oggetti di SQL CE (di cui magari parlerò in un prossimo post) e ad oggetti “grafici”, ovvero classi del namespace System.Drawing. Nella NETCF gli oggetti Font, Image, Bitmap, Pen, Brush sono semplici wrapper attorno alle rispettive risorse native, che nei sistemi operativi basati su Windows CE sono gestiti dalla GWES (Graphics, Windowing and Event Subsystem). Che significa? Significa che nel loro .Dispose() vengono effettivamente rilasciate le risorse native, e quindi *è necessario invocare .Dispose() per gli oggetti di tipo Drawing* (o metodi che la invochino indirettamente, come ad esempio il .Clear() della classe ImageList.ImageCollection – che non ha la .Dispose()). Da notare che fra i contatori messi a disposizione dal Remote Performance Monitor, la categoria “Windows.Forms” contiene proprio:

      • Controls Created
      • Brushes Created
      • Pens Created
      • Bitmaps Created
      • Regions Created
      • Fonts Created
      • Graphics Created (FromImage)
      • Graphics Created (CreateGraphics)

N.B. Non mi riferisco solo a oggetti direttamente “nati” come Brush, Pen, etc.: mi riferisco anche ad oggetti nelle cui proprietà siano presenti degli oggetti grafici, come ad esempio una ImageList o una PictureBox (o indirettamente, una ImageList di una ToolBar). Quindi, nel momento in cui viene chiusa una form, bisogna ricordarsi di:

 this.ImageList1.Images.Clear();
this.ToolBar1.ImageList.Images.Clear();
this.PictureBox1.Image.Dispose();
//etc...

Infine, ancora in merito alle form, una semplice tecnica che ho spesso usato per individuare possibili problemi con oggetti non correttamente rilasciati alla chiusura di una form è stata emulare l’interazione dell’utente nell’apertura e chiusura “automatica” della form. Mi riferisco semplicemente ad un codice di test del tipo:

 int N = 1000;
for (int i = 1; i <= N; i++)
{
    if (i % 200 == 0) MessageBox.Show(i.ToString()); //solo per sapere che sta andando
    // e per bloccare l'applicazione nel caso si volessero salvare i contatori (.STAT)
    // e la GC Heap View (.GCLOG)

    frmTest frm = new frmTest(/*...*/);
    frm.Show();
    frm.Close();
    frm.Dispose(); 
}
MessageBox.Show("Over. Done. Finito.");

Dopo aver fatto girare il loop N volte, il Remote Performance Monitor sarà di notevole aiuto per verificare cosa stia andando storto… Nerd

Un’ultima nota prima di concludere. Può darsi che un’applicazione sia talmente complessa da richiedere “molta” memoria. Questo non sarebbe un problema, fintantochè vi sia spazio per le righe “verdi” del mio post precedente. Richiedere “molta” memoria significa però che il Garbage Collector dovrà azionarsi più frequentemente, impattando in generale le performance dell’applicazione (il GC deve prima “bloccare” i thread in uno stato sicuro). Il punto è che nel caso in cui l’applicazione sia talmente complessa da richiedere un uso troppo frequente del Garbage Collector (e di conseguenza performance non accettabili dagli utilizzatori), allora potrebbe valer la pena dividere l’applicazione in 2 parti, ad esempio una di controllo e una di user interface. Questo a costo di un ulteriore process slot, ma spesso si tratta di qualcosa di sacrificabile. Oppure, poichè le DLL managed sono caricate nella Large Memory Area senza sprecare preziosissimo Address Space del processo, un’idea sarebbe quella di inserire tutte le classi, anche quelle delle form, non nell’EXE dell’applicazione ma nelle DLL! Un’idea semplicissima ma molto efficace, di cui Rob Tiffany ha parlato nel suo articolo MemMaker for the .NET Compact Framework

Nel prossimo post concluderò la serie parlando di SQL CE… spero sia d’aiuto!

A presto!
~raffaele

Raffaele Limosani
Senior Support Engineer
Windows Mobile & Embedded Developer Support
My Blog around Mobile Development