ZIP i miliony plików

0

Jest aplikacja (napisana w XE2), która wyciąga z bazy danych pewne informacje i za pomocą strumieni wrzuca do struktur System.TZipFile.

Mechanizm generalnie działa i nie byłoby zasadniczo większego problemu, ale plików, które powstają są miliony (dosłownie - milion plików to norma). Każdy z nich ma wielkość ok 1,5 kB.

Problem w tym, że aplikacja bardzo puchnie w pamięci. Po przetworzeniu kilkuset tysięcy plików zajmuje już ponad 1 GB.

Może znacie jakiś komponent, który radzi sobie z ogromnymi ilościami plików i nie zapycha zbytnio pamięci ?

0

Czy na pewno:

  1. nigdzie nie przydzielasz pamięci?
  2. zawsze zamykasz plik?
  3. robisz to w głównym wątku lub poprawnie wątki zamykasz?
  4. czemu uważasz że to przez TZipFile a nie przez komponenty bazy?
0
_13th_Dragon napisał(a):

Czy na pewno:

  1. nigdzie nie przydzielasz pamięci?
  2. zawsze zamykasz plik?
  3. robisz to w głównym wątku lub poprawnie wątki zamykasz?
  4. czemu uważasz że to przez TZipFile a nie przez komponenty bazy?

Główny kawałek kodu wygląda tak:

procedure Tfrm_Glowne.DatabaseSpaceOptimalization(AAuto: Boolean);
var Zipfile: TZipFile;
    Directory : TDirectory;
    DPath,XMLFileName,ZIPFileName : string;
begin
    DPath:=ExtractFilePath(ParamStr(0))+'\Tokens';
    ZipFile := TZipFile.Create;
    ZipFile.UTF8Support:=TRUE;
    if not Directory.Exists(DPath) then begin
        Directory.CreateDirectory(DPath);
    end;
    if Directory.Exists(DPath) then begin
        pgExecute.SQL.Clear;
        pgExecute.SQL.Add('SELECT pe.odp_xml,pe.id_oper ');
        pgExecute.SQL.Add('FROM tabelka pe ');
        pgExecute.Active:=TRUE;
        if not pgExecute.Eof then begin
            try
                //Connection.StartTransaction;
                ZIPFileName:=DPath+'\TokensPackage.kpz';
                if not ZipFile.IsValid(ZIPFileName) then begin
                    DeleteFile(ZIPFileName);
                end;
                ZipFile.Open(ZIPFileName,zmWrite);
                pgExecute.First;
                while not pgExecute.Eof do begin
                    XMLFileName:=pgExecute.FieldByName('id_oper').AsString+'.xml';
                    ZipFile.Delete(XMLFileName);
                    ZipFile.Add(AnsiStringToStream(pgExecute.FieldByName('odp_xml').AsString),XMLFileName);
                    pgExecute.Next;
                end;
                ZipFile.Close;
                //Connection.Commit;
            except
                //Connection.Rollback;
            end;
        end;
    end;
end;
 

Funkcja AnsiStringToStream robi tylko tyle, że przepakowuje stringa w TStream - chodziło o wyeliminowanie zapisywania pliku na dysk, pobierania go do archiwum i kasowania - cały proces przyspieszył dzięki temu kilkukrotnie. Ale - dla porządku podaję jej kod:

 
function Tfrm_Glowne.AnsiStringToStream(const AString: AnsiString): TStream;
begin
    Result := TMemoryStream.Create;
    Result.Write(PAnsiChar(AString)^, Length(AString));
    Result.Position := 0;
end;

Jest jeszcze opcja, żeby wszystkie pliki wrzucić do jednej paczki XMLowej i pakować jako jeden plik - aktualne rozwiązanie daje jednak pewien komfort powtórnego wyszukania i wyciągnięcia potrzebnego pliku. Chodzi o to, że przez te dane baza danych koszmarnie puchnie. Więc żeby ją odchudzić - aktualnie niewykorzystywane dane wyciągane są na zewnątrz (do archiwum), pakowane, a w razie potrzeby pobierane i wrzucane z powrotem do bazy.

1

Na mój gust brakuje Ci w finally ZipFile.Free();. Ale daaawno nic nie pisałem w Delphi, więc to raczej strzał.

0
ŁF napisał(a):

Na mój gust brakuje Ci w finally ZipFile.Free();. Ale daaawno nic nie pisałem w Delphi, więc to raczej strzał.

Było, ale widać w trakcie coraz to kolejnych prób gdzieś się zjadło.

Oczywiście masz rację, ale to zaskutkuje dopiero po zakończeniu wszystkich operacji.

Tu problem jest jakby to nazwać ... w locie. W pętli (while) obszar zajętej pamięci stale rośnie.

0

możesz spróbować dodawanie plików przeprowadzić blokami - po kilka tysięcy plików, potem sprzątać, ponownie tworzyć niezbędne obiekty i jechać dalej.

1

kolejny genialny kod...
Result := TMemoryStream.Create; ale żeby zwolnić to już nie ma komu... No i nie słyszałeś pewnie o czymś takim jak TStringStream czy też wykorzystania jednego obiektu wiele razy zamiast co chwilę go tworzyć i zwalniać. A wystarczyło dodać fastmm do projektu albo włączyć raportowanie wycieków pamięci jak masz nowszy kompilator

0
abrakadaber napisał(a):

kolejny genialny kod...
Result := TMemoryStream.Create; ale żeby zwolnić to już nie ma komu... No i nie słyszałeś pewnie o czymś takim jak TStringStream czy też wykorzystania jednego obiektu wiele razy zamiast co chwilę go tworzyć i zwalniać. A wystarczyło dodać fastmm do projektu albo włączyć raportowanie wycieków pamięci jak masz nowszy kompilator
Po pierwsze - kod nie jest mój - znalazłem go na szybko w internecie w czasie kolejnych prób przyspieszenia eksportu danych do plików.

Owszem - słyszałem o TStringStream, ale TZipFile.Add nie chciał go z jakiegoś powodu przyjąć.

Następna sprawa - jeżeli mamy Result:=TMemoryStream.Create i pod koniec funkcji zrobimy Result.Free, to co zostanie zwrócone przez funkcję ?

Zawsze wydawało mi się (być może błędnie), że obiekty utworzone wewnątrz funkcji są automatycznie zwalniane.

Dziękuję Ci, że zwróciłeś na to uwagę - ale czy na prawdę tak dużym kłopotem jest zrobić to deczko uprzejmiej ? Gdybym był na Twoim poziomie nie szukałbym pomocy. Rejt ?

0
toyman napisał(a):

Po pierwsze - kod nie jest mój - znalazłem go na szybko w internecie w czasie kolejnych prób przyspieszenia eksportu danych do plików.

A to coś zmienia?

Owszem - słyszałem o TStringStream, ale TZipFile.Add nie chciał go z jakiegoś powodu przyjąć.

W programowaniu nie ma czegoś takiego, że coś nie działa bo wiatr z zachodu wieje. Jak nie działa to wyrzuca jakiś komunikat i/lub błąd. Jak jest błąd to trzeba go rozwiązać.

Następna sprawa - jeżeli mamy Result:=TMemoryStream.Create i pod koniec funkcji zrobimy Result.Free, to co zostanie zwrócone przez funkcję ?

A czy ja gdzieś napisałem, że masz go zwolnić pod koniec funkcji? Jak funkcja zwraca OBIEKT to ten obiekt ma zwolnić ten, kto tę funkcję wywołał i "chce" tego obiektu.

Zawsze wydawało mi się (być może błędnie), że obiekty utworzone wewnątrz funkcji są automatycznie zwalniane.
Bardzo źle Ci się wydawało. Obiekty nigdy nie są same niszczone w Delphi. Samoistnie sprzątane są jedynie typy proste (int, float, ...) stringi, tablice dynamiczne i interfejsy. Dodatkowo można sprawić aby komponenty zostały zwolnione "automagicznie" jeśli poda się im Ownera podczas ich tworzenia. Jednak najlepiej jest przyjąć zasadę co sam stworzyłeś, sam zwolnij. Kiedy jeszcze nie do końca wszystko ogarniasz jest to najbezpieczniejsze.

Dziękuję Ci, że zwróciłeś na to uwagę - ale czy na prawdę tak dużym kłopotem jest zrobić to deczko uprzejmiej ? Gdybym był na Twoim poziomie nie szukałbym pomocy. Rejt ?
Gdybyś dodał fastmm lub włączył raportowanie wycieków to po zakończeniu aplikacji byś dostał dokładnie ile i jakich obiektów nie zostało zwolnionych. W tym wypadku nic więcej nie potrzeba.

0

No dobrze. Zmodyfikowałem kod w taki sposób:

 
procedure Tfrm_Glowne.DatabaseSpaceOptimalization(AAuto: Boolean);
var Zipfile: TZipFile;
    Directory : TDirectory;
    DPath,XMLFileName,ZIPFileName : string;
    MS : TMemoryStream;
begin
    DPath:=ExtractFilePath(ParamStr(0))+'\Tokens';
    ZipFile := TZipFile.Create;
    ZipFile.UTF8Support:=TRUE;
    if not Directory.Exists(DPath) then begin
        Directory.CreateDirectory(DPath);
    end;
    MS := TMemoryStream.Create;
    if Directory.Exists(DPath) then begin
        pgExecute.SQL.Clear;
        pgExecute.SQL.Add('SELECT pe.odp_xml,pe.id_oper ');
        pgExecute.SQL.Add('FROM tabelka pe ');
        pgExecute.Active:=TRUE;
        if not pgExecute.Eof then begin
            try
                //Connection.StartTransaction;
                ZIPFileName:=DPath+'\TokensPackage.kpz';
                if not ZipFile.IsValid(ZIPFileName) then begin
                    DeleteFile(ZIPFileName);
                end;
                ZipFile.Open(ZIPFileName,zmWrite);
                pgExecute.First;
                while not pgExecute.Eof do begin
                    XMLFileName:=pgExecute.FieldByName('id_oper').AsString+'.xml';
                    MS.Write(PAnsiChar(pgExecute.FieldByName('odp_xml').AsString)^, Length(pgExecute.FieldByName('odp_xml').AsString));
                    MS.Position := 0;
                    ZipFile.Add(MS,XMLFileName);
                    pgExecute.Next;
                end;
                ZipFile.Close;
                ZipFile.Free;
                MS.Free;
                //Connection.Commit;
            except
                //Connection.Rollback;
            end;
        end;
        pgExecute.SQL.Clear;
    end;
end;

Okazało się, że po wykonaniu pgExecute.SQL.Clear zwolniło się prawie 1 GB z 1.7 GB, których aplikacja użyła. Z tym mogę walczyć w ten sposób, że będę pobierał dane partiami (ograniczenie klauzulą LIMIT ... OFFSET)

Aplikacja po starcie i przed wykonaniem powyższych operacji zajmuje w pamięci 27 MB - czyli do zwolnienia zostało jeszcze ok 600 MB.

Pomożesz mi z fastmm ? Jak tego użyć ?

EDIT: Po ZipFile.Add dodałem jeszcze MS.Clear. Aplikacja ładnie utrzymuje użycie pamięci w obrębie kilku megabajtów - po załadowaniu 100 000 rekordów z bazy zajmuje w pamięci ok 0,5 GB, a w trakcie pracy przyrasta ok raptem kilka MB. Po wykonaniu wszystkich operacji wraca do poziomu 32 MB czyli ok 5 MB więcej niż tuż po starcie.

1
  1. co do fastmm to jaka wersja delphi?
  2. masz do kitu formatowanie - nie widać co do czego
  3. naucz się używać try .. finally .. end
  4. w złym miejscu zwalniasz obiekty - jak się coś sypnie to obiekty nie zostaną zwolnione
  5. dwa razy sprawdzasz, czy katalog istnieje - po co? Zamiast sprawdzać i ew tworzyć możesz po prostu zrobić ForceDirectory - do doczytania co to i po co
procedure Tfrm_Glowne.DatabaseSpaceOptimalization(AAuto: Boolean);
var
  Zipfile: TZipFile;
  Directory: TDirectory;
  DPath, XMLFileName, ZIPFileName: string;
  MS: TMemoryStream;
begin
  DPath := ExtractFilePath(ParamStr(0)) + '\Tokens';
  Zipfile := TZipFile.Create;
  MS := TMemoryStream.Create;
  try
    Zipfile.UTF8Support := true;
    if not Directory.Exists(DPath) then
      Directory.CreateDirectory(DPath);

    pgExecute.SQL.Clear;
    pgExecute.SQL.Add('SELECT pe.odp_xml,pe.id_oper ');
    pgExecute.SQL.Add('FROM tabelka pe ');
    pgExecute.Open; // Active := true;
    if pgExecute.Eof then
      Exit;

    try
      // Connection.StartTransaction;
      ZIPFileName := DPath + '\TokensPackage.kpz';
      if not Zipfile.IsValid(ZIPFileName) then
        DeleteFile(ZIPFileName);

      Zipfile.Open(ZIPFileName, zmWrite);
      pgExecute.First;
      while not pgExecute.Eof do
      begin
        MS.Clear;
        XMLFileName := pgExecute.FieldByName('id_oper').AsString + '.xml';
        MS.Write(PAnsiChar(pgExecute.FieldByName('odp_xml').AsString)^, Length(pgExecute.FieldByName('odp_xml').AsString));
        MS.Position := 0;
        Zipfile.Add(MS, XMLFileName);
        pgExecute.Next;
      end;
      // Connection.Commit;
    except
      // Connection.Rollback;
    end;
  finally
    Zipfile.Close;
    Zipfile.Free;
    MS.Free;
  end;
end;
0
abrakadaber napisał(a):
  1. co do fastmm to jaka wersja delphi?
  2. masz do kitu formatowanie - nie widać co do czego
  3. naucz się używać try .. finally .. end
  4. w złym miejscu zwalniasz obiekty - jak się coś sypnie to obiekty nie zostaną zwolnione
  5. dwa razy sprawdzasz, czy katalog istnieje - po co? Zamiast sprawdzać i ew tworzyć możesz po prostu zrobić ForceDirectory - do doczytania co to i po co
  1. XE2
  2. taki styl mi odpowiada - dla mnie jest czytelny. Nie. Nie dyskutuję - każdy ma swoje przyzwyczajenia. Mnie to wystarcza do sprawnego poruszania się po kodzie.
  3. Aplikacja może być uruchamiana w dwóch trybach (Automatycznym z poziomu zaplanowanych zadań systemu Windows i ręcznie - guzior w aplikacji) procedura jest wspólna, a obsługa błędów jest robiona na wyższym poziomie.
  4. W związku z powyższym to rzeczywiście jest problem.
  5. Nie wiedziałem, że istnieje - dzięki.
0
  1. taki styl mi odpowiada - dla mnie jest czytelny. Nie. Nie dyskutuję - każdy ma swoje przyzwyczajenia. Mnie to wystarcza do sprawnego poruszania się po kodzie.

Ale to Ty chcesz od nas pomocy. Więc wypadało by żeby kod był zrozumiały dla NAS byśmy mogli znaleźć w nim błąd. Ciężko ogarnąć, że nie Ty robisz nam łaskę zadając pytanie tylko my robimy łaskę Tobie, że na nie odpowiadamy?

0
toyman napisał(a):
  1. XE2
    poczytaj o ReportMemoryLeaksOnShutdown
  1. Aplikacja może być uruchamiana w dwóch trybach (Automatycznym z poziomu zaplanowanych zadań systemu Windows i ręcznie - guzior w aplikacji) procedura jest wspólna, a obsługa błędów jest robiona na wyższym poziomie.
    to nie ma żadnego znaczenia! Masz źle napisany ten kawałek kodu i koniec. Jeśli tworzysz obiekt to musisz go zwolnić, a jak wyleci błąd a nie będziesz miał bloku try..finally..end, gdzie zwolnienie obiektów nie będzie w finally..end to masz wyciek pamięci i koniec.
1
babubabu napisał(a):
  1. taki styl mi odpowiada - dla mnie jest czytelny. Nie. Nie dyskutuję - każdy ma swoje przyzwyczajenia. Mnie to wystarcza do sprawnego poruszania się po kodzie.

Ale to Ty chcesz od nas pomocy. Więc wypadało by żeby kod był zrozumiały dla NAS byśmy mogli znaleźć w nim błąd. Ciężko ogarnąć, że nie Ty robisz nam łaskę zadając pytanie tylko my robimy łaskę Tobie, że na nie odpowiadamy?
Przepraszam. Nie chciałem nikogo urazić. Nie zabierałeś do tej pory głosu na temat, więc nie miałem pojęcia, że przeszkadza Ci mój styl kodowania. Abrakadaber poradził sobie przeformatowując mój kod tak, żeby był czytelny dla niego.

Przepraszam. Nie przyszło mi do głowy, że formatowanie kodu który pokazałem może stanowić aż tak wielki problem. Okazuje się, że stanowi większy niż ten z którym się zwracam.

W takim razie - powiedz jak sobie życzysz, a ja sformatuję mój kod w taki sposób, żeby nie raził w oczy.

abrakadaber napisał(a):

to nie ma żadnego znaczenia! Masz źle napisany ten kawałek kodu i koniec. Jeśli tworzysz obiekt to musisz go zwolnić, a jak wyleci błąd a nie będziesz miał bloku try..finally..end, gdzie zwolnienie obiektów nie będzie w finally..end to masz wyciek pamięci i koniec.
W porządku - nie kwestionuję tego. Nie ma powodu do nerwów. Przecież nie następuję na twoją godność, więdzę ani doświadczenie.

W punkcie trzecim przyznałem, że stanowi to problem bo obsługa błędów jest wyżej. Dziękuję, że zwróciłeś mi na to uwagę - będę musiał to jakoś rozwiązać.

0

Okej - przerobiłem procedurę w ten sposób:

 
procedure Tfrm_Glowne.DatabaseSpaceOptimalization(AAuto: Boolean);
var Zipfile: TZipFile;
    Directory : TDirectory;
    DPath,XMLFileName,ZIPFileName : string;
    MS : TMemoryStream;
begin
    DPath:=ExtractFilePath(ParamStr(0))+'\Tokens';
    ZipFile := TZipFile.Create;
    ZipFile.UTF8Support:=TRUE;
    if not Directory.Exists(DPath) then begin
        Directory.CreateDirectory(DPath);
    end;
    MS := TMemoryStream.Create;
    if Directory.Exists(DPath) then begin
        pgExecute.SQL.Clear;
        pgExecute.SQL.Add('SELECT pe.odp_xml,pe.id_oper ');
        pgExecute.SQL.Add('FROM tabelka pe ');
        pgExecute.Active:=TRUE;
        if not pgExecute.Eof then begin
            try
                ZIPFileName:=DPath+'\TokensPackage.kpz';
                if not ZipFile.IsValid(ZIPFileName) then begin
                    DeleteFile(ZIPFileName);
                end;
                ZipFile.Open(ZIPFileName,zmWrite);
                pgExecute.First;
                while not pgExecute.Eof do begin
                    XMLFileName:=pgExecute.FieldByName('id_oper').AsString+'.xml';
                    MS.Write(PAnsiChar(pgExecute.FieldByName('odp_xml').AsString)^, Length(pgExecute.FieldByName('odp_xml').AsString));
                    MS.Position := 0;
                    ZipFile.Add(MS,XMLFileName);
                    pgExecute.Next;
                    MS.Clear;
                end;
                ZipFile.Close;
            finally
                ZipFile.Free;
                MS.Free;
            end;
        end;
        pgExecute.SQL.Clear;
    end;
end;

A wywołanie w trybie ręcznym wygląda tak:

procedure Tfrm_Glowne.Button19Click(Sender: TObject);
begin
    try
        try
            DatabaseSpaceOptimalization(FALSE);
            Application.MessageBox(PChar('Zakończono proces porządkowania bazy danych'),'Informacja',MB_OK+MB_ICONINFORMATION);
        except
            on E : Exception do begin
                Application.MessageBox('Błąd na wysokim poziomie','Błąd aplikacji',MB_ICONERROR+MB_OK);
            end;
        end;
    finally
        frm_Glowne.Enabled:=TRUE;
    end;
end;
 

Wg debuggera (po wymuszeniu błędu wewnątrz pętli while) - najpierw wykonuje się sekcja finally wewnątrz procedury, a następnie except w procedurze wywołującej.

ReportMemoryLeaksOnShutdown aktualnie pokazuje jakieś dwa małe wycieki na kilkadziesiąt bajtów, ale to w innym obszarze.

Oczywiście - będę jeszcze musiał posprzątać i blok try ... finally ... except zrobić tak jak pokazał abrakadaber kilka postów wyżej.

Pytanie czy poza tym widzicie jeszcze jakieś znaczące błędy ?

0

Okej - po ostatecznym porządkowaniu:

 
procedure Tfrm_Glowne.DatabaseSpaceOptimalization(AAuto: Boolean);
var Zipfile: TZipFile;
    Directory : TDirectory;
    DPath,XMLFileName,ZIPFileName : string;
    MS : TMemoryStream;
begin
    try
        ZipFile := TZipFile.Create;
        MS := TMemoryStream.Create;
        ZipFile.UTF8Support:=TRUE;
        DPath:=ExtractFilePath(ParamStr(0))+'\Tokens';
        if not Directory.Exists(DPath) then begin
            Directory.CreateDirectory(DPath);
        end;
        pgExecute.SQL.Clear;
        pgExecute.SQL.Add('SELECT pe.odp_xml,pe.id_oper ');
        pgExecute.SQL.Add('FROM tabelka pe ');
        pgExecute.Active:=TRUE;
        if not pgExecute.Eof then begin
            try
                Connection.StartTransaction;
                ZIPFileName:=DPath+'\TokensPackage.kpz';
                if not ZipFile.IsValid(ZIPFileName) then begin
                    DeleteFile(ZIPFileName);
                end;
                ZipFile.Open(ZIPFileName,zmWrite);
                pgExecute.First;
                while not pgExecute.Eof do begin
                    XMLFileName:=pgExecute.FieldByName('id_oper').AsString+'.xml';
                    MS.Write(PAnsiChar(pgExecute.FieldByName('odpb_xml').AsString)^, Length(pgExecute.FieldByName('odp_xml').AsString));
                    MS.Position := 0;
                    ZipFile.Add(MS,XMLFileName);
                    pgExecute.Next;
                    MS.Clear;
                end;
                ZipFile.Close;
                Connection.Commit;
            except
                on E : Exception do begin
                    Connection.Rollback;
                    raise Exception.Create(E.Message);
                end;
            end;
        end;
    finally
        ZipFile.Free;
        MS.Free;
        pgExecute.SQL.Clear;
    end;
end;

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