Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Algorithmen, Datenstrukturen und Klassendesign (https://www.delphipraxis.net/78-algorithmen-datenstrukturen-und-klassendesign/)
-   -   Clientdataset Speicherfreigabe (https://www.delphipraxis.net/210718-clientdataset-speicherfreigabe.html)

lxo 31. Mai 2022 15:38

Clientdataset Speicherfreigabe
 
Hallo zusammen,

wieso wird beim TClientdataset der Speicher nicht freigegeben wenn man Datensätze löscht?
Muss ich da noch irgendetwas auslösen? Beim TFDMemTable wird der Speicher direkt nach dem löschen wieder freigegeben.

Delphi-Quellcode:
unit Unit121;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Data.DB, Datasnap.DBClient, Vcl.StdCtrls,
  FireDAC.Stan.Intf, FireDAC.Stan.Option, FireDAC.Stan.Param,
  FireDAC.Stan.Error, FireDAC.DatS, FireDAC.Phys.Intf, FireDAC.DApt.Intf,
  FireDAC.Comp.DataSet, FireDAC.Comp.Client;

type
  TForm121 = class(TForm)
    gb_clientdataset: TGroupBox;
    b_clientdataset_add: TButton;
    b_clientdataset_delete: TButton;
    l_clientdataset: TLabel;
    gb_fdmemtable: TGroupBox;
    l_fdmemtable: TLabel;
    b_fdmemtable_add: TButton;
    b_fdmemtable_delete: TButton;
    procedure b_clientdataset_addClick(Sender: TObject);
    procedure b_clientdataset_deleteClick(Sender: TObject);
    procedure b_fdmemtable_addClick(Sender: TObject);
    procedure b_fdmemtable_deleteClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private-Deklarationen }
  public
    { Public-Deklarationen }
    lClientDataSet: TClientDataSet;
    lFDMemTable: TFDMemTable;
    procedure Add( xDataset: TDataSet;
                   xLabel: TLabel);
    procedure Delete( xDataset: TDataSet;
                      xLabel: TLabel);
  end;

var
  Form121: TForm121;

implementation

{$R *.dfm}

procedure TForm121.Add(xDataset: TDataSet; xLabel: TLabel);
begin
  for var i := 1 to 9999 do
  begin
    xDataset.Append;
    xDataset.FieldByName('TEST1').AsString := Random( MaxInt).ToString;
    xDataset.FieldByName('TEST2').AsString := Random( MaxInt).ToString;
    xDataset.FieldByName('TEST3').AsString := Random( MaxInt).ToString;
    xDataset.Post;
  end;
  xLabel.Caption := Format( 'Recordcount: %d', [ xDataset.RecordCount]);
end;

procedure TForm121.b_clientdataset_addClick(Sender: TObject);
begin
  Add( lClientDataSet,
       l_clientdataset);
end;

procedure TForm121.b_clientdataset_deleteClick(Sender: TObject);
begin
  Delete( lClientDataSet,
          l_clientdataset);
end;

procedure TForm121.b_fdmemtable_addClick(Sender: TObject);
begin
  Add( lFDMemTable,
       l_fdmemtable);
end;

procedure TForm121.b_fdmemtable_deleteClick(Sender: TObject);
begin
  Delete( lFDMemTable,
          l_fdmemtable);
end;

procedure TForm121.Delete(xDataset: TDataSet; xLabel: TLabel);
begin
  while ( xDataset.RecordCount > 0) do
  begin
    xDataset.Delete;
  end;
  xLabel.Caption := Format( 'Recordcount: %d', [ xDataset.RecordCount]);
end;

procedure TForm121.FormCreate(Sender: TObject);
begin
  Randomize;
  lClientDataSet := TClientDataSet.Create( Self);
  lClientDataSet.FieldDefs.Add( 'TEST1', ftString, 500);
  lClientDataSet.FieldDefs.Add( 'TEST2', ftString, 500);
  lClientDataSet.FieldDefs.Add( 'TEST3', ftString, 500);
  lClientDataSet.CreateDataSet;

  lFDMemTable := TFDMemTable.Create( Self);
  lFDMemTable.FieldDefs.Add( 'TEST1', ftString, 500);
  lFDMemTable.FieldDefs.Add( 'TEST2', ftString, 500);
  lFDMemTable.FieldDefs.Add( 'TEST3', ftString, 500);
  lFDMemTable.CreateDataSet;
end;

end.

Uwe Raabe 31. Mai 2022 15:50

AW: Clientdataset Speicherfreigabe
 
Hast du es mal mit MergeChangeLog probiert?

lxo 31. Mai 2022 16:02

AW: Clientdataset Speicherfreigabe
 
Zitat:

Zitat von Uwe Raabe (Beitrag 1506564)
Hast du es mal mit MergeChangeLog probiert?

Wenn ich nach dem Insert und nach dem Delete MergeChangeLog ausführe habe ich den Unterschied, dass beim erneuten Insert von 9999 Datensätzen er vermutlich den selben Speicher verwendet. Trotzdem wird der Speicher nicht freigegeben.

Wenn ich nur an einer stelle Insert oder Delete MergeChangeLog ausführe wächst die Auslastung bei jedem neuen Insert.

LogChanges hilft auch nicht.

himitsu 31. Mai 2022 16:10

AW: Clientdataset Speicherfreigabe
 
Welcher Speicher wächst?

Ich tippe mal auf eine Speicherfragmentierung im FastMM.
Was sagt denn GetMemoryManagerState?



Hat ein billiges ClientDataSet überhaupt ein ChangeLog?

lxo 31. Mai 2022 16:55

AW: Clientdataset Speicherfreigabe
 
Zitat:

Zitat von himitsu (Beitrag 1506568)
Welcher Speicher wächst?

Ich tippe mal auf eine Speicherfragmentierung im FastMM.
Was sagt denn GetMemoryManagerState?



Hat ein billiges ClientDataSet überhaupt ein ChangeLog?

Die Arbeitsspeicherauslastung (Arbeitsspeicher aktiver privater Arbeitssatz), wird beim entfernen von Datensätzen nicht kleiner.



Clientdataset
  1. Programm Start - 2648K
  2. Add - 18900K
  3. Delete - 18800K
  4. Add - 33476K
  5. Delete - 33476K
  6. Add - 48792K

FDMemTable
  1. Programm Start - 2596K
  2. Add - 19648K
  3. Delete - 6852K
  4. Add - 19712K
  5. Delete - 6912K
  6. Add - 19712K

Was sagt denn GetMemoryManagerState?
- Da steht immer das selbe hättest du da ein Beispiel wie ich das richtig anwende bzw. was möchtest du da genau sehen?



Beim dxMemTable von DevExpress wird der Speicher auch freigegeben.
Wenn ich bei einer TList oder TObjectList Einträge entferne werden die auch direkt freigegeben.
Ich hab das Problem nur bei TClientDataSets

lxo 31. Mai 2022 18:22

AW: Clientdataset Speicherfreigabe
 
Liste der Anhänge anzeigen (Anzahl: 1)
Hier nochmal das komplette Testprojekt.

himitsu 31. Mai 2022 18:23

AW: Clientdataset Speicherfreigabe
 
Die scheinen den Speicher direkt bei Windows zu reservieren.

Hmmm, im FastMM steigt nichts an.
Aber im Code (Datasnap.DBClient) finde ich nur ein AllocMem, was eigentlich im Delphi-MemoryManager (FastMM) landen sollte. :gruebel:

Ich komm nach genügend Durchläufen (Add+Delete) sogar in einen OutOfMemory.


Delphi-Quellcode:
procedure TForm8.FormCreate(Sender: TObject);
var
  CDS: TClientDataSet;
  Mem: UInt64;
procedure ShowState;
  var
    GStatus: TMemoryStatusEx;
    MMState: TMemoryManagerState;
  begin
    //Memo1.Lines.Add('  Records ' + CDS.RecordCount.ToString);

    GStatus.dwLength := SizeOf(GStatus);
    GlobalMemoryStatusEx(GStatus);

    GetMemoryManagerState(MMState);
    var FastMM: Int64 := 0;
    for var i := 0 to High(MMState.SmallBlockTypeStates) do
      Inc(FastMM, MMState.SmallBlockTypeStates[i].UseableBlockSize * MMState.SmallBlockTypeStates[i].AllocatedBlockCount);
    Inc(FastMM, MMState.TotalAllocatedMediumBlockSize);
    Inc(FastMM, MMState.TotalAllocatedLargeBlockSize);

    Memo1.Lines.Add(Format('  Memory %d%% %.2nm %s%.2nm / %.2nm', [
      GStatus.dwMemoryLoad, Int64(GStatus.ullTotalVirtual - GStatus.ullAvailVirtual) / 1048576,
      IfThen(Mem < GStatus.ullAvailVirtual, '', '+'), Int64(Mem - GStatus.ullAvailVirtual) / 1048576,
      FastMM / 1048576
    ]));
    Mem := GStatus.ullAvailVirtual;
  end;
begin
  Mem := 0;
  CDS := TClientDataSet.Create(Self);
  CDS.FieldDefs.Add('TEST1', ftString, 500);
  CDS.FieldDefs.Add('TEST2', ftString, 500);
  CDS.FieldDefs.Add('TEST3', ftString, 500);
  CDS.CreateDataSet;

  for var L := 1 to 99 do
    try
      Memo1.Lines.Add(L.ToString);
      //Memo1.Lines.Add('Add');
      for var i := 1 to 9999 do begin
        CDS.Append;
        CDS.FieldByName('TEST1').AsString := Random(MaxInt).ToString;
        CDS.FieldByName('TEST2').AsString := Random(MaxInt).ToString;
        CDS.FieldByName('TEST3').AsString := Random(MaxInt).ToString;
        CDS.Post;
      end;
      ShowState;

      //Memo1.Lines.Add('Delete');
      while CDS.RecordCount > 0 do
        CDS.Delete;
      ShowState;
    except
      on E: Exception do begin
        Memo1.Lines.Add(L.ToString + ' : ' + E.Message);
        ShowState;
        Break;
      end;
    end;
end;

Uwe Raabe 1. Jun 2022 10:56

AW: Clientdataset Speicherfreigabe
 
Vielleicht ist es nicht jedem bewusst, aber die DB-Engine des TClientDataset liegt in der Midas.dll bzw. ihrem eingelinkten binären Zwilling. Damit arbeitet es schon etwas anders als die reine Delphi-Implementierung von FireDAC.

TigerLilly 1. Jun 2022 16:48

AW: Clientdataset Speicherfreigabe
 
Die ClientDataSets sind ja eigentlich für n-tier Anwendungen gemacht, hängen also an einem Provider, der die das Änderungslog (DELTA) weiterreicht. Ein DELETE löscht also nicht wirklich, sondern hebt alles auf, was notwendig ist, um die Änderung weiterreichen zu können UND um das Löschen auch rückgängig machen zu können. Siehe "CancelUpdates".
Es überrascht mich also nicht, dass Speicher alloziert bleibt.

Jedes Feld gibt es im CDS 3x: newValue/Value/oldValue - da kommt im Code unten schon (unrealistischerweise) was zusammen: 99 x 9999 x 3 x 3 x 500

Mit dem Zerstören des CDS wird dann auch der Speicher freigegeben.

Frickler 1. Jun 2022 17:32

AW: Clientdataset Speicherfreigabe
 
Einfach mal nach "CreateDataSet" die Property "LogChanges" auf false setzen.

lxo 1. Jun 2022 17:33

AW: Clientdataset Speicherfreigabe
 
Zitat:

Zitat von Frickler (Beitrag 1506646)
Einfach mal nach "CreateDataSet" die Property "LogChanges" auf false setzen.

Hab ich auch probiert.
Macht keinen Unterschied.

TigerLilly 2. Jun 2022 08:35

AW: Clientdataset Speicherfreigabe
 
ApplyUpdates + Handler für BeforeApplyUpdates sollte das Delta leeren.

CDS schließen und öffnen.

CDS zerstören + neu erzeugen.

Ein anderes MemoryDataSet nehmen.

Das Problem ignorieren - in der Praxis sollte das doch nicht stören, oder was machst du da?

himitsu 2. Jun 2022 09:23

AW: Clientdataset Speicherfreigabe
 
Hab meinen Testcode noch etwas erweitert (erstmal alles, was an Property und Methoden im ClientDataSet zu sehen war, nach dem Delete rein, gesehen es geht und dann schrittweise alles Unnütze wieder raus)
und eine Teillösung gefunden.

Es gibt den Speicher nicht komplett frei, aber zumindesten das Log schein weg zu sein.
(beim nächsten Add und Delete wird kein neuer Speicher hinzugefügt oder freigegeben)
  • LogChanges:=False half nichts
  • CancelUpdates und MergeChangeLog hilft auch nicht
  • aber EmptyDataSet nach oder besser noch anstatt dem While-Delete ist hier die Lösung
    • und es ist sogar um Längen schneller


Delphi-Quellcode:
{$OVERFLOWCHECKS OFF}

uses
  System.StrUtils, Data.DB, Datasnap.DBClient;

procedure TForm8.FormCreate(Sender: TObject);
var
  CDS: TClientDataSet;
  Mem: UInt64;
procedure ShowState;
  var
    GStatus: TMemoryStatusEx;
    MMState: TMemoryManagerState;
  begin
    //Memo1.Lines.Add(' Records ' + CDS.RecordCount.ToString);

    GStatus.dwLength := SizeOf(GStatus);
    GlobalMemoryStatusEx(GStatus);

    GetMemoryManagerState(MMState);
    var FastMM: Int64 := 0;
    for var i := 0 to High(MMState.SmallBlockTypeStates) do
      Inc(FastMM, MMState.SmallBlockTypeStates[i].UseableBlockSize * MMState.SmallBlockTypeStates[i].AllocatedBlockCount);
    Inc(FastMM, MMState.TotalAllocatedMediumBlockSize);
    Inc(FastMM, MMState.TotalAllocatedLargeBlockSize);

    Memo1.Lines.Add(Format(' Memory %d%% %.2nm %s%.2nm / %.2nm', [
      GStatus.dwMemoryLoad, Int64(GStatus.ullTotalVirtual - GStatus.ullAvailVirtual) / 1048576,
      IfThen(Mem < GStatus.ullAvailVirtual, '', '+'), Int64(Mem - GStatus.ullAvailVirtual) / 1048576,
      FastMM / 1048576
    ]));
    Mem := GStatus.ullAvailVirtual;
  end;
begin
  Mem := 0;
  CDS := TClientDataSet.Create(Self);
  CDS.FieldDefs.Add('TEST1', ftString, 500);
  CDS.FieldDefs.Add('TEST2', ftString, 500);
  CDS.FieldDefs.Add('TEST3', ftString, 500);
  CDS.CreateDataSet;
  CDS.LogChanges := False;

  for var L := 1 to 199 do
    try
      Memo1.Lines.Add(L.ToString);
      //Memo1.Lines.Add('Add');
      for var i := 1 to 9999 do begin
        CDS.Append;
        CDS.FieldByName('TEST1').AsString := Random(MaxInt).ToString;
        CDS.FieldByName('TEST2').AsString := Random(MaxInt).ToString;
        CDS.FieldByName('TEST3').AsString := Random(MaxInt).ToString;
        CDS.Post;
      end;
      ShowState;

      //Memo1.Lines.Add('Delete');
      {$IF false}
      while CDS.RecordCount > 0 do
        CDS.Delete;
      {$ELSE}
      CDS.EmptyDataSet;
      {$IFEND}
      ShowState;
    except
      on E: Exception do begin
        Memo1.Lines.Add(L.ToString + ' : ' + E.Message);
        ShowState;
        Break;
      end;
    end;
end;

lxo 2. Jun 2022 09:36

AW: Clientdataset Speicherfreigabe
 
Zitat:

Zitat von TigerLilly (Beitrag 1506680)
ApplyUpdates + Handler für BeforeApplyUpdates sollte das Delta leeren.

CDS schließen und öffnen.

CDS zerstören + neu erzeugen.

Ein anderes MemoryDataSet nehmen.

Das Problem ignorieren - in der Praxis sollte das doch nicht stören, oder was machst du da?

Ich verwende das Dataset für ein aktives Log.
Die letzten 25 Logeinträge werden dort angezeigt.
Bevor der 26.Eintrag geschrieben wird, wird der älteste Eintrag gelöscht.

Dabei ist mir das aufgefallen, obwohl ich immer nur 25 Einträge habe läuft der Speicher immer weiter voll.

himitsu 2. Jun 2022 12:20

AW: Clientdataset Speicherfreigabe
 
Wenn du nur die Datensätze überschreibst, anstatt zu löschen und neu zu erstellen, dann gibt es kein Problem.
Aber man sollte dringend LogChanges abschalten, denn erstmal bremst das ja ganz extrem und es verbrät dennoch ordentlich Speicher.

Delphi-Quellcode:
{$OVERFLOWCHECKS OFF}

uses
  System.StrUtils, Data.DB, Datasnap.DBClient;

procedure TForm8.FormCreate(Sender: TObject);
var
  CDS: TClientDataSet;
  Mem: UInt64;
procedure ShowState;
  var
    GStatus: TMemoryStatusEx;
    MMState: TMemoryManagerState;
  begin
    GStatus.dwLength := SizeOf(GStatus);
    GlobalMemoryStatusEx(GStatus);

    GetMemoryManagerState(MMState);
    var FastMM: Int64 := 0;
    for var i := 0 to High(MMState.SmallBlockTypeStates) do
      Inc(FastMM, MMState.SmallBlockTypeStates[i].UseableBlockSize * MMState.SmallBlockTypeStates[i].AllocatedBlockCount);
    Inc(FastMM, MMState.TotalAllocatedMediumBlockSize);
    Inc(FastMM, MMState.TotalAllocatedLargeBlockSize);

    Memo1.Lines.Add(Format(' Memory %d%% %.2nm %s%.2nm / %.2nm / %.1nk records / %.1nk changes', [
      GStatus.dwMemoryLoad, Int64(GStatus.ullTotalVirtual - GStatus.ullAvailVirtual) / 1048576,
      IfThen(Mem < GStatus.ullAvailVirtual, '', '+'), Int64(Mem - GStatus.ullAvailVirtual) / 1048576,
      FastMM / 1048576, CDS.RecordCount / 1000, CDS.ChangeCount / 1000
    ]));
    Mem := GStatus.ullAvailVirtual;
  end;
begin
  Mem := 0;
  CDS := TClientDataSet.Create(Self);
  CDS.FieldDefs.Add('TEST1', ftString, 500);
  CDS.FieldDefs.Add('TEST2', ftString, 500);
  CDS.FieldDefs.Add('TEST3', ftString, 500);
  CDS.CreateDataSet;
  CDS.LogChanges := False;

  for var i := 1 to 25 do begin
    CDS.Append;
    CDS.FieldByName('TEST1').AsString := Random(MaxInt).ToString;
    CDS.FieldByName('TEST2').AsString := Random(MaxInt).ToString;
    CDS.FieldByName('TEST3').AsString := Random(MaxInt).ToString;
    CDS.Post;
  end;
  ShowState;

  for var i := 1 to 100000 do
    try
      CDS.RecNo := i mod {25} CDS.RecordCount + 1;
      CDS.Edit;
      CDS.FieldByName('TEST1').AsString := Random(MaxInt).ToString;
      CDS.FieldByName('TEST2').AsString := Random(MaxInt).ToString;
      CDS.FieldByName('TEST3').AsString := Random(MaxInt).ToString;
      CDS.Post;
      if i mod 5000 = 0 then
        ShowState;
    except
      on E: Exception do begin
        Memo1.Lines.Add(i.ToString + ' : ' + E.Message);
        ShowState;
        Break;
      end;
    end;
  ShowState;
end;

TigerLilly 2. Jun 2022 12:24

AW: Clientdataset Speicherfreigabe
 
Vielleicht ist ein CDS auch nicht optimal für die Aufgabenstellung. Vielleicht wäre eine simple Liste passender? Oder wenn du es visualisiert haben willst, nimm eine TListBox + füttere dort jeweils 5 Items.

lxo 2. Jun 2022 17:03

AW: Clientdataset Speicherfreigabe
 
Zitat:

Zitat von TigerLilly (Beitrag 1506691)
Vielleicht ist ein CDS auch nicht optimal für die Aufgabenstellung. Vielleicht wäre eine simple Liste passender? Oder wenn du es visualisiert haben willst, nimm eine TListBox + füttere dort jeweils 5 Items.

Das kann gut sein, ich wollte nur gerne auch den Hintergrund wissen, wieso sich das TClientDataSet so verhält.
Hätte ich nicht erwartet, da auch andere DataSets von FireDAC oder DevExpress z.B. sich nicht so verhalten.

TigerLilly 6. Jun 2022 16:45

AW: Clientdataset Speicherfreigabe
 
Naja, DevEx und Firedac sind reine InMemory-Datasets + konsolidieren die Änderunggen eben nicht in einem Delta für n-Tier.


Alle Zeitangaben in WEZ +1. Es ist jetzt 23:56 Uhr.

Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz