unit FastMM4DataCollector;

{$I FastMM4Options.inc}

interface

type
  TStaticCollector = record
  strict private const
    CDefaultPromoteGen1_sec  = 1; // promote every second
    CDefaultPromoteGen1Count = 1; // promote allocations with Count > 1
    CGeneration1Size = 1024;
    CGeneration2Size = 256;
    CCollectedDataSize = CGeneration2Size;
    CMaxPointers = 11; // same as in FastMM4
  public type
    TPointers = record
      Pointers: array [1..CMaxPointers] of Pointer;
      Count   : integer;
      class operator Equal(const a, b: TPointers): boolean;
    end;
    TDataInfo = record
      Data : TPointers;
      Count: integer;
    end;
    TCollectedData = array [1..CCollectedDataSize] of TDataInfo;
    TGenerationOverflowCount = record
      Generation1: integer;
      Generation2: integer;
    end;
  strict private type
    PDataInfo = ^TDataInfo;
    TGenerationPlaceholder = array [1..1] of TDataInfo;
    PGenerationPlaceholder = ^TGenerationPlaceholder;
    TGenerationInfo = record
      Data            : PGenerationPlaceholder;
      Size            : integer;
      Last            : integer;
      NextGeneration  : integer;
      PromoteEvery_sec: integer;
      PromoteCountOver: integer;
      OverflowCount   : integer;
      LastCheck_ms    : int64;
    end;
  var
    FGeneration1   : array [1..CGeneration1Size] of TDataInfo;
    FGeneration2   : array [1..CGeneration2Size] of TDataInfo;
    FGenerationInfo: array [0..2] of TGenerationInfo; //gen0 is used for merging
    FLocked        : ByteBool;
    FPadding       : array [1..3] of byte;
    function GetGen1_PromoteCountOver: integer;
    function GetGen1_PromoteEvery_sec: integer;
    function GetOverflowCount: TGenerationOverflowCount;
    procedure Lock;
    function Now_ms: int64; inline;
    procedure SetGen1_PromoteCountOver(const value: integer);
    procedure SetGen1_PromoteEvery_sec(const value: integer);
  private
    procedure AddToGeneration(generation: integer; const aData: TPointers;
      count: integer = 1);
    procedure CheckPromoteGeneration(generation: integer); inline;
    function FindInGeneration(generation: integer; const aData: TPointers): integer; inline;
    function FindInsertionPoint(generation, count: integer): integer; inline;
    procedure FlushAllGenerations;
    function InsertIntoGeneration(generation: integer; const dataInfo: TDataInfo): boolean;
    procedure PromoteGeneration(oldGen, newGen: integer);
    procedure ResortGeneration(generation, idxData: integer);
  public
    procedure Initialize;
    procedure Add(const pointers: pointer; count: integer);
    procedure GetData(var data: TCollectedData; var count: integer);
    procedure Merge(var mergedData: TCollectedData; var mergedCount: integer;
      const newData: TCollectedData; newCount: integer);
    property Gen1_PromoteCountOver: integer read GetGen1_PromoteCountOver
      write SetGen1_PromoteCountOver;
    property OverflowCount: TGenerationOverflowCount read GetOverflowCount;
    property Gen1_PromoteEvery_sec: integer read GetGen1_PromoteEvery_sec write
      SetGen1_PromoteEvery_sec;
  end;
  PStaticCollector = ^TStaticCollector;

implementation

uses
  Winapi.Windows; //used in Now_ms

{$RANGECHECKS OFF}

type
  PByteBool = ^ByteBool;

// Copied from FastMM4.pas
function LockCmpxchg8(CompareVal, NewVal: ByteBool; AAddress: PByteBool): ByteBool;
asm
{$if SizeOf(Pointer) = 4}
  {On entry:
    al = CompareVal,
    dl = NewVal,
    ecx = AAddress}
  {$ifndef LINUX}
  lock cmpxchg [ecx], dl
  {$else}
  {Workaround for Kylix compiler bug}
  db $F0, $0F, $B0, $11
  {$endif}
{$else}
  {On entry:
    cl = CompareVal
    dl = NewVal
    r8 = AAddress}
  .noframe
  mov rax, rcx
  lock cmpxchg [r8], dl
{$ifend}
end;

{ TStaticCollector.TPointers }

class operator TStaticCollector.TPointers.Equal(const a, b: TPointers): boolean;
var
  i: integer;
begin
  Result := a.Count = b.Count;
  if Result then
    for i := 1 to a.Count do
      if a.Pointers[i] <> b.Pointers[i] then
        Exit(false);
end;

{ TStaticCollector }

procedure TStaticCollector.Add(const pointers: pointer; count: integer);
var
  ptrData: TPointers;
begin
  Lock;
  ptrData.Count := CMaxPointers;
  if count < CMaxPointers then
    ptrData.Count := count;
  Move(pointers^, ptrData.Pointers[1], ptrData.Count * SizeOf(pointer));
  AddToGeneration(1, ptrData);
  FLocked := false;
end;

procedure TStaticCollector.AddToGeneration(generation: integer; const aData: TPointers;
  count: integer = 1);
var
  dataInfo: TDataInfo;
  idxData : integer;
begin
  CheckPromoteGeneration(generation);

  with FGenerationInfo[generation] do begin
    idxData := FindInGeneration(generation, aData);
    if idxData >= 1 then begin
      Data^[idxData].Count := Data^[idxData].Count + count;
      ResortGeneration(generation, idxData);
    end
    else begin
      dataInfo.Data := aData;
      dataInfo.Count := count;
      InsertIntoGeneration(generation, dataInfo);
    end;
  end;
end; { TStaticCollector.AddToGeneration }

procedure TStaticCollector.CheckPromoteGeneration(generation: integer);
begin
  with FGenerationInfo[generation] do begin
    if NextGeneration > 0 then begin
      if LastCheck_ms = 0 then
        LastCheck_ms := Now_ms
      else if ((Now_ms - LastCheck_ms) div 1000) >= PromoteEvery_sec then begin
        PromoteGeneration(generation, NextGeneration);
        LastCheck_ms := Now_ms;
      end;
    end;
  end;
end;

function TStaticCollector.FindInGeneration(generation: integer; const aData: TPointers):
  integer;
begin
  with FGenerationInfo[generation] do begin
    for Result := 1 to Last do
      if Data^[Result].Data = aData then
        Exit;
  end;
  Result := 0;
end;

function TStaticCollector.FindInsertionPoint(generation, count: integer): integer;
var
  insert: integer;
begin
  with FGenerationInfo[generation] do begin
    for insert := Last downto 1 do begin
      if Data^[insert].Count > count then
        Exit(insert+1);
    end;
    Result := 1;
  end;
end;

procedure TStaticCollector.FlushAllGenerations;
var
  generation: integer;
  nextGen   : integer;
begin
  generation := 1;
  while generation <> 0 do begin
    nextGen := FGenerationInfo[generation].NextGeneration;
    if nextGen > 0 then
      PromoteGeneration(generation, nextGen);
    generation := nextGen;
  end;
end;

procedure TStaticCollector.GetData(var data: TCollectedData; var count: integer);
begin
  Lock;
  FlushAllGenerations;
  Assert(Length(data) = Length(FGeneration2));
  count := FGenerationInfo[2].Last;
  Move(FGeneration2[1], data[1], count * SizeOf(data[1]));
  FLocked := false;
end;

function TStaticCollector.GetGen1_PromoteCountOver: integer;
begin
  Result := FGenerationInfo[1].PromoteCountOver;
end;

function TStaticCollector.GetGen1_PromoteEvery_sec: integer;
begin
  Result := FGenerationInfo[1].PromoteEvery_sec;
end;

function TStaticCollector.GetOverflowCount: TGenerationOverflowCount;
begin
  Result.Generation1 := FGenerationInfo[1].OverflowCount;
  Result.Generation2 := FGenerationInfo[2].OverflowCount;
end;

procedure TStaticCollector.Initialize;
begin
  Assert(SizeOf(TStaticCollector) mod SizeOf(pointer) = 0);
  with FGenerationInfo[1] do begin
    Data := PGenerationPlaceholder(@FGeneration1);
    Size := CGeneration1Size;
    Last := 0;
    NextGeneration := 2;
    PromoteEvery_sec := CDefaultPromoteGen1_sec;
    PromoteCountOver := CDefaultPromoteGen1Count;
    LastCheck_ms := 0;
  end;
  with FGenerationInfo[2] do begin
    Data := PGenerationPlaceholder(@FGeneration2);
    Size := CGeneration2Size;
    NextGeneration := 0;
  end;
end;

function TStaticCollector.InsertIntoGeneration(generation: integer; const dataInfo:
  TDataInfo): boolean;
var
  idx: integer;
begin
  // We already know that this element does not exist in the generation.

  Result := true;
  with FGenerationInfo[generation] do begin
    idx := FindInsertionPoint(generation, dataInfo.Count);
    if idx > Last then begin
      if Last = Size then begin
        Inc(OverflowCount);
        Result := false;
      end
      else begin
        Inc(Last);
        Data^[Last] := dataInfo;
      end;
    end
    else begin
      if Last < Size then begin
        Move(Data^[idx], Data^[idx+1], (Last-idx+1) * SizeOf(Data^[idx]));
        Inc(Last);
      end
      else begin
        if Last > idx then
          Move(Data^[idx], Data^[idx+1], (Last-idx) * SizeOf(Data^[idx]));
        Inc(OverflowCount);
      end;
      Data^[idx] := dataInfo;
    end;
  end;
end;

procedure TStaticCollector.Lock;
begin
{$ifndef AssumeMultiThreaded}
  if IsMultiThread then
{$endif}
  begin
    while LockCmpxchg8(False, True, @FLocked) <> False do
    begin
{$ifdef NeverSleepOnThreadContention}
  {$ifdef UseSwitchToThread}
      SwitchToThread;
  {$endif}
{$else}
      Sleep(0);
      if LockCmpxchg8(False, True, @FLocked) = False then
        Break;
      Sleep(1);
{$endif}
    end;
  end;
end;

procedure TStaticCollector.Merge(var mergedData: TCollectedData;
  var mergedCount: integer; const newData: TCollectedData; newCount: integer);
var
  iNew: integer;
begin
  // Merges two sorted arrays.

  FGenerationInfo[0].Data := PGenerationPlaceholder(@mergedData);
  FGenerationInfo[0].Last := mergedCount;
  FGenerationInfo[0].Size := CCollectedDataSize;
  FGenerationInfo[0].NextGeneration := 0;

  for iNew := 1 to newCount do
    AddToGeneration(0, newData[iNew].Data, newData[iNew].Count);

  mergedCount := FGenerationInfo[0].Last;
end;

function TStaticCollector.Now_ms: int64;
var
  st: TSystemTime;
begin
  // We cannot use SysUtils as that gets memory allocator called before FastMM is initialized.
  GetSystemTime(st);
  SystemTimeToFileTime(st, TFileTime(Result));
  Result := Result div 10000;
end;

procedure TStaticCollector.PromoteGeneration(oldGen, newGen: integer);
var
  canInsert : boolean;
  idxNew    : integer;
  idxOld    : integer;
  newGenData: PGenerationPlaceholder;
  pOldData  : PDataInfo;
begin
  canInsert := true;
  newGenData := FGenerationInfo[newGen].Data;
  with FGenerationInfo[oldGen] do begin
    for idxOld := 1 to Last do begin
      pOldData := @Data^[idxOld];
      if pOldData^.Count <= PromoteCountOver then
        break; //for idxOld
      idxNew := FindInGeneration(newGen, pOldData^.Data);
      if idxNew > 0 then begin
        newGenData^[idxNew].Count := newGenData^[idxNew].Count + pOldData^.Count;
        ResortGeneration(newGen, idxNew);
      end
      else if canInsert then
        canInsert := InsertIntoGeneration(newGen, pOldData^)
      else with FGenerationInfo[newGen] do
        Inc(OverflowCount);
    end; //for idxOld
    Last := 0;
  end;
end;

procedure TStaticCollector.ResortGeneration(generation, idxData: integer);
var
  dataInfo: TDataInfo;
  idx     : integer;
begin
  // Data^[idxData].Count was just updated, resort the generation.
  with FGenerationInfo[generation] do begin
    idx := FindInsertionPoint(generation, Data^[idxData].Count);
    if idx < idxData then begin
      dataInfo := Data^[idxData];
      Move(Data^[idx], Data^[idx+1], (idxData-idx) * SizeOf(Data^[idx]));
      Data^[idx] := dataInfo;
    end;
  end;
end;

procedure TStaticCollector.SetGen1_PromoteCountOver(const value: integer);
begin
  FGenerationInfo[1].PromoteCountOver := value;
end;

procedure TStaticCollector.SetGen1_PromoteEvery_sec(const value: integer);
begin
  FGenerationInfo[1].PromoteEvery_sec := value;
end;

end.
