{
 *****************************************************************************
  This file is part of LazUtils.

  See the file COPYING.modifiedLGPL.txt, included in this distribution,
  for details about the license.
 *****************************************************************************
}
unit LazLoggerProfiling;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils,
  // LazUtils
  LazLoggerBase, LazSysUtils, LazUTF8;

type

  { TLazLoggerBlockTimer }

  TLazLoggerBlockTimer = class(TLazLoggerBlockHandler)
  private
    FMaxDepth: Integer;
    FTimes: Array of QWord;
    FNested: Array of QWord;
    function GetNested(ALevel: Integer): QWord;
    function GetTimeDiff(ALevel: Integer): QWord;
    procedure SetMaxDepth(AValue: Integer);
  public
    constructor Create;
    procedure EnterBlock({%H-}Sender: TLazLogger; Level: Integer); override;
    procedure ExitBlock({%H-}Sender: TLazLogger; Level: Integer); override;
    property  MaxDepth: Integer read FMaxDepth write SetMaxDepth;
    property  TimeDiff[ALevel: Integer]: QWord read GetTimeDiff;
    property  Nested[ALevel: Integer]: QWord read GetNested;
  end;

  { TLazLoggerBlockMemWatch }

  TLazLoggerBlockMemWatch = class(TLazLoggerBlockHandler)
  private
    FMaxDepth: Integer;
    FMem: Array of Int64;
    FNested: Array of Int64;
    function GetMemDiff(ALevel: Integer): Int64;
    function GetNested(ALevel: Integer): Int64;
    procedure SetMaxDepth(AValue: Integer);
  public
    constructor Create;
    procedure EnterBlock({%H-}Sender: TLazLogger; Level: Integer); override;
    procedure ExitBlock({%H-}Sender: TLazLogger; Level: Integer); override;
    property  MaxDepth: Integer read FMaxDepth write SetMaxDepth;
    property  MemDiff[ALevel: Integer]: Int64 read GetMemDiff;
    property  Nested[ALevel: Integer]: Int64 read GetNested;
  end;

// %0:s Current block, since enter
// %1:s Sum of Nested blocks frame
// %2:s Parent block, since enter
// %4:s Sum of Nested blocks in parent frame
function DbgsMemUsed(AFormat: String = '%0:d'): string;
function DbgsTimeUsed(AFormat: String = '%0:n'): string;

procedure DbgStartTimer(AName: String);
procedure DbgStopTimer(AName: String);
procedure DbgStartMemWatch(AName: String);
procedure DbgStopMemWatch(AName: String);

function DbgsMemUsed(AFormat: String; AName: String): string;
function DbgsTimeUsed(AFormat: String; AName: String): string;

implementation

var
  NamedMemWatches: TStringListUTF8Fast = nil;
  NamedTimer: TStringListUTF8Fast = nil;
  NamedMemWatchesData: Array of record Sum, Last: Int64; end;
  NamedTimerData: array of record Sum, Last: QWord; end;


function GetMemWatch: TLazLoggerBlockMemWatch;
var
  i: Integer;
begin
  Result := nil;
  i := DebugLogger.BlockHandlerCount - 1;
  while (i >= 0) and (Result = nil) do
    if DebugLogger.BlockHandler[i] is TLazLoggerBlockMemWatch then
      Result := DebugLogger.BlockHandler[i] as TLazLoggerBlockMemWatch
    else
      dec(i);
end;

function GetTimer: TLazLoggerBlockTimer;
var
  i: Integer;
begin
  Result := nil;
  i := DebugLogger.BlockHandlerCount - 1;
  while (i >= 0) and (Result = nil) do
    if DebugLogger.BlockHandler[i] is TLazLoggerBlockTimer then
      Result := DebugLogger.BlockHandler[i] as TLazLoggerBlockTimer
    else
      dec(i);
end;

function DbgsMemUsed(AFormat: String): string;
var
  l: TLazLoggerBlockMemWatch;
  i: Integer;
begin
  Result := '';
  i := DebugLogger.CurrentIndentLevel;
  l := GetMemWatch;
  if l = nil then exit;
  try
    Result := Format(AFormat, [l.MemDiff[i], l.Nested[i], l.MemDiff[i-1], l.Nested[i-1]]);
  except
    Result := Format('%0:d %1:d', [l.MemDiff[i], l.Nested[i], l.MemDiff[i-1], l.Nested[i-1]]);
  end;
end;

function DbgsTimeUsed(AFormat: String): string;
var
  l: TLazLoggerBlockTimer;
  i: Integer;
begin
  Result := '';
  i := DebugLogger.CurrentIndentLevel;
  l := GetTimer;
  if l = nil then exit;
  try
    Result := Format(AFormat, [l.TimeDiff[i]/1000, l.Nested[i]/1000, l.TimeDiff[i-1]/1000, l.Nested[i-1]/1000]);
  except
    Result := Format('%0:n %1:n', [l.TimeDiff[i]/1000, l.Nested[i]/1000, l.TimeDiff[i-1]/1000, l.Nested[i-1]/1000]);
  end;
end;

procedure DbgStartTimer(AName: String);
var
  idx: Integer;
begin
  if NamedTimer = nil then begin
    NamedTimer := TStringListUTF8Fast.Create;
    NamedTimer.Sorted := True;
    NamedTimer.Duplicates := dupError;
  end;
  idx := NamedTimer.IndexOf(AName);
  if idx < 0 then begin
    idx := NamedTimer.AddObject(AName, TObject(length(NamedTimerData)));
    SetLength(NamedTimerData, length(NamedTimerData) + 1);
    NamedTimerData[length(NamedTimerData)-1].Sum := 0;
  end;
  idx := PtrInt(NamedTimer.Objects[idx]);
  NamedTimerData[idx].Last := GetTickCount64;
end;

procedure DbgstopTimer(AName: String);
var
  idx: Integer;
  t: QWord;
begin
  if NamedTimer = nil then exit;
  idx := NamedTimer.IndexOf(AName);
  if idx < 0 then exit;
  idx := PtrInt(NamedTimer.Objects[idx]);
  t := GetTickCount64;
  if t >= NamedTimerData[idx].Last then
    t := t - NamedTimerData[idx].Last
  else // timer overflow
    t := high(t) - NamedTimerData[idx].Last + t;
  NamedTimerData[idx].Sum := NamedTimerData[idx].Last + t;
end;

procedure DbgStartMemWatch(AName: String);
var
  idx: Integer;
begin
  if NamedMemWatches = nil then begin
    NamedMemWatches := TStringListUTF8Fast.Create;
    NamedMemWatches.Sorted := True;
    NamedMemWatches.Duplicates := dupError;
  end;
  idx := NamedMemWatches.IndexOf(AName);
  if idx < 0 then begin
    idx := NamedMemWatches.AddObject(AName, TObject(length(NamedMemWatchesData)));
    SetLength(NamedMemWatchesData, length(NamedMemWatchesData) + 1);
    NamedMemWatchesData[length(NamedMemWatchesData)-1].Sum := 0;
  end;
  idx := PtrInt(NamedMemWatches.Objects[idx]);
  NamedMemWatchesData[idx].Last := GetHeapStatus.TotalAllocated;
end;

procedure DbgStopMemWatch(AName: String);
var
  idx: Integer;
begin
  if NamedMemWatches = nil then exit;
  idx := NamedMemWatches.IndexOf(AName);
  if idx < 0 then exit;
  idx := PtrInt(NamedMemWatches.Objects[idx]);
  NamedMemWatchesData[idx].Sum := NamedMemWatchesData[idx].Sum + (GetHeapStatus.TotalAllocated - NamedMemWatchesData[idx].Last);
  NamedMemWatchesData[idx].Last := 0;
end;

function DbgsMemUsed(AFormat: String; AName: String): string;
var
  idx: Integer;
begin
  Result := '';
  if NamedMemWatches = nil then exit;
  idx := NamedMemWatches.IndexOf(AName);
  if idx < 0 then exit;
  idx := PtrInt(NamedMemWatches.Objects[idx]);
  try
    Result := Format(AFormat, [NamedMemWatchesData[idx].Sum]);
  except
    Result := Format('%d', [NamedMemWatchesData[idx].Sum]);
  end;
end;

function DbgsTimeUsed(AFormat: String; AName: String): string;
var
  idx: Integer;
begin
  Result := '';
  if NamedTimer = nil then exit;
  idx := NamedTimer.IndexOf(AName);
  if idx < 0 then exit;
  idx := PtrInt(NamedTimer.Objects[idx]);
  try
    Result := Format(AFormat, [NamedTimerData[idx].Sum/1000]);
  except
    Result := Format('%n', [NamedTimerData[idx].Sum /1000]);
  end;
end;

{ TLazLoggerBlockMemWatch }

procedure TLazLoggerBlockMemWatch.SetMaxDepth(AValue: Integer);
begin
  if FMaxDepth = AValue then Exit;
  SetLength(FMem, AValue+1);
  SetLength(FNested, AValue);

  if (FMaxDepth = 0) and (AValue > 0) then begin
    FMem[0] := GetHeapStatus.TotalAllocated;
    FNested[0] := 0;
  end;

  FMaxDepth := AValue;
end;

function TLazLoggerBlockMemWatch.GetMemDiff(ALevel: Integer): Int64;
var
  t: Int64;
begin
  Result := 0;
  if (ALevel < 0) or (ALevel >= FMaxDepth + 1) then exit;
  t := GetHeapStatus.TotalAllocated;
  Result := t - FMem[ALevel];
end;

function TLazLoggerBlockMemWatch.GetNested(ALevel: Integer): Int64;
begin
  Result := 0;
  if (ALevel < 0) or (ALevel >= FMaxDepth) then exit;
  Result := FNested[ALevel];
end;

constructor TLazLoggerBlockMemWatch.Create;
begin
  inherited;
  MaxDepth := 100;
  FMem[0] := GetHeapStatus.TotalAllocated;
  FNested[0] := 0;
end;

procedure TLazLoggerBlockMemWatch.EnterBlock(Sender: TLazLogger; Level: Integer);
begin
  if (Level < 0) or (Level >= FMaxDepth + 1) then exit;
  FMem[Level] := GetHeapStatus.TotalAllocated;
  if (Level >= FMaxDepth) then exit;
  FNested[Level] := 0;
end;

procedure TLazLoggerBlockMemWatch.ExitBlock(Sender: TLazLogger; Level: Integer);
begin
  if (Level < 1) or (Level >= FMaxDepth + 1) then exit;
  FMem[Level - 1] := FNested[Level - 1] + GetMemDiff(Level);
end;

{ TLazLoggerBlockTimer }

procedure TLazLoggerBlockTimer.SetMaxDepth(AValue: Integer);
begin
  if FMaxDepth = AValue then Exit;
  SetLength(FTimes, AValue+1);
  SetLength(FNested, AValue);

  if (FMaxDepth = 0) and (AValue > 0) then begin
    FTimes[0] := GetTickCount64;
    FNested[0] := 0;
  end;

  FMaxDepth := AValue;
end;

function TLazLoggerBlockTimer.GetTimeDiff(ALevel: Integer): QWord;
var
  t: QWord;
begin
  Result := 0;
  if (ALevel < 0) or (ALevel >= FMaxDepth + 1) then exit;
  t := GetTickCount64;
  if t >= FTimes[ALevel] then
    Result := t - FTimes[ALevel]
  else // timer overflow
    Result := high(t) - FTimes[ALevel] + t;
end;

function TLazLoggerBlockTimer.GetNested(ALevel: Integer): QWord;
begin
  Result := 0;
  if (ALevel < 0) or (ALevel >= FMaxDepth) then exit;
  Result := FNested[ALevel];
end;

constructor TLazLoggerBlockTimer.Create;
begin
  inherited;
  MaxDepth := 100;
  FTimes[0] := GetTickCount64;
  FNested[0] := 0;
end;

procedure TLazLoggerBlockTimer.EnterBlock(Sender: TLazLogger; Level: Integer);
begin
  if (Level < 0) or (Level >= FMaxDepth + 1) then exit;
  FTimes[Level] := GetTickCount64;
  if (Level >= FMaxDepth) then exit;
  FNested[Level] := 0;
end;

procedure TLazLoggerBlockTimer.ExitBlock(Sender: TLazLogger; Level: Integer);
begin
  if (Level < 1) or (Level >= FMaxDepth + 1) then exit;
  FNested[Level - 1] := FNested[Level - 1] + GetTimeDiff(Level);
end;

initialization
  DebugLogger.AddBlockHandler(TLazLoggerBlockTimer.Create);
  DebugLogger.AddBlockHandler(TLazLoggerBlockMemWatch.Create);

finalization
  FreeAndNil(NamedTimer);
  FreeAndNil(NamedMemWatches);
  NamedTimerData := nil;
  NamedMemWatchesData := nil;

end.

