2012-01-24 10 views
7

Ho riscontrato problemi quando i controlli che allocavano grandi quantità di memoria venivano distrutti e ricostruiti utilizzando gli eventi Input dall'interfaccia utente.Come può una memoria libera utilizzata da pesanti controlli WPF in modo deterministico?

Ovviamente, l'impostazione di un contenuto della finestra su null non è sufficiente per liberare la memoria utilizzata dal controllo contenuto. Alla fine sarà GCed. Ma come gli eventi di input per distruggere e costruire i controlli diventano più frequenti, il GC sembra saltare alcuni oggetti.

ho rotto giù a questo esempio:

XAML:

<Window x:Class="WpfApplication1.MainWindow" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Title="MainWindow" Height="350" Width="525" x:Name="window" MouseMove="window_MouseMove"> 
</Window> 

C#:

using System; 
using System.Windows; 
using System.Windows.Controls; 
using System.Windows.Input; 

namespace WpfApplication1 
{ 
    public partial class MainWindow : Window 
    { 
     private class HeavyObject : UserControl 
     { 
      private byte[] x = new byte[750000000]; 

      public HeavyObject() 
      { 
       for (int i = 0; i++ < 99999999;) { } // just so we can follow visually in Process Explorer 
       Content = "Peekaboo!"; // change Content to el cheapo re-trigger MouseMove 
      } 
     } 

     public MainWindow() 
     { 
      InitializeComponent(); 

      //Works 1: 
      //while (true) 
      //{ 
      // window.Content = null; 
      // window.Content = new HeavyObject(); 
      //} 
     } 

     private void window_MouseMove(object sender, MouseEventArgs e) 
     { 
      if (window.Content == null) 
      { 
       GC.Collect(); 
       GC.WaitForPendingFinalizers(); 
       // Works 2: 
       //new HeavyObject(); 
       //window.Content = "Peekaboo!"; // change Content to el cheapo re-trigger MouseMove 
       //return; 
       window.Content = new HeavyObject(); 
      } 
      else 
      { 
       window.Content = null; 
      } 
     } 
    } 
} 

Questo fa allocare un oggetto di 750 MB ~ (HeavyObject) ad ogni evento MouseMove e mettere è il contenuto della finestra principale. Se il Cursore viene tenuto fermo sulla finestra, la modifica del contenuto riattiva all'infinito l'evento. Il riferimento a HeavyObject viene annullato prima della sua successiva costruzione e viene attivato un inutile tentativo di GC sui suoi resti.

Quello che possiamo vedere in ProcessExplorer/Taskman è che a volte ci sono almeno 1,5 GB allocati. In caso contrario, muovi il mouse selvaggiamente e lascia e rientra nella finestra. Se compili per x86 senza LARGEADDRESSAWARE, riceverai invece una OutOfMemoryException (limite ~ 1,3 GB).

Non si verificherà questo comportamento se si alloca l'oggetto HeavyObject in un ciclo infinito senza utilizzare l'input del mouse (sezione uncomment "Works 1") o se si attiva l'allocazione mediante l'input del mouse ma non si inserisce l'oggetto nel albero visivo (uncomment seciton "Works 2").

Quindi suppongo che abbia qualcosa a che fare con l'albero visuale liberando pigramente le risorse. Ma poi c'è un altro strano effetto: se si consumano 1,5 GB durante il cursore fuori dalla finestra, sembra che il GC non esegua il kick in finché MouseMove non viene attivato di nuovo. Almeno, il consumo di memoria sembra stabilizzarsi finché non vengono attivati ​​ulteriori eventi. Quindi o c'è un riferimento longevo lasciato da qualche parte o GC diventa pigro quando non c'è attività.

Sembra abbastanza strano per me. Riesci a capire cosa sta succedendo?

Edit: Come ha commentato BalamBalam: I dati dietro il controllo potrebbe essere dereferenziati prima di distruggere il controllo. Quello sarebbe stato il piano B. Tuttavia forse c'è una soluzione più generica.

Dire che tali controlli sono codici errati non è utile. Mi piacerebbe sapere perché un oggetto non tenga alcun riferimento a GCed se lascio l'interfaccia utente da solo per alcune zecche dopo averlo dereferenziato, ma indugia per sempre se un altro (che deve essere stato creato dall'input dell'utente) prende immediatamente il suo posto.

+2

Non riesco a capire quale sia il tuo problema ... presumendo che il codice pazzo che hai qui sia solo per test. Ma hai imparato una cosa: i tentativi di forzare GC spesso non fanno quello che pensi che faranno. –

+0

Il codice è un esempio, contenente due soluzioni alternative in cui il problema non si verificherà. Giusto per limitare le presunzioni. Il problema è: avere un grande controllo come contenuto della finestra -> impostare il nuovo grande controllo come contenuto della finestra ** con l'input del mouse ** -> più di un grande controllo vivo in una sola volta. –

+0

Beh, spero solo che nessuno possa allocare un oggetto da 750 MB su una mossa del mouse. Questo è uno scenario molto irrealistico, e quindi tutto il tuo fondamento qui è shakey. –

risposta

7

Ok, penso che potrei sapere che cosa sta succedendo qui. Prendilo con un pizzico di sale però ...

Ho modificato leggermente il tuo codice.

Xaml

<Window x:Class="WpfApplication1.MainWindow" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     Title="MainWindow" Height="350" Width="525" x:Name="window" MouseDown="window_MouseDown"> 
</Window> 

Codice Dietro

using System; 
using System.Windows; 
using System.Windows.Controls; 
using System.Windows.Input; 

namespace WpfApplication1 
{ 
    public partial class MainWindow : Window 
    { 
     private class HeavyObject : UserControl 
     { 
      private byte[] x = new byte[750000000]; 

      public HeavyObject() 
      { 
       for (int i = 0; i < 750000000; i++) 
       {      
        unchecked 
        { 
         x[i] = (byte)i;  
        } 
       } 

       // Release optimisation will not compile the array 
       // unless you initialize and use it. 
       Content = "Loadss a memory!! " + x[1] + x[1000]; 
      } 
     } 

     const string msg = " ... nulled the HeavyObject ..."; 

     public MainWindow() 
     { 
      InitializeComponent(); 
      window.Content = msg; 
     } 

     private void window_MouseDown(object sender, MouseEventArgs e) 
     { 
      if (window.Content.Equals(msg)) 
      { 
       window.Content = new HeavyObject(); 
      } 
      else 
      { 
       window.Content = msg; 
      } 
     } 
    } 
} 

Ora eseguire questo su e cliccare sulla finestra. Ho modificato il codice in non utilizzare GC.Collect() e in impostare la memoria sul mouse. Sto compilazione in modalità di rilascio e funzionante senza un debugger allegato.

Fare clic lentamente sulla finestra.

Il primo clic si vede l'utilizzo della memoria salire da 750MBytes. Clic successivi alternano il messaggio tra .. Nulled the heavy object.. e Loads a memory!finché si fa clic lentamente. C'è un leggero ritardo perché la memoria è allocata e deallocata. L'utilizzo della memoria non dovrebbe superare i 750MByte ma non diminuisce anche quando l'oggetto pesante viene annullato. Questo è designato come GC Large Object Heap is lazy and collects large object memory when new large object memory is needed. Da MSDN:

Se non si dispone di spazio sufficiente per soddisfare le richieste di allocazione di oggetti di grandi dimensioni, tenterò innanzitutto di acquisire più segmenti dal sistema operativo. Se ciò fallisce, attiverò una garbage collection di generazione 2 nella speranza di liberare spazio.

Clicca veloce sulla finestra

Circa 20 scatti. Che succede? Sul mio PC l'utilizzo della memoria non aumenta, ma la finestra principale ora non aggiorna più il messaggio. Si congela. Non ottengo un'eccezione di memoria insufficiente e l'utilizzo complessivo della memoria del mio sistema rimane piatto.

stress il sistema utilizzando MouseMove nella finestra

oggi sostituiscono i MouseDown per un evento MouseMove (secondo il vostro codice di domanda) cambiando questo Xaml:

<Window x:Class="WpfApplication1.MainWindow" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     Title="MainWindow" Height="350" Width="525" x:Name="window" MouseMove="window_MouseDown"> 
</Window> 

posso muovere il mouse su di esso per un po 'ma alla fine getta un'eccezione OutofMemory. Un buon modo per forzare l'eccezione è fare più mosse quindi provare a massimizzare la finestra.

La teoria

Ok quindi questo è la mia teoria. Poiché il thread dell'interfaccia utente è in esecuzione senza problemi durante la creazione e l'eliminazione della memoria all'interno di un evento del loop di messaggi (MouseMove), Garbage Collector non è in grado di tenere il passo. La raccolta dei dati inutili sull'heap di oggetti di grandi dimensioni viene eseguita when memory is needed. È anche un compito costoso eseguito in modo non frequente.

Il ritardo che abbiamo notato quando il clic veloce on/off/on/off diventa molto più pronunciato quando eseguito in un MouseMove. Se un altro oggetto grande deve essere allocato, in base a tale articolo MSDN, il GC tenterà di raccogliere (se la memoria non è disponibile) o inizierà a utilizzare il disco.

Sistema in memoria insufficiente Situazione: questo accade quando si riceve la notifica di memoria alta dal sistema operativo. Se penso che fare un GC di seconda generazione sarà produttivo, ne scatenerò uno.

Ora questa parte è interessante e sto indovinando qui, ma

Infine, a partire da questo momento, il LOH non viene compattata come parte della collezione, ma questo è un dettaglio di implementazione che dovrebbe non essere invocato. Quindi per assicurarti che qualcosa non venga spostato dal GC, fallo sempre col pin. Ora prendi la tua nuova conoscenza LOH e vai a prendere il controllo del mucchio.

Quindi, se il Large Object Heap non è compattato, e richiedere più oggetti di grandi dimensioni in un loop stretto, è possibile che le collezioni portano alla frammentazione causando alla fine un OutOfMemoryException anche se ci dovrebbe teoricamente essere sufficiente memoria?

La soluzione

Let liberare l'interfaccia utente filo un po 'per consentire la stanza GC di respirare. Avvolgere il codice di allocazione in una chiamata Dispatcher asincrona e utilizzare una priorità bassa, ad esempio SystemIdle. In questo modo sarà solo allocare la memoria quando il thread dell'interfaccia utente è libero.

private void window_MouseDown(object sender, MouseEventArgs e) 
    { 
     Dispatcher.BeginInvoke((ThreadStart)delegate() 
      { 
       if (window.Content.Equals(msg)) 
       { 
        window.Content = new HeavyObject(); 
       } 
       else 
       { 
        window.Content = msg; 
       } 
      }, DispatcherPriority.ApplicationIdle); 
    } 

Usando questo posso ballare il puntatore del mouse in tutto il modulo e non è mai getta un OutOfMemoryException. Che funzioni davvero o abbia "risolto" il problema che non conosco, ma vale la pena testarlo.

In conclusione

  • Nota che gli oggetti di queste dimensioni stanno per essere allocato sul Large Object Heap
  • allocazioni LOH innescare un GC, se non è disponibile spazio sufficiente
  • Loh è non compattato durante un ciclo GC, quindi potrebbe verificarsi una frammentazione, che porta a OutOfMemoryException nonostante teoricamente ci sia abbastanza memoria
  • Idealmente riutilizzare oggetti di grandi dimensioni e non riallocarli. Se ciò non è possibile:
    • Allcate tuoi oggetti di grandi dimensioni in un thread in background per disaccoppiare dal thread dell'interfaccia utente
    • Per consentire la stanza GC a respirare, usa il Dispatcher di disaccoppiare l'assegnazione fino a quando l'applicazione è inattiva
+0

Grazie per questa elaborazione. Ho riprodotto i tuoi test. I "20 clic" = blocco non si sono verificati sul mio computer. la finestra è rimasta invariata fino a quando tutti i clic sono stati gestiti (ha richiesto un po 'di tempo) e quindi è tornato ad aggiornare normalmente. Forse non hai aspettato abbastanza? Il comportamento della memoria è lo stesso qui. Massimizzare la finestra sotto stress sembra fare esattamente lo stesso di uscire e rientrare nella finestra con il cursore del mouse (come nella domanda originale). Forse gli eventi leave and enter assegnano memoria in modo che il prossimo HeavyObject non si adatti più allo spazio del vecchio? –

+1

@die_hoernse per forzare il software a rallentarlo, è possibile utilizzare il Dispatcher con priorità bassa per disaccoppiare l'evento Click dalla funzione DoBigMemoryOperation e impedire la riattivazione finché non viene restituito BigMemoryOperation. Effettuare il tuo BigMemoryOperation in un thread in background consente anche di segnalare i progressi e ingrigire l'interfaccia utente/disabilitare i pulsanti mentre si lavora: trucchi del genere possono davvero aiutare a prevenire arresti di produzione quando si sollecita GC –

+0

In modalità "stress", quando c'è due volte (o più - reso 250 invece di 750 MB) la memoria di HeavyObject allocata e il cursore si trova all'esterno della finestra, rimane allocata fino al rientro del cursore. La mia teoria è che GC è inattivo a causa dell'inattività del programma e della pressione della memoria del sistema. Dato che non ci sono perdite ma il LOH alla fine sarà compattato, considero la tua ultima conclusione la soluzione: mantenerla in un thread in background. Tuttavia, se dovessi scoprire che la memoria rimane allocata per sempre, anche sotto pressione di memoria senza I/O menzionabili o carico della CPU, questa domanda verrà aggiornata. –

Problemi correlati