TFileStream + TMemoryStream + TStringList = Out of memory while expanding memory stream

0

Cześć,

chciałbym Was prosić o pomoc w rozwiązaniu pewnego problemu. Zaczynając od początku idea jest następująca:

  • dobieram się do pliku txt, który jest zablokowany, więc ładuje jego zawartość do TFileStream
  • szukając interesujących mnie informacji w dalszym kodzie do TStringList ładuje zawartość FileStream
  • gdy aplikacja generuje wyjątek out of memory próbuję załadować mniejszy fragment streamu

Kod przedstawia się następująco:

LinesList := TStringList.Create;
FileStream := nil;
FileStream := TFileStream.Create(nazwaPliku,fmOpenRead or fmShareDenyNone);
FileStream.Seek(0,soFromBeginning);
//LinesList.LoadFromStream(FileStream);


try
  try
    LinesList.LoadFromStream(FileStream);  // <----- tutaj się wywalał kiedyś błąd
  except
  { tutaj go obsłużyłem i dłuższy czas było ok, ale znów się coś popsuło }
    if FileExists('temp.log') then   
      DeleteFile('temp.log');
    rozmiarStream := FileStream.Size div 4;
    FileStream.Seek(rozmiarStream*3, soBeginning);
    FileStream.Seek(0, soBeginning);
    MemoryStream := TMemoryStream.Create;
    MemoryStream.CopyFrom(FileStream, rozmiarStream);
    MemoryStream.SaveToFile('temp.log');
    LinesList.LoadFromFile('temp.log');
    ostatnioPrzeczytanaLinia := 0;
  end;
except
  on e:exception do
   FormMain.ObsluzBlad(e,'procedura:odczytpliku','');
  end;

W przypadku pliku wczytanego do TFileStream > ok 5MB generuje się wyjątek:
Out of memory while expanding memory stream

Kod jest trochę zamieszany, ale niegdyś wyjątki często generowały się w drugim TRY, dlatego obsłużyłem go i postanowiłem w EXCEPT dopisać trochę kodu. Ostatnio nawet tam zaczynają się generować. Widzicie jakąś możliwość na uproszczenie tego, albo zastosowanie bardziej profesjonalnej metody? :)

0

Hmm, przydałaby się selektywna obsługa wyjątków żeby mieć pewność, że faktycznie wystąpił wyjątek klasy EOutOfMemory; 5MB to nie jest dużo - klasa powinna sobie poradzić bez problemu;

Poza tym widzę wyciek pamięci, tutaj:

MemoryStream := TMemoryStream.Create;
MemoryStream.CopyFrom(FileStream, rozmiarStream);
MemoryStream.SaveToFile('temp.log');

nigdzie nie zwalniasz MemoryStream (no chyba, że jest to jakiś globalny obiekt i usuwasz go z pamięci gdzie indziej), choć przyczyną błędu może być coś zupełnie innego;

Przede wszystkim program należy zdebugować - będziesz wiedział dokładnie gdzie crashuje.


  • dobieram się do pliku txt, który jest zablokowany, więc ładuje jego zawartość do TFileStream

Dziwnie to brzmi - jeśli plik jest zablokowany to nijak go nie załadujesz - ani do strumienia, ani do listy; Więc dopóki pliku nie odblokujesz Ty, czy inna aplikacja nie możesz nic zrobić (teoretycznie);

  • gdy aplikacja generuje wyjątek out of memory próbuję załadować mniejszy fragment streamu

W ogóle niepotrzebnie wykorzystujesz strumień przy próbie załadowania całego pliku do listy; Spróbuj mniej więcej w ten sposób:

var
  LinesList: TStringList;
  FileStream: TFileStream;
begin
  LinesList := TStringList.Create();
  try
    try
      LinesList.LoadFromFile('C:\FileName.ext');
    except
      on EOutOfMemory do
      begin
        // wczytanie mniejszego kawałka pliku
      end;
    end;
  finally
    LinesList.Free();
  end;
end;

W ten sposób przechwytujesz wyjątek EOutOfMemory i tylko wtedy próbujesz wczytać mniejszą cześć pliku;

Poza tym Twoja metoda dzielenia pliku na mniejsze kawałki jest niepoprawna, bo skąd masz pewność, że po podzieleniu rozmiaru pliku na 4 trafisz akurat na koniec linii w tym miejscu? Jeśli chcesz dzielić plik na mniejsze części to najpierw wylicz rozmiar kawałka, przesuń kursor w strumieniu na wyliczony offset a następnie znajdź najbliższy znak końca linii (Linux) lub ciąg końca linii (Windows - #13#10, odpowiednik stałej sLineBreak); Wtedy będziesz miał pewnośc, że nie utniesz linii w połowie.

0

Dzięki za odpowiedź.
Zwalniać zwalniam wszystkie obiekty na samym końcu procedury, a ta jest bardzo długa:

FreeAndNil(FileStream);
FreeAndNil(LinesList);
FreeAndNil(MemoryStream);

Niestety z debuggowaniem mam problem. Aplikacja działa na dużej liczbie PC w firmie porozrzucanej na oddziały i obserwuję w bazie ilość raportowanych błędów. U mnie na debuggerze wszystko chodzi ok.
Co do wycieków pamięci: gdzieś na pewno musi być ponieważ appka dotychczas zajmowała < 15 mb pamięci fizycznej, a po ostatnim release w na stacjach gdzie występują błędy ok 90 mb.

1

Zwalniać zwalniam wszystkie obiekty na samym końcu procedury, a ta jest bardzo długa

  1. jeśli procedura jest dłuższa niż jeden ekran - wymaga podzielenia na mniejsze kawałki;
  2. podczas dynamicznego tworzenia obiektów ze względów bezpieczeństwa warto stosować bloki try .. finally i dodatkowo jeśli trzeba try .. except - jeśli zdarzy się wyjątek to jeśli zwalnianie obiektów nie jest zaimplementowane w bloku try .. finally, a dokładniej w jego części finally .. end - nie zostaną zwolnione, przez co masz wyciek pamięci;
    Podstawowa zasada - tworzysz obiekt dynamicznie - resztę wykonujesz w bloku try .. finally, a zwalniasz go w finally .. end, mały przykład:
var
  LinesList: TStringList;
  FileStream: TFileStream;
begin
  LinesList := TStringList.Create();
  FileStream := TFileStream.Create('C:\FileName.ext', fmOpenRead);
  try
    try
      LinesList.LoadFromStream(FileStream);
      {...}
    except
      // obsługa wyjątku
    end;
  finally
    // w razie wystąpienia wyjątku lub nie klasy będą zawsze zwalniane
    LinesList.Free();
    FileStream.Free();
  end;
end;

Dzięki temu zawsze bezpiecznie zostaną zwolnione obiekty - bez względu na to czy wystąpił wyjątej czy nie;


Jeśli chodzi o zwalnianie obiektów - z procedury FreeAndNil korzystaj tylko i wyłącznie wtedy, gdy masz globalny obiekt i wykorzystujesz go w wielu miejscach; Jeśli obiekty są tworzone lokalnie w procedurach - nie musisz ich nilować i wystarczy zwolnić je metodą Free.

0

Dzięki za wskazówkę z .Free, niby oczywistość, a nigdy się nad tym nie zastanawiałem. Człowiek jeszcze dużo musi się nauczyć...
Jeśli chodzi o bloki do dokładnie w ten sposób zazwyczaj robię:

try
  try

  finally

  end;
except
 //obsługa błędu do log'a
end;

dodatkowo w tej metodzie ponieważ jest długa (jeszcze) wszystko jest wrzucone do try .. finally .. end i tam zwalniam wszystkie obiekty. Zastanawia mnie tylko dlaczego metoda LoadFromStream w klasie TStringList powoduje wystąpienie wyjątku... Czy może być powodem uruchomienie całej procedury w osobnym wątku? Używam biblioteki SynchedThreads do tego.

0

jestem pewny, że masz gdzie indziej problemy. Może to być np. wczytywanie setki takich plików bez zwalniania pamięci po każdym pliku. Operuję na większych plikach i nigdy nie udało mi się wysypać filestreama nawet na plikach mających ponad 1GB. tstringlist daje sobie radę z plikami wielkości dziesiątków MB (większych nie miałem serca ładować na "raz"). Z drugiej strony czy musisz najpierw wczytywać CAŁY plik do pamięci - nie możesz szukać tego co chcesz "grzebiąc w pliku", tzn. wczytując małe bloki (np. 1000 znaków) do bufora i operować na raz na nich?

1
tomix » komentarz napisał(a)

zawsze się nad tym zastanawiałem i kiedyś wyszło mi "na logikę", że moja wersja jest lepsza. czy mógłbyś mi to jakoś uzasadnić? oczywiście wierzę Ci na słowo, ale logicznie byłoby mi prościej to pojąć

@tomix - a wystarczy sobie takie coś zdebugować :]

Pierwszy przykład:

procedure Foo();
var
  slBald: TStringList;
begin
  slBald := TStringList.Create();
  try
    try
      slBald.LoadFromFile('FileName.ext');
    except
      on E: Exception do
      begin
        slBald.Add('[Error]' + E.Message);
        // dalsze operacje z wykorzystaniem 'slBald'
      end;
    end;
  finally
    slBald.Free();
  end;
end;

i teraz jeśli zostanie zgłoszony jakikolwiek wyjątek zaistniały podczas wczytywania pliku do listy - wyjątek zostanie obsłużony, czyli do listy zostanie zapisana treść wyjątku; Będziesz mógł dalej z tą listą coś zrobić - np. skojarzyć ją z listą jakiejś kontrolki np. TMemo, po czym lista zostanie zwolniona z pamięci;

Drugi przykład:

procedure Foo();
var
  slBald: TStringList;
begin
  slBald := TStringList.Create();
  try
    try
      slBald.LoadFromFile('FileName.ext');
    finally
      slBald.Free();
    end;
  except
    on E: Exception do
    begin
      slBald.Add('[Error]' + E.Message);
      // dalsze operacje z wykorzystaniem 'slBald'
    end;
  end;
end;

jeśli przy wczytywaniu zawartości pliku do listy zostanie zgłoszony wyjątek - lista co prawda zostanie zwolniona z pamięci, jednak podczas próby dodania do niej pozycji z treścią błędu dostaniesz AV, bo obiekt już nie istnieje w pamięci; Tak więc w takim przypadku nie możesz już nic zrobić z tym obiektem; Jeśli nie jest to potrzebne bo chcesz np. jedynie wyświetlić komunikat to Ok, ale jeżeli obiekt będzie jeszcze potrzebny to wystąpią kolejne wyjątki (w tym naruszenia pamięci EAccessViolation);

Dlatego bezpieczniej i czytelniej jest osadzić blok try .. except w bloku try .. finally, przez co po wystąpieniu wyjątku możesz dalej operować na tym obiekcie, bo wtedy jeszcze istnieje jego instancja w pamięci.

0

powracając do meritum chciałbym jeszcze dodać, że przed wyjątkiem, o którym pisałem wyżej występuje jeszcze jeden:
Thread creation error: W magazynie brak miejsca dla wykonania tego polecenia (Thread creation error: W magazynie brak miejsca dla wykonania tego polecenia(error:8)) i dopiero potem wyjątek Out of memory while expanding memory stream. Czyżby jednak zbyt mała ilość pamięci zarezerwowana na uruchomienie metody w wątku i wczytanie ok 5mb textu?

@furious programming
To co piszesz ma sen, pozwól jednak, że przedstawię Ci swoją metodę:

procedure ObsluzBlad(e:exception; Query:TZQuery);
begin
try
  try
    Query.SQL.Text := 'INSERT INTO...';
    Query.Connection.Connect;
    Query.ExecSQL;
  finally
    Query.Connection.Disconnect;
  end;
except
  {...}
end;

procedure PobierzDane(Query:TZQuery);
begin
  try
    try
      ...
      query.open;
      raise exception.create('generujemy wyjatek');
  except
    on e:exception do ObsluzBlad(e,Query);
  end;
finally;
 Query.Connection.Disconect;
end;
end;

Zwróć uwagę, że jeśli obie metody operują na tym samym obiekcie Query to w trakcie wyjątku przekazanie jego referencji do metody ObsluzBlad w trakcie realizacji metody PobierzDane kiedy jeszcze ów obiekt jest zajęty spowoduje wystąpienie kolejnego wyjątku tym razem w ObsluzBlad ;) Czy sie mylę? Bezpieczniej zawsze wydawało mi się najpierw uwolnić obiekt Query od innych zadań, a potem dopiero go użyć.

@abrakadaber
W jaki sposób proponujesz załadować do bufora FileStream tylko część pliku? O ile się nie mylę to konstruktor TFileStream nie posiada takiego parametru, a pamiętaj, że plik jest non stop w użyciu przez inną aplikację, więc jest zablokowany. Kiedy już go załaduje do FileStream i wysypuje się przy TStringList.LoadFroStream to dopiero wtedy robię Seek na 3/4 wielkości, kopiuję do MemoryStream i ładuję do listy. Chodź w sumie tak głośno się teraz zastanawiam czy nie na to samo by wyszło gdybym zrobił tak:

FileStream.Seek((FileStream.Size div 4)*3,soFromBegining);
TStringList.LoadFromStream(FileStream);

i pominął zupełnie MemoryStream jak wyżej wkleiłem.

0

Bezpieczniej zawsze wydawało mi się najpierw uwolnić obiekt Query od innych zadań, a potem dopiero go użyć.

Uwolnić tak, ale nie niszczyć (zwalniać z pamięci);

Zwróć jednak uwage na to, że zarówno w procedurze PobierzDane jak i ObsluzBlad wywołujesz:

Query.Connection.Disconect;

więc robisz to dwa razy; Jeśli w procedurze PobierzDate wystąpi wyjątek (lub statycznie go wywołasz tak, jak masz w kodzie) najpierw zostanie on obsłużony, czyli wywołana zostanie procedura ObsluzBlad; Następnie do niej został przekazany obiekt, na którym wykonujesz jakieś operacje, po czym wywołujesz metodę rozłączającą zapytanie (Query.Connection.Disconnect); Po wykonaniu tej procedury wracamy do bloku finally w procedurze PobierzDane, gdzie znów zostaje wywołana metoda Quote.Connection.Disconnect, co jest błędem; Być może tutaj tkwi jeden błąd, ale nie mam doświadczenia z bazami danych, więc musisz to sam sprawdzić pod debugerem.

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