WPF DataGrid ItemSource wyciek pamięci

Odpowiedz Nowy wątek
2015-01-21 13:55
PAV
0

Witam,

Mam dosyć poważny wyciek pamięci w aplikacji. Po wielu testach doszedłem do tego, że prawdopodobnie spowodowany jest on przeładowywaniem danych we właściwości ItemsSource w obiekcie DataGrid.

Dla celów testowych uprościłem wszystko, stworzyłem odrębne okno z DataGridem (_grid) oraz Buttonem (_button).

Implementacja readera pobierającego dane z bazy danych Oracle jest bardzo prosta i wygląda tak jak poniżej (fragment klasy DbConnection):

public OracleDataReader ExecuteReader(string query)
        {
             try
            {
                using (OracleCommand sqlCommand = new OracleCommand(query, _sqlConnection))
                {
                    return sqlCommand.ExecuteReader();
                }
            }
            catch (OracleException ex)
            {
                throw ex;
            }
        }

Nie wiem na ile ma to duże znaczenie- ale przez cały czas życia aplikacji połączenie z bazą (obiekt _sqlConnection) jest otwarte

Inicjalizacja przycisku w konstruktorze wygląda następująco:

_button.Click += new RoutedEventHandler(test);

Obsługa kliknięcia wersja I (poniższy kod jeszcze nie powoduje wycieku pamięci):


       private DataTable dataTable;
       private DBConnection _dbconn; //to pole jest inicjowane gdzie indziej, ale obiekt jest dostępny w tym oknie
        private void MemoryLeakTest(object sender, RoutedEventArgs e)
        {
            if(dataTable != null)
            {
                _dataTable.Clear();
            }
            _dataTable = new DataTable();
            _dataTable.BeginLoadData();
            _dataTable.Load(_dbconn.ExecuteReader("SELECT * FROM WORKERS"));
            _dataTable.EndLoadData();
            //_grid.ItemsSource = dataTable.DefaultView;  
        }

Obsługa kliknięcia wersja II (po usunięciu komentarza przypisującego DataView do grida powstaje wyciek pamięci)


        private DataTable dataTable;
         private DBConnection _dbconn;  //to pole jest inicjowane gdzie indziej, ale obiekt jest dostępny w tym oknie
        private void MemoryLeakTest(object sender, RoutedEventArgs e)
        {
            if(dataTable != null)
            {
                _dataTable.Clear();
            }
            _dataTable = new DataTable();
            _dataTable.BeginLoadData();
            _dataTable.Load(_dbconn.ExecuteReader("SELECT * FROM WORKERS"));
            _dataTable.EndLoadData();
            _grid.ItemsSource = dataTable.DefaultView;  
        }

Z każdym kolejnym kliknięciem na przycisk widać wyraźny wzrost pamięci. Wygląda to tak, jakby pole ItemSource nie było nigdy czyszczone z "historycznych" danych...
Dodam jeszcze, że przy wczytywaniu danych pobieram kilkaset wierszy z bazy danych- także raczej nie za wiele. Próbowałem również ręcznie czyścić wiersze przypisane do pola ItemSource ale nie wiele to zmienia.

Czy może ma ktoś pomysł jak pozbyć się tego problemu, i co tak na prawdę jest jego przyczyną?

edytowany 6x, ostatnio: PAV, 2015-01-21 15:00

Pozostało 580 znaków

2015-01-21 14:53

Po pierwsze powinieneś zamykać połączenie po każdym pobraniu danych.
Wyraźny wzrost pamięci czyli jak duży?
private _dataTable dataTable; Co to jest _dataTable? Rozszerzasz jakoś DataTable czy po prostu literówka?
Zrobiłem u siebie test pobierałem 500 rekordów z bazy.
Bez zamykania połączenia klikając w przycisk pobierający dane pamięć wzrastała dość szybko do 35mb później trzeba się było sporo naklikać żeby dobiło do 42 i pewnie mógłbym dalej.
Z zamykaniem połączenia po przekroczeniu 40mb pamięć zaraz spadała do 36.
Nie mam Oracla i testowałem to na SqlServer.

Pozostało 580 znaków

2015-01-21 15:05
PAV
0

Tak, to była literówka (poprawiłem pierwszego posta). Chodziło mi o .NETową klasę DataTable.

Czy istnieje jeszcze jakaś inna przyczyna wycieku pamięci w powyższym przykładzie?

Jeśli to jest jedyna przyczyna, to zastanawiam się czy może moje testowanie po prostu jest nie do końca prawidłowe. Ile czasu należy poczekać, aż pamięć spadnie? I czy jest możliwa taka sytuacja: powiedzmy klikamy 10 razy- pamięć wzrasta wyraźnie, potem za każdym kolejnym kliknięciem zaczyna przyrastać coraz wolniej, aż z czasem stabilizuje się na względnie stałym poziomie i klikanie na przycisk niczego już nie zmienia?

edytowany 2x, ostatnio: PAV, 2015-01-21 15:06

Pozostało 580 znaków

2015-01-21 15:11
0

Co to znaczy "wyraźny wzrost pamięci"? Kilka GB?
Pewno GC nie usuwa niepotrzebnych obiektów tak szybko jak Ty byś chciał.


"HUMAN BEINGS MAKE LIFE SO INTERESTING. DO YOU KNOW, THAT IN A UNIVERSE SO FULL OF WONDERS, THEY HAVE MANAGED TO INVENT BOREDOM."

Pozostało 580 znaków

2015-01-21 18:57
PAV
0

Być może. Może za krótko to obserwuję po prostu. Dlatego to też biorę trochę pod uwagę.

Mówimy o wzroście rzędu 40-50 mb po kilkunastu minutach klikania różnych opcji w aplikacji, które generalnie sprowadzają się do tego co pokazałem wyżej:

  1. pobrania rekordów z bazy
  2. rzucenia ich na DataGrid i odświeżenia jego starej zawartości

Jak jest potem nie wiem- bo wyłączałem program szukając przyczyny szybkiego przyrostu pamięci.

Przypomniałem sobie o jeszcze jednym:
Czy to, że duża część logiki jest wywoływana ze statycznych metod ma jakieś znaczenie? Wiem, że klasy statyczne są jakoś inaczej traktowane przez GC- czy to może powodować wyciek?

EDIT
Czy kolorowanie wierszy w DataGrid (kod piszę z pamięci- chodzi o ogólny sens) może powodować wyciek pamięci?

for(int i = 0; i < _dataView.Rows.Count(); i++) //_dataView to dataTable.DefaultView -> pobrany z bazy danych i przypisany pod pole ItemSource DataGrida
{
   DataGridRow dataGridRow = _grid.ItemContainerGenerator.ContainerFromIndex(i);
   if(dataGridRow != null)
   {
        if(_dataView.Rows[i]["ColumnName"] == "SomeValue")
        {
            dataGridRow.Background = _colorGreen; //gdzie _colorGreen to pole statyczne typu SolidBrush
        }
   }
}

Pozostało 580 znaków

2015-01-21 19:17
msm
0

I czy jest możliwa taka sytuacja: powiedzmy klikamy 10 razy- pamięć wzrasta wyraźnie, potem za każdym kolejnym kliknięciem zaczyna przyrastać coraz wolniej, aż z czasem stabilizuje się na względnie stałym poziomie i klikanie na przycisk niczego już nie zmienia?

Raczej "za każdym kliknięciem przybywa 10kb pamięci, z każdym kliknięciem przybywa 10kb pamięci, po czym po pewnym kliknięciu ubywa 40 MB pamięci i cykl się powtarza".

Prawdopodobnie po prostu na tyle mało pamięci aplikacja alokuje że GC sie nawet nie uruchamia, bo czeka aż będzie co zwalniać.

Popatrz ew. czy nie zapisujesz się gdzieś kilkakrotnie pod event, bo to chyba najczęstsza przyczyna memory leaków w .net.
Poza tym wypada wywoływać Dispose() na wszystkim co implementuje IDisposable, ale to i tak w finalizerze się by wykonało.

edytowany 1x, ostatnio: msm, 2015-01-21 19:19

Pozostało 580 znaków

2015-01-21 22:09
PAV
0
msm napisał(a):

I czy jest możliwa taka sytuacja: powiedzmy klikamy 10 razy- pamięć wzrasta wyraźnie, potem za każdym kolejnym kliknięciem zaczyna przyrastać coraz wolniej, aż z czasem stabilizuje się na względnie stałym poziomie i klikanie na przycisk niczego już nie zmienia?

Raczej "za każdym kliknięciem przybywa 10kb pamięci, z każdym kliknięciem przybywa 10kb pamięci, po czym po pewnym kliknięciu ubywa 40 MB pamięci i cykl się powtarza".

Prawdopodobnie po prostu na tyle mało pamięci aplikacja alokuje że GC sie nawet nie uruchamia, bo czeka aż będzie co zwalniać.

Popatrz ew. czy nie zapisujesz się gdzieś kilkakrotnie pod event, bo to chyba najczęstsza przyczyna memory leaków w .net.
Poza tym wypada wywoływać Dispose() na wszystkim co implementuje IDisposable, ale to i tak w finalizerze się by wykonało.

Mój problem z tą aplikacją polega na tym, że była ona robiona jak to się mówi "na kolanie" i przez kogoś innego. Stąd moje dmuchanie trochę na zimne- może tego wycieku faktycznie nie ma- i GC nie zdążył się jeszcze uruchomić. Ale czy jeśli po uruchomieniu aplikacji- mam zużycie na poziomie 80 mb, a po jakimś czasie przeładowywania danych w DataGridach wzrasta ono do powiedzmy 120-130 mb to jest wciąż mało? Takie rzeczy jak użycie składni using już poprawiłem- więc tu nie powinno zjadać pamięci. Ew. zostało jeszcze to ciągle otwarte połączenie z bazą danych.

Czy mógłbyś podać przykład "zapisywania się kilkakrotnie pod event"? Czy chodzi o sytuację, gdy mam jakąś metodę, która powiedzmy wywoływana jest co jakiś czas, ma ona jakąś logikę i w jednej linii powiedzmy podpina kolejny raz pod jakiś event kolejny raz tę samą metodę (przykład poniżej- trochę głupi, ale chodzi o ilustrację). Jeśli tak- to nie widziałem w tej aplikacji takiego czegoś. Jedyne przypięcia metod pod eventy są w konstruktorach- ale to jest raczej normalne, prawda?

public void Test
{
   for(int i = 0; i < 100; i++)
   {
      //do something
      _dataGrid.Click += new RoutedEventHandler(DataGridClickMethod); 
   } 

}

Wrócę do pytania z poprzedniego posta: czy użycie statycznych metod ma jakiś negatywny wpływ na pamięć? Gdzieś na zagranicznym forum, czytałem, że zasoby zużywane przez statyczne metody są kasowane z pamięci, po jej wykonaniu. To prawda?

edytowany 2x, ostatnio: PAV, 2015-01-21 22:17

Pozostało 580 znaków

2015-01-22 11:32
msm
1

Wrócę do pytania z poprzedniego posta: czy użycie statycznych metod ma jakiś negatywny wpływ na pamięć?

Nie.

Gdzieś na zagranicznym forum, czytałem, że zasoby zużywane przez statyczne metody są kasowane z pamięci, po jej wykonaniu. To prawda?

Tak.

Tak samo jak ze zwykłymi metodami, zmienne tworzone w metodach statycznych są zwalniane przez GC kiedy już nie są potrzebne.

Jedyne przypięcia metod pod eventy są w konstruktorach- ale to jest raczej normalne, prawda?

Raczej tak - problem z eventami jest wtedy kiedy
a) podpinasz się pod jeden event za dużo razy (np. przy każdym kliknięciu myszką/etc)
b) obiekt którego trzyma event wychodzi z zasięgu. Eventy trzymają obiekty dla GC, więc efektywnie tworzysz memory leak
prawdopodobnie żadna z tych sytuacji nie zachodzi, ale możesz sprawdzić dla pewności.

Możesz sprawdzić jaki będzie wynik wywołania poniższego kodu:

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.WaitForFullGCComplete();
        GC.Collect();

(Jeśli wróci do 80 mb to widać że tyle faktycznie jest używane. Jeśli nie wróci to trzeba szukać dalej (to jeszcze niczego nie dowodzi, bo nikt nie mówi że program musi zwalniać pamięć do systemu od razu jeśli nie używa całej)).

Jeśli chcesz poważnie do tego podejść, możesz zawsze użyć jakiegoś Memory Profilera dla .net, będziesz miał 100% pewnosci co się dzieje.

edytowany 1x, ostatnio: msm, 2015-01-22 11:37

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

1 użytkowników online, w tym zalogowanych: 0, gości: 1, botów: 0