xtool/contrib/fundamentals/Utils/flcTimers.pas

846 lines
25 KiB
ObjectPascal

{******************************************************************************}
{ }
{ Library: Fundamentals 5.00 }
{ File name: flcTimers.pas }
{ File version: 5.20 }
{ Description: Timer functions }
{ }
{ Copyright: Copyright (c) 1999-2020, David J Butler }
{ All rights reserved. }
{ Redistribution and use in source and binary forms, with }
{ or without modification, are permitted provided that }
{ the following conditions are met: }
{ Redistributions of source code must retain the above }
{ copyright notice, this list of conditions and the }
{ following disclaimer. }
{ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND }
{ CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED }
{ WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED }
{ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A }
{ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL }
{ THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, }
{ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR }
{ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, }
{ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF }
{ USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) }
{ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER }
{ IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING }
{ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE }
{ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE }
{ POSSIBILITY OF SUCH DAMAGE. }
{ }
{ Github: https://github.com/fundamentalslib }
{ E-mail: fundamentals.library at gmail.com }
{ }
{ Revision history: }
{ }
{ 1999/11/10 0.01 Initial version. }
{ 2005/08/19 4.02 Compilable with FreePascal 2.0.1 }
{ 2005/08/26 4.03 Improvements to timer functions. }
{ 2005/08/27 4.04 Revised for Fundamentals 4. }
{ 2007/06/08 4.05 Compilable with FreePascal 2.04 Win32 i386 }
{ 2009/10/09 4.06 Compilable with Delphi 2009 Win32/.NET. }
{ 2010/06/27 4.07 Compilable with FreePascal 2.4.0 OSX x86-64 }
{ 2011/05/04 4.08 Split from cDateTime unit. }
{ 2015/01/16 4.09 Added GetTickAccuracy. }
{ 2015/01/17 5.10 Revised for Fundamentals 5. }
{ 2018/07/17 5.11 Word32 changes. }
{ 2019/04/02 5.12 Compilable with Delphi 10.2 Linux64. }
{ 2019/06/08 5.13 Use CLOCK_MONOTONIC on Delphi Posix. }
{ 2019/06/08 5.14 Add GetTick64. }
{ 2019/10/06 5.15 MicroTick functions. }
{ 2020/01/28 5.16 MilliTick and MicroDateTime functions. }
{ 2020/01/31 5.17 Use DateTime implementations on iOS. }
{ 2020/03/10 5.18 Use MachAbsoluteTime on OSX. }
{ 2020/03/22 5.19 Use maximum resolution on OSX and Delphi Posix. }
{ Add high resolution tick functions. }
{ 2020/05/20 5.20 Use MachAbsoluteTime on iOS. }
{ Remove legacy tick timer, use MilliTick timer for same }
{ functionality. }
{ Remote legacy HPTimer, use HighResolutionTick for same }
{ functionality. }
{ Add GetMicroDateTimeNowUT and GetMicroDateTimeNowUTC. }
{ }
{ Supported compilers: }
{ }
{ Delphi 2010-10.4 Win32/Win64 5.20 2020/06/02 }
{ Delphi 10.2-10.4 Linux64 5.20 2020/06/02 }
{ FreePascal 3.0.4 Win64 5.20 2020/06/02 }
{ }
{******************************************************************************}
{$INCLUDE ..\flcInclude.inc}
{$IFDEF DEBUG}
{$IFDEF TEST}
{$DEFINE TIMERS_TEST}
{$ENDIF}
{$ENDIF}
unit flcTimers;
interface
uses
{ System }
SysUtils,
{ Fundamentals }
flcStdTypes;
{ }
{ Error }
{ }
type
ETimerError = class(Exception);
{ }
{ High resolution tick }
{ GetHighResolutionFrequency returns the resolution of the high resolution }
{ timer in units per second. }
{ }
function GetHighResolutionTick: Word64;
function GetHighResolutionFrequency: Word64;
function HighResolutionTickDelta(const T1, T2: Word64): Int64;
function HighResolutionTickDeltaU(const T1, T2: Word64): Word64;
{ }
{ MicroTick }
{ Timer in microsecond units, based on high resolution timer if available. }
{ }
const
MicroTickFrequency = 1000000;
function GetMicroTick: Word64;
function MicroTickDelta(const T1, T2: Word64): Int64;
function MicroTickDeltaU(const T1, T2: Word64): Word64;
{ }
{ MilliTick }
{ Timer in millisecond units, based on high resolution timer if available. }
{ }
const
MilliTickFrequency = 1000;
function GetMilliTick: Word64;
function MilliTickDelta(const T1, T2: Word64): Int64;
function MilliTickDeltaU(const T1, T2: Word64): Word64;
{ }
{ MicroDateTime }
{ Represents a TDateTime in microsecond units. }
{ }
function DateTimeToMicroDateTime(const DT: TDateTime): Word64;
function MicroDateTimeToDateTime(const DT: Word64): TDateTime;
{ }
{ GetMicroDateTimeNow }
{ Returns current system date/time as a MicroDateTime. }
{ }
function GetMicroDateTimeNow: Word64;
{ }
{ GetMicroDateTimeNowUT }
{ Returns current UT date/time as a MicroDateTime. }
{ }
function GetNowUT: TDateTime;
function GetMicroDateTimeNowUT: Word64;
{ }
{ GetMicroDateTimeNowUTC }
{ Returns current UT date/time as a MicroDateTime using a cached start }
{ time to speed up calculation and ensure monotonic values. }
{ The UTC version may drift from the uncached UT version. }
{ If ReInit is True, the cache start time is resynchronised with UT time }
{ from the system clock. }
{ }
function GetNowUTC(const ReInit: Boolean = False): TDateTime;
function GetMicroDateTimeNowUTC(const ReInit: Boolean = False): Word64;
{ }
{ Tests }
{ }
{$IFDEF TIMERS_TEST}
procedure Test;
{$ENDIF}
implementation
{$IFDEF WindowsPlatform}
uses
{ System }
Windows,
DateUtils;
{$ENDIF}
{$IFDEF FREEPASCAL}
{$IFDEF POSIX}
uses
{ System }
BaseUnix,
Unix,
System.DateUtils;
{$ENDIF}
{$ENDIF}
{$IFDEF DELPHI}
{$IFDEF POSIX}
uses
{ System }
Posix.Time,
{$IFDEF MACOS}
Macapi.CoreServices,
{$ENDIF}
{$IFDEF IOS}
Macapi.Mach,
{$ENDIF}
System.DateUtils;
{$ENDIF}
{$ENDIF}
// Turn off overflow checking.
// Most functions rely on overflow checking off to correctly handle counters that wrap around.
{$IFOPT Q+}{$DEFINE QOn}{$ELSE}{$UNDEF QOn}{$ENDIF}{$Q-}
{ }
{ High resolution counters }
{ }
{ Windows }
{$IFDEF WindowsPlatform}
{$DEFINE Defined_GetHighResolutionCounter}
const
SHighResCounterError = 'High resolution counter error';
var
HighResolutionCounterInit : Boolean = False;
HighResolutionFrequency : Word64 = 0;
HighResolutionMillisecondFactor : Word64 = 0;
HighResolutionMicrosecondFactor : Word64 = 0;
function CPUClockFrequency: Word64;
var
Freq : Windows.TLargeInteger;
begin
Freq := 0;
if not QueryPerformanceFrequency(Freq) then
raise ETimerError.Create(SHighResCounterError);
if Freq = 0 then
raise ETimerError.Create(SHighResCounterError);
Result := Word64(Freq);
end;
procedure InitHighResolutionCounter;
var
Freq : Word64;
begin
Freq := CPUClockFrequency;
HighResolutionFrequency := Freq;
HighResolutionMillisecondFactor := Freq div 1000;
HighResolutionMicrosecondFactor := Freq div 1000000;
if HighResolutionMicrosecondFactor = 0 then
raise ETimerError.Create(SHighResCounterError);
HighResolutionCounterInit := True;
end;
function GetHighResolutionCounter: Word64;
var
Ctr : Windows.TLargeInteger;
begin
if not HighResolutionCounterInit then
InitHighResolutionCounter;
Ctr := 0;
if not QueryPerformanceCounter(Ctr) then
raise ETimerError.Create(SHighResCounterError);
Result := Word64(Ctr);
end;
{$ENDIF}
{ Delphi Posix; excluding IOS and OSX }
{$IFDEF DELPHI}
{$IFDEF POSIX}
{$IFNDEF IOS}
{$IFNDEF MACOS}
{$DEFINE Defined_GetHighResolutionCounter}
const
HighResolutionFrequency = Word64(1000000000);
HighResolutionMillisecondFactor = 1000000;
HighResolutionMicrosecondFactor = 1000;
function GetHighResolutionCounter: Word64;
var
TimeVal : timespec;
Ticks64 : Word64;
begin
clock_gettime(CLOCK_MONOTONIC, @TimeVal);
Ticks64 := Word64(Word64(TimeVal.tv_sec) * Word64(1000000000));
Ticks64 := Word64(Ticks64 + Word64(TimeVal.tv_nsec));
Result := Ticks64;
end;
{$ENDIF}
{$ENDIF}
{$ENDIF}
{$ENDIF}
{ Delphi OSX/iOS }
// Apple recommends to use the equivalent clock_gettime_nsec_np(CLOCK_UPTIME_RAW) in nanoseconds.
{$IFDEF DELPHI}
{$IFDEF MACOS}
const
{$IFDEF UNDERSCOREIMPORTNAME}
_PU = '_';
{$ELSE}
_PU = '';
{$ENDIF}
const
LibcLib = '/usr/lib/libc.dylib';
function MachAbsoluteTime: UInt64; cdecl external LibcLib name _PU + 'mach_absolute_time';
{$DEFINE Defined_GetHighResolutionCounter}
const
HighResolutionFrequency = Word64(1000000000);
HighResolutionMillisecondFactor = 1000000;
HighResolutionMicrosecondFactor = 1000;
function GetHighResolutionCounter: Word64;
var
Ticks64 : Word64;
begin
Ticks64 := Word64(AbsoluteToNanoseconds(MachAbsoluteTime));
Result := Ticks64;
end;
{$ENDIF}
{$ENDIF}
{ FreePascal Posix }
{$IFDEF FREEPASCAL}
{$IFDEF POSIX}
{$DEFINE Defined_GetHighResolutionCounter}
const
HighResolutionFrequency = 1000000;
HighResolutionMillisecondFactor = 1000;
HighResolutionMicrosecondFactor = 1;
function GetHighResolutionCounter: Word64;
var
TV : TTimeVal;
TZ : PTimeZone;
Ticks64 : Word64;
begin
TZ := nil;
fpGetTimeOfDay(@TV, TZ);
Ticks64 := Word64(Word64(TV.tv_sec) * 1000000);
Ticks64 := Word64(Ticks64 + Word64(TV.tv_usec));
Result := Ticks64;
end;
{$ENDIF}
{$ENDIF}
{ Default implementation using system time }
{$IFNDEF Defined_GetHighResolutionCounter}
{$DEFINE Defined_GetHighResolutionCounter}
const
HighResolutionFrequency = 1000000;
HighResolutionMillisecondFactor = 1000;
HighResolutionMicrosecondFactor = 1;
function GetHighResolutionCounter: Word64;
const
MicroSecPerDay = Word64(24) * 60 * 60 * 1000 * 1000;
var
N : Double;
T : Word64;
begin
N := Now;
N := N * MicroSecPerDay;
T := Word64(Trunc(N));
Result := T;
end;
{$ENDIF}
{ }
{ High resolution tick }
{ }
function GetHighResolutionTick: Word64;
begin
Result := GetHighResolutionCounter;
end;
function GetHighResolutionFrequency: Word64;
begin
{$IFDEF WindowsPlatform}
if not HighResolutionCounterInit then
InitHighResolutionCounter;
{$ENDIF}
Result := HighResolutionFrequency;
end;
// Overflow checking needs to be off here to correctly handle tick values that
// wrap around the maximum value.
function HighResolutionTickDelta(const T1, T2: Word64): Int64;
begin
Result := Int64(Word64(T2 - T1));
end;
function HighResolutionTickDeltaU(const T1, T2: Word64): Word64;
begin
Result := Word64(T2 - T1);
end;
{ }
{ MicroTick }
{ }
function GetMicroTick: Word64;
var
T : Word64;
begin
T := GetHighResolutionCounter;
T := T div HighResolutionMicrosecondFactor;
Result := T;
end;
// Overflow checking needs to be off here to correctly handle tick values that
// wrap around the maximum value.
function MicroTickDelta(const T1, T2: Word64): Int64;
begin
{$IFDEF WindowsPlatform}
if not HighResolutionCounterInit then
InitHighResolutionCounter;
{$ENDIF}
Result := Int64(Word64(
Word64(T2 * HighResolutionMicrosecondFactor) -
Word64(T1 * HighResolutionMicrosecondFactor))) div Int64(HighResolutionMicrosecondFactor);
end;
function MicroTickDeltaU(const T1, T2: Word64): Word64;
begin
{$IFDEF WindowsPlatform}
if not HighResolutionCounterInit then
InitHighResolutionCounter;
{$ENDIF}
Result := Word64(
Word64(T2 * HighResolutionMicrosecondFactor) -
Word64(T1 * HighResolutionMicrosecondFactor)) div HighResolutionMicrosecondFactor;
end;
{ }
{ MilliTick }
{ }
function GetMilliTick: Word64;
var
T : Word64;
begin
T := GetHighResolutionCounter;
T := T div Word64(HighResolutionMillisecondFactor);
Result := T;
end;
// Overflow checking needs to be off here to correctly handle tick values that
// wrap around the maximum value.
function MilliTickDelta(const T1, T2: Word64): Int64;
begin
{$IFDEF WindowsPlatform}
if not HighResolutionCounterInit then
InitHighResolutionCounter;
{$ENDIF}
Result := Int64(Word64(
Word64(T2 * HighResolutionMillisecondFactor) -
Word64(T1 * HighResolutionMillisecondFactor))) div Int64(HighResolutionMillisecondFactor);
end;
function MilliTickDeltaU(const T1, T2: Word64): Word64;
begin
{$IFDEF WindowsPlatform}
if not HighResolutionCounterInit then
InitHighResolutionCounter;
{$ENDIF}
Result := Word64(
Word64(T2 * HighResolutionMillisecondFactor) -
Word64(T1 * HighResolutionMillisecondFactor)) div HighResolutionMillisecondFactor;
end;
{ }
{ MicroDateTime }
{ }
const
// Microseconds per day
// = 24 * 60 * 60 * 1000 * 1000
// = 86400000000
// = $141DD76000
MicroDateTimeFactor = Word64(86400000000);
function DateTimeToMicroDateTime(const DT: TDateTime): Word64;
var
Fl : Double;
Da : Word64;
FT : Double;
Ti : Word64;
begin
Fl := DT;
if (Fl < -1.0e-12) or (Fl >= 106751990.0) then
raise ETimerError.Create('Invalid date/time');
Da := Word64(Trunc(Fl));
Da := Word64(Da * MicroDateTimeFactor);
FT := Frac(Fl);
FT := FT * MicroDateTimeFactor;
Ti := Word64(Trunc(FT));
Result := Word64(Da + Ti);
end;
function MicroDateTimeToDateTime(const DT: Word64): TDateTime;
var
Da : Word64;
Ti : Word64;
Fl : Double;
begin
Da := DT div MicroDateTimeFactor;
Ti := DT;
Dec(Ti, Da * MicroDateTimeFactor);
Fl := Ti;
Fl := Fl / MicroDateTimeFactor;
Fl := Fl + Da;
Result := Fl;
end;
function GetMicroDateTimeNow: Word64;
begin
Result := DateTimeToMicroDateTime(Now);
end;
{ }
{ GetNowUT }
{ }
{$IFDEF DELPHIXE2_UP}
{$DEFINE SupportUniversalTime}
function GetNowUT: TDateTime;
begin
Result := System.DateUtils.TTimeZone.Local.ToUniversalTime(Now);
end;
{$ENDIF}
{$IFDEF FREEPASCAL}
{$IFDEF WindowsPlatform}
{$DEFINE SupportUniversalTime}
function GetUTBias: TDateTime;
var
TZI : TTimeZoneInformation;
BiasMin : Integer;
begin
case GetTimeZoneInformation(TZI) of
TIME_ZONE_ID_STANDARD : BiasMin := TZI.StandardBias;
TIME_ZONE_ID_DAYLIGHT : BiasMin := TZI.DaylightBias
else
BiasMin := 0;
end;
Inc(BiasMin, TZI.Bias);
Result := BiasMin / (24 * 60);
end;
function GetNowUT: TDateTime;
begin
Result := Now + GetUTBias;
end;
{$ENDIF}
{$ENDIF}
{$IFDEF FREEPASCAL}
{$IFDEF POSIX}
{$DEFINE SupportUniversalTime}
function GetUTBias: TDateTime;
var
TV : TTimeVal;
TZ : PTimeZone;
BiasMin : Integer;
begin
TZ := nil;
fpGetTimeOfDay(@TV, TZ);
if Assigned(TZ) then
BiasMin := TZ^.tz_minuteswest div 60
else
BiasMin := 0;
Result := BiasMin / (24 * 60);
end;
function GetNowUT: TDateTime;
begin
Result := Now + GetUTBias;
end;
{$ENDIF}
{$ENDIF}
{$IFNDEF SupportUniversalTime}
function GetNowUT: TDateTime;
begin
Result := Now;
end;
{$ENDIF}
{ }
{ GetMicroDateTimeNowUT }
{ }
function GetMicroDateTimeNowUT: Word64;
begin
Result := DateTimeToMicroDateTime(GetNowUT);
end;
{ }
{ GetMicroDateTimeNowUT(C)ached }
{ }
var
NowUTStartInit : Boolean = False;
NowUTStartDT : TDateTime = 0.0;
NowUTStartMicroTick : Word64 = 0;
NowUTStartMicroDT : Word64 = 0;
procedure InitNowUTStart;
var
DT : TDateTime;
MT : Word64;
begin
DT := GetNowUT;
MT := GetMicroTick;
NowUTStartDT := DT;
NowUTStartMicroTick := MT;
NowUTStartMicroDT := DateTimeToMicroDateTime(DT);
NowUTStartInit := True;
end;
function GetNowUTC(const ReInit: Boolean): TDateTime;
var
MT : Word64;
begin
if ReInit or not NowUTStartInit then
InitNowUTStart;
MT := GetMicroTick;
Result := MicroDateTimeToDateTime(NowUTStartMicroDT + (MT - NowUTStartMicroTick));
end;
function GetMicroDateTimeNowUTC(const ReInit: Boolean): Word64;
var
MT : Word64;
begin
if ReInit or not NowUTStartInit then
InitNowUTStart;
MT := GetMicroTick;
Result := NowUTStartMicroDT + (MT - NowUTStartMicroTick);
end;
{$IFDEF QOn}{$Q+}{$ENDIF}
{ }
{ Tests }
{ }
{$IFDEF TIMERS_TEST}
{$ASSERTIONS ON}
procedure Test_MilliTickDelta;
begin
Assert(MilliTickDelta(0, 10) = 10);
Assert(MilliTickDelta(Word64($FFFFFFFFFFFFFFFF), 10) = 11);
Assert(MilliTickDelta(10, 0) = -10);
Assert(MilliTickDelta(Word64($FFFFFFFFFFFFFFF6), 0) = 10);
Assert(MilliTickDeltaU(0, 10) = 10);
Assert(MilliTickDeltaU(Word64($FFFFFFFFFFFFFFFF), 10) = 11);
end;
procedure Test_MilliTickTimer1;
var
A, B : Word64;
I : Integer;
begin
// test tick timer using sleep
A := GetMilliTick;
I := 1;
repeat
Sleep(1);
Inc(I);
B := GetMilliTick;
until (I = 2000) or (B <> A);
Assert(B <> A);
Assert(I < 100);
Assert(MilliTickDelta(A, B) > 0);
Assert(MilliTickDelta(A, B) < 100);
end;
procedure Test_MicroTickTimer1;
var
A, B : Word64;
I : Integer;
begin
// test tick timer using sleep
A := GetMicroTick;
I := 1;
repeat
Sleep(1);
Inc(I);
B := GetMicroTick;
until (I = 2000) or (B <> A);
Assert(B <> A);
Assert(I < 100);
Assert(MicroTickDelta(A, B) > 0);
Assert(MicroTickDelta(A, B) < 100000);
end;
procedure Test_MilliTickTimer2;
var
A, B : Word64;
P, Q : TDateTime;
I : Integer;
begin
// test tick timer using clock
A := GetMilliTick;
I := 1;
P := Now;
repeat
Inc(I);
Q := Now;
B := GetMilliTick;
until (I = 100000000) or (B <> A) or
(Q >= P + 2.0 / (24.0 * 60.0 * 60.0)); // two seconds
Assert(B <> A);
Assert(MilliTickDelta(A, B) > 0);
Assert(MilliTickDelta(A, B) < 100);
end;
procedure Test_MicroTickTimer2;
var
A, B : Word64;
P, Q : TDateTime;
I : Integer;
begin
// test tick timer using clock
A := GetMicroTick;
I := 1;
P := Now;
repeat
Inc(I);
Q := Now;
B := GetMicroTick;
until (I = 100000000) or (B <> A) or
(Q >= P + 2.0 / (24.0 * 60.0 * 60.0)); // two seconds
Assert(B <> A);
Assert(MicroTickDelta(A, B) > 0);
Assert(MicroTickDelta(A, B) < 100000);
end;
procedure Test_MicroDateTime1;
var
DT1 : TDateTime;
DT2 : TDateTime;
MD1 : Word64;
MD2 : Word64;
begin
// Specific TDateTime
DT1 := 43971.5231084028;
MD1 := DateTimeToMicroDateTime(DT1);
Assert(MD1 = 3799139596566001);
Assert(MD1 < $00FFFFFFFFFFFFFF);
DT2 := MicroDateTimeToDateTime(MD1);
Assert(Abs(DT1 - DT2) < 1.0e-11);
MD2 := DateTimeToMicroDateTime(DT2);
Assert(MD2 = 3799139596566001);
// Zero TDatetime
DT1 := 0.0;
MD1 := DateTimeToMicroDateTime(DT1);
Assert(MD1 = 0);
DT2 := MicroDateTimeToDateTime(MD1);
Assert(Abs(DT2) < 1.0e-11);
end;
procedure Test_MicroDateTime2;
var
MD1 : Word64;
MD2 : Word64;
D : Int64;
begin
// NowUT
MD1 := GetMicroDateTimeNowUT;
Sleep(5);
MD2 := GetMicroDateTimeNowUT - MD1;
Assert(MD2 > 2000); // 2ms
Assert(MD2 < 100000); // 100 ms
// NowUTC
MD1 := GetMicroDateTimeNowUTC(True);
Sleep(5);
MD2 := GetMicroDateTimeNowUTC(False) - MD1;
Assert(MD2 > 2000); // 2ms
Assert(MD2 < 100000); // 100 ms
// NowUT / NowUTC drift
Sleep(10);
MD1 := GetMicroDateTimeNowUT;
MD2 := GetMicroDateTimeNowUTC(False);
if MD2 >= MD1 then
D := MD2 - MD1
else
D := MD1 - MD2;
Assert(D >= 0);
Assert(D < 100000); // 100ms
end;
procedure Test;
begin
Test_MilliTickDelta;
Test_MilliTickTimer1;
Test_MicroTickTimer1;
Test_MilliTickTimer2;
Test_MicroTickTimer2;
Test_MicroDateTime1;
Test_MicroDateTime2;
end;
{$ENDIF}
end.