Użycie IEnumerable. Jak uniknąć OutOfMemmory Exception?

1

W jaki sposób powinienem używać foreach dla IEnumerable, żeby nie zawalić RAMu?

Przykładowa sytuacja:

IEnumerable<BusinessLineData> data = unitOfWork.BusinessLineAggregateDatas.Get(businessLineId); //pobieramy dane EF (ok. 2mln. obiektów)
// póki co jest ok, RAM nie zajęty bo jeszcze obiektów nie pobraliśmy
// aplikacja wykorzystuje 100MB

foreach (var s in data)
            {
                string a = $"{s.Id};{s.ComplaintNumber};{s.ReasonForRejection}";
// Podczas działania pętli obiekty są kolejno ładowane do RAMu, który w końcu się zapycha.
// Obiekt z poprzedniej iteracji nie zostaje zwolniony, mimo że w tym przypadku zmienna a jest tworzona na nowo co przebieg pętli.
// nie trzymamy przecież żadnej referencji do obiektu "s".

// Wywołanie w tym miejscu s.Dispose(); i GC nic nie daje.
            }
// aplikacja wykorzystuje ponad 1GB RAMu

Jak sobie poradzić, by nie zapchać RAMu, tak by obiekt z poprzedniej iteracji był zwalniany?

screenshot-20181226011014.png

0

Po pierwsze: czemu ładujesz wszystko na raz? Potrzeba biznesowa czy nie do końca przemyślane założenie?
Po drugie: stronicowanie i lazy loading.

Po trzecie: pamięć zawala Ci kolekcja, nie "s". Przecież nie są usuwane z Enumerable'a po ich wykorzystaniu.
Po czwarte: następnym razem zrób snapshot pamięci i zajrzyj do środka, to się przekonasz co naprawdę żre pamięć, ile jest referencji itp.

I na koniec, rozwiązaniem problemu jest ograniczenie wielkości kolekcji, czyli pytanie z punktu pierwszego i wskazówki z drugiego :)

Edit: Późno jest, teraz dopiero zauważyłem...
W jaki sposób masz zrobione relacje?
Bo zakładam, że agregat przechowuje kolekcje obiektów, które się znajdują pod nim. W takim wypadku (zdaje mi się? Niech mnie ktoś poprawi, jak plotę) EF załaduje Ci wszystkie pierdoły, których nie potrzebujesz w danym momencie - wszystkie obiekty pod strukturą. Rozwiązaniem mogłoby być odwrócenie relacji na encjach bazodanowych, żeby mieć większą kontrolę nad tym co, kiedy i w jaki sposób ładujesz do pamięci.

0

Specjalnie uprościłem na razie ten kawałek kodu i uruchamiałem by dojść do sedna problemu. Wiem, że na razie ten kod nic sensownego nie robi, ale póki nie uporam się z tym problemem nie ma sensu iść dalej. Docelowo dane nie mają być przypisywane do tymczasowej zmiennej tylko dodawane do strumienia i tworzyć dużą CSVkę do pobrania ze strony. Stronicowanie mam zrobione w innej części aplikacji i wyświetlam na stronie. Tutaj niestety musi powstać duża CSVka.

Póki co ładowany obiekt nie posiada żadnych referencji/relacji. Kilka prostych pól.

    public class BusinessLineData
    {
        public int Id { get; set; }
        public string ComplaintNumber { get; set; }
        public bool IsRejected { get; set; }
        public string ReasonForRejection { get; set; }
    }

Oczywiście gdybym zrobił unitOfWork.BusinessLineAggregateDatas.Get(businessLineId).ToList(); to by od razu rozsadziło pamięć, ale z IEnumerable w pętli rośnie zużycie RAMu przyrostowo jak widać na wykresie. Obiekt załadowany do pamięci już z niej nie wylatuje przy kolejnej iteracji.

Nie wiem jak to rozwiązać.

0

Aj, bardzo późno było jednak. Wybacz.

Pozostaje tylko jedna kwestia, która się nie zmienia to fakt, że mógłbyś utrzymać stronicowanie wyników i po prostu zapisywać te dane do pliku csv porcjami.
Pytanie jeszcze jak często taka csvka miałaby być tworzona?

0

Sypię głowę popiołem.

To jednak EF zawaliło pamięć bo śledziło wszystkie obiekty, czyli szukałem nie tam gdzie trzeba.
.AsNoTracking() rozwiązało sprawę.

Co prawda mam teraz jeszcze do rozwiązania problem jak w ASP.NET MVC5 wysłać coś strumieniem. Ale z 1GB zajętego RAMu obecnie generowanie pliku zabiera tylko 100mb czyli tyle ile wynosi plik do pobrania. Trzeba było jednak wyłączyć: Response.BufferOutput = false; bo inaczej tworzone HashTable zżerały dopiero sporo pamięci.

2

Jak zaimplementowane jest to repozytorium? Czy BusinessLineData jest encją EF?
Domyślnie EF robi 2 rzeczy które mogą Ci przeszkadzać:

  1. Tracking obiektów
  2. Tworzenie proxy do lazy-loadingu
    Trackingu można uniknąć przez dopisanie .AsNoTracking() w zapytaniu LINQ, a tworzenie proxy można wyłączyć w DbContext.Configuration.ProxyCreationEnabled.
    Natomiast najprostszy i najwydajniejszy sposób na uniknięcie związanego z nimi narzutu to stworzenie osobnej klasy DTO na dane potrzebne w danym przypadku użyciu i zrobienie projekcji, bo klas niebędących encjami powyższe rzeczy nie dotyczą:
    DbContext.BusinessLineData.Where(jakieś_warunki).Select(x => new BusinessLineDataDTO
    {
    Id = x.Id,
    ComplaintNumber = x.ComplaintNumber,
    IsRejected = x.IsRejected,
    ReasonForRejection = x.ReasonForRejection,
    }).ToList();

EDIT:
Spóźniłem się 20 sekund :)

0
mad_penguin napisał(a):

Spóźniłem się 20 sekund :)

Dzięki za odpowiedź mimo wszystko. Przetestowałem twoją propozycję zapisywania do DTO i faktycznie zdaje to egzamin. Wydaje się to o tyle lepsze, że jawnie przesyłamy DTO i nikt się nie będzie mylił później że dostał nie śledzone encje.

1

przy takich wymaganiach rozwiązanie jest jedno - proste i skuteczne. Wywal ORMa (jakiego byś nie miał to przy takiej ilości rekordów się zapcha) i weź DataReadera. Będzie zjadało dużo mniej pamięci a i szybciej powinno być.

1

W ostateczności

<gcAllowVeryLargeObjects enabled="true" />

On 64-bit platforms, enables arrays that are greater than 2 gigabytes (GB) in total size.

:D

1
Webowy Koziołek napisał(a):

Sypię głowę popiołem.

To jednak EF zawaliło pamięć bo śledziło wszystkie obiekty, czyli szukałem nie tam gdzie trzeba.

Nie, to Ty zawaliłeś, używając EF do czegoś, do czego EF (i ORMy w ogóle) nie służą.

mad_penguin napisał(a):

Natomiast najprostszy i najwydajniejszy sposób na uniknięcie związanego z nimi narzutu to stworzenie osobnej klasy DTO na dane potrzebne w danym przypadku użyciu i zrobienie projekcji, bo klas niebędących encjami powyższe rzeczy nie dotyczą

Najprostszy i najwydajniejszy sposób, to użyć BCP do wygenerowania pliku CSV z wyniku zapytania SQL. Zużyje 0 pamięci i GC nawet nie będzie miał czego sprzątać.

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