Browse Source

* Alternat implementation based on event, from Derek (Bug ID 28831)

git-svn-id: trunk@33298 -
michael 9 years ago
parent
commit
6ab9435e44
1 changed files with 343 additions and 73 deletions
  1. 343 73
      packages/fcl-base/src/fptimer.pp

+ 343 - 73
packages/fcl-base/src/fptimer.pp

@@ -23,12 +23,32 @@
   
   
   A nice improvement would be an implementation that works
   A nice improvement would be an implementation that works
   in all threads, such as the threadedtimer of IBX for linux.
   in all threads, such as the threadedtimer of IBX for linux.
+
+  Replaced SLEEP with TEvent for those platforms supporting threading:
+           Windows, Linux, BSD.
+  On the other platforms, use sleep. This unfortunately has a high overhead
+     resulting in drift.  A five minute timer could be up to 40 seconds late
+     do to entering and returning (linux x64).  MOdified to check the absolute
+     time every minute, has reduced that lag to about 0.100 second.  This is
+     still greater than TEvent, where the delay is only a few milliseconds (0-3).     
 }
 }
 
 
-unit fpTimer;
+unit fptimer;
 
 
 {$mode objfpc}{$H+}
 {$mode objfpc}{$H+}
 
 
+{ 
+  Windows, or any platform that uses Cthreads has TEvent with a timed wait
+  which can include android and embedded.
+  You can force the use of the Sleep() based timer by defining USESLEEP
+}
+
+{$IFNDEF USESLEEP}
+{$if Defined(MSWINDOWS) or (Defined(UNIX) and not Defined(BEOS))}
+{$define Has_EventWait}
+{$endif}
+{$ENDIF}
+
 interface
 interface
 
 
 uses
 uses
@@ -36,20 +56,25 @@ uses
 
 
 type
 type
   TFPTimerDriver = Class;
   TFPTimerDriver = Class;
-  
+
+  { TFPCustomTimer }
+
   TFPCustomTimer = class(TComponent)
   TFPCustomTimer = class(TComponent)
   private
   private
-    FInterval: Integer;
     FDriver : TFPTimerDriver;
     FDriver : TFPTimerDriver;
-    FOnTimer: TNotifyEvent;
-    FContinue: Boolean;
-    FRunning: Boolean;
-    FEnabled: Boolean;
-    procedure   SetEnabled(Value: Boolean );
+    FOnStartTimer : TNotifyEvent;
+    FOnStopTimer : TNotifyEvent;
+    FOnTimer : TNotifyEvent;
+    FInterval : Cardinal;
+    FActive : Boolean;
+    FEnabled : Boolean;
+    FUseTimerThread : Boolean;
+    procedure SetEnabled(const AValue: Boolean );
+    procedure SetInterval(const AValue: Cardinal);
   protected
   protected
-    property  Continue: Boolean read FContinue write FContinue;
-    procedure Timer; virtual;
+    property Active: Boolean read FActive write FActive;
     Function CreateTimerDriver : TFPTimerDriver;
     Function CreateTimerDriver : TFPTimerDriver;
+    procedure Timer; virtual;
   public
   public
     Constructor Create(AOwner: TComponent); override;
     Constructor Create(AOwner: TComponent); override;
     Destructor Destroy; override;
     Destructor Destroy; override;
@@ -57,25 +82,36 @@ type
     procedure StopTimer; virtual;
     procedure StopTimer; virtual;
   protected
   protected
     property Enabled: Boolean read FEnabled write SetEnabled;
     property Enabled: Boolean read FEnabled write SetEnabled;
-    property Interval: Integer read FInterval write FInterval;
+    property Interval: Cardinal read FInterval write SetInterval;
+    property UseTimerThread: Boolean read FUseTimerThread write FUseTimerThread;
     property OnTimer: TNotifyEvent read FOnTimer write FOnTimer;
     property OnTimer: TNotifyEvent read FOnTimer write FOnTimer;
+    property OnStartTimer: TNotifyEvent read FOnStartTimer write FOnStartTimer;
+    property OnStopTimer: TNotifyEvent read FOnStopTimer write FOnStopTimer;
   end;
   end;
 
 
   TFPTimer = Class(TFPCustomTimer)
   TFPTimer = Class(TFPCustomTimer)
   Published
   Published
     Property Enabled;
     Property Enabled;
     Property Interval;
     Property Interval;
+    Property UseTimerThread;
     Property OnTimer;
     Property OnTimer;
-  end;  
+    Property OnStartTimer;
+    Property OnStopTimer;
+  end;
+
+  { TFPTimerDriver }
 
 
   TFPTimerDriver = Class(TObject)
   TFPTimerDriver = Class(TObject)
   Protected
   Protected
     FTimer : TFPCustomTimer;
     FTimer : TFPCustomTimer;
+    FTimerStarted : Boolean;
+    procedure SetInterval(const AValue: Cardinal); virtual;
   Public
   Public
     Constructor Create(ATimer : TFPCustomTimer); virtual;
     Constructor Create(ATimer : TFPCustomTimer); virtual;
     Procedure StartTimer; virtual; abstract;
     Procedure StartTimer; virtual; abstract;
     Procedure StopTimer; virtual; abstract;
     Procedure StopTimer; virtual; abstract;
     Property Timer : TFPCustomTimer Read FTimer;
     Property Timer : TFPCustomTimer Read FTimer;
+    property TimerStarted: Boolean read FTimerStarted;
   end;
   end;
   TFPTimerDriverClass = Class of TFPTimerDriver;
   TFPTimerDriverClass = Class of TFPTimerDriver;
 
 
@@ -100,9 +136,8 @@ end;
 destructor TFPCustomTimer.Destroy;
 destructor TFPCustomTimer.Destroy;
 
 
 begin
 begin
-  If FEnabled then
-    StopTimer;
-  FDriver.FTimer:=Nil;  
+  StopTimer;
+  FDriver.FTimer:=Nil;
   FreeAndNil(FDriver);
   FreeAndNil(FDriver);
   Inherited;
   Inherited;
 end;
 end;
@@ -114,34 +149,59 @@ begin
   Result:=DefaultTimerDriverClass.Create(Self);
   Result:=DefaultTimerDriverClass.Create(Self);
 end;
 end;
 
 
-procedure TFPCustomTimer.SetEnabled(Value: Boolean);
+procedure TFPCustomTimer.SetEnabled(const AValue: Boolean);
 begin
 begin
-  if Value <> FEnabled then
+  if AValue <> FEnabled then
     begin
     begin
-    if Value then
+    FEnabled := AValue;
+    if FEnabled then
       StartTimer
       StartTimer
     else
     else
       StopTimer;
       StopTimer;
     end;
     end;
 end;
 end;
 
 
+procedure TFPCustomTimer.SetInterval(const AValue: Cardinal);
+begin
+  if FInterval <> AValue then
+    begin
+    fInterval := AValue;
+    if FActive and (fInterval > 0) then
+      FDriver.SetInterval(AValue)  // Allow driver to update Interval
+    else
+      StopTimer;                   // Timer not required
+    end;
+end;
+
 procedure TFPCustomTimer.StartTimer;
 procedure TFPCustomTimer.StartTimer;
+var
+  IsActive: Boolean;
 begin
 begin
-  If FEnabled then
-    Exit;
-  FEnabled:=True;
-  FContinue:=True;  
-  If Not (csDesigning in ComponentState) then  
+  IsActive:=FEnabled and (fInterval > 0) and Assigned(FOnTimer);
+  If IsActive and not fActive and Not (csDesigning in ComponentState) then
+    begin
     FDriver.StartTimer;
     FDriver.StartTimer;
+    if FDriver.TimerStarted then
+      begin
+      FActive := True;
+      if Assigned(OnStartTimer) then
+        OnStartTimer(Self);
+      end;
+    end;
 end;
 end;
 
 
 procedure TFPCustomTimer.StopTimer;
 procedure TFPCustomTimer.StopTimer;
 begin
 begin
-  If Not FEnabled then 
-    Exit;
-  FEnabled:=False;
-  FContinue:=False;  
-  FDriver.StopTimer;
+  if FActive then
+    begin
+    FDriver.StopTimer;
+    if not FDriver.TimerStarted then
+      begin
+      FActive:=False;
+      if Assigned(OnStopTimer) then
+        OnStopTimer(Self);
+      end;
+    end;
 end;
 end;
 
 
 procedure TFPCustomTimer.Timer;
 procedure TFPCustomTimer.Timer;
@@ -149,14 +209,13 @@ procedure TFPCustomTimer.Timer;
 begin
 begin
   { We check on FEnabled: If by any chance a tick comes in after it was
   { We check on FEnabled: If by any chance a tick comes in after it was
     set to false, the user won't notice, since no event is triggered.}
     set to false, the user won't notice, since no event is triggered.}
-  If FEnabled and Assigned(FOnTimer) then
+  If FActive and Assigned(FOnTimer) then
     FOnTimer(Self);
     FOnTimer(Self);
 end;
 end;
 
 
 { ---------------------------------------------------------------------
 { ---------------------------------------------------------------------
   TFPTimerDriver
   TFPTimerDriver
   ---------------------------------------------------------------------}
   ---------------------------------------------------------------------}
-  
 
 
 Constructor TFPTimerDriver.Create(ATimer : TFPCustomTimer);
 Constructor TFPTimerDriver.Create(ATimer : TFPCustomTimer);
 
 
@@ -164,116 +223,327 @@ begin
   FTimer:=ATimer;
   FTimer:=ATimer;
 end;
 end;
 
 
+procedure TFPTimerDriver.SetInterval(const AValue: Cardinal);
+begin
+  // Default implementation is to restart the timer on Interval change
+  if TimerStarted then
+    begin
+    StopTimer;
+    FTimerStarted := (AValue > 0);
+    if FTimerStarted then
+      StartTimer;
+    end;
+end;
+
 
 
 { ---------------------------------------------------------------------
 { ---------------------------------------------------------------------
     Default implementation. Threaded timer, one thread per timer.
     Default implementation. Threaded timer, one thread per timer.
   ---------------------------------------------------------------------}
   ---------------------------------------------------------------------}
+
+const
+  cMilliSecs: Extended = 60.0 * 60.0 * 24.0 * 1000.0;
   
   
 Type
 Type
+
+  { TFPTimerThread }
+
   TFPTimerThread = class(TThread)
   TFPTimerThread = class(TThread)
   private
   private
     FTimerDriver: TFPTimerDriver;
     FTimerDriver: TFPTimerDriver;
+    FStartTime : TDateTime;
+    {$ifdef Has_EventWait}
+    FWaitEvent: PEventState;
+    {$else}
+    fSignaled: Boolean;
+    {$endif}
+    fInterval: Cardinal;
     Function Timer : TFPCustomTimer;
     Function Timer : TFPCustomTimer;
+    Function GetWakeTime(var AInterval,Counter : Int64; Out WakeInterval : Integer; Out WakeTime : TDateTime) : Boolean;
   public
   public
     procedure Execute; override;
     procedure Execute; override;
     constructor CreateTimerThread(ATimerDriver: TFPTimerDriver);
     constructor CreateTimerThread(ATimerDriver: TFPTimerDriver);
+    procedure Terminate;
+    procedure SetInterval(const AValue: Cardinal);
   end;
   end;
 
 
+  { TFPThreadedTimerDriver }
+
   TFPThreadedTimerDriver = Class(TFPTimerDriver)
   TFPThreadedTimerDriver = Class(TFPTimerDriver)
   Private
   Private
     FThread : TFPTimerThread;
     FThread : TFPTimerThread;
-    Procedure DoNilTimer(Sender : TObject);
+  protected
+    Procedure SetInterval(const AValue: cardinal); override;
   Public
   Public
     Procedure StartTimer; override;
     Procedure StartTimer; override;
     Procedure StopTimer; override;
     Procedure StopTimer; override;
   end;
   end;
 
 
-function _GetTickCount: Cardinal;
-begin
-  Result := Cardinal(Trunc(Now * 24 * 60 * 60 * 1000));
-end;
-
 { ---------------------------------------------------------------------
 { ---------------------------------------------------------------------
     TFPTimerThread
     TFPTimerThread
   ---------------------------------------------------------------------}
   ---------------------------------------------------------------------}
-  
+
 constructor TFPTimerThread.CreateTimerThread(ATimerDriver: TFPTimerDriver);
 constructor TFPTimerThread.CreateTimerThread(ATimerDriver: TFPTimerDriver);
 begin
 begin
   inherited Create(True);
   inherited Create(True);
   FTimerDriver:=ATimerDriver;
   FTimerDriver:=ATimerDriver;
+  {$ifdef Has_EventWait}
+  FWaitEvent := BasicEventCreate(nil,false,false,'');
+  {$else}
+  fSignaled := False;
+  {$endif}
+  fInterval := ATimerDriver.Timer.Interval;
   FreeOnTerminate := True;
   FreeOnTerminate := True;
 end;
 end;
 
 
+procedure TFPTimerThread.Terminate;
+begin
+  inherited Terminate;
+  {$ifdef Has_EventWait}
+  BasicEventSetEvent(fWaitEvent);
+  {$else}
+  fSignaled := True;
+  {$endif}
+end;
+
+procedure TFPTimerThread.SetInterval(const AValue: Cardinal);
+begin
+  if fInterval <> AValue then
+    begin
+    fInterval := AValue;
+    {$ifdef Has_EventWait}
+    BasicEventSetEvent(fWaitEvent);   // Wake thread
+    {$else}
+    fSignaled := True;
+    {$endif}
+    end;
+end;
+
 Function TFPTimerThread.Timer : TFPCustomTimer;
 Function TFPTimerThread.Timer : TFPCustomTimer;
 
 
 begin
 begin
   If Assigned(FTimerDriver) Then
   If Assigned(FTimerDriver) Then
     Result:=FTimerDriver.FTimer
     Result:=FTimerDriver.FTimer
   else
   else
-    Result:=Nil;  
+    Result:=Nil;
+end;
+
+Function TFPTimerThread.GetWakeTime(var AInterval,Counter : Int64; Out WakeInterval : Longint; Out WakeTime : TDateTime) : Boolean;
+
+
+Var
+  Diff: Extended;
+   
+begin
+    { Use Counter*fInterval to avoid numerical errors resulting from adding
+      small values (AInterval/cMilliSecs) to a large real number (TDateTime),
+      even when using Extended precision }
+  WakeTime := FStartTime + (Counter*AInterval / cMilliSecs);
+  Diff := (WakeTime - Now);
+  if Diff > 0 then
+    begin
+    WakeInterval := Trunc(Diff * cMilliSecs);
+    if WakeInterval < 10 then
+      WakeInterval := 10;    // Provide a minimum wait time
+    end
+  else
+    begin
+    WakeInterval:=MaxInt;
+    // Time has already expired, execute Timer and restart wait loop
+    try
+      if not Timer.UseTimerThread then
+        Synchronize(@Timer.Timer)  // Call user event
+      else
+        Timer.Timer;
+    except
+      // Trap errors to prevent this thread from terminating
+    end;
+    Inc(Counter);
+    Result:=True;
+    end;
 end;
 end;
 
 
+{$ifdef Has_EventWait}
 procedure TFPTimerThread.Execute;
 procedure TFPTimerThread.Execute;
 var
 var
-  SleepTime: Integer;
+  WakeTime, StartTime: TDateTime;
+  WakeInterval: Integer;
+  Counter: int64; { use Int64 to avoid overflow with Counter*fInterval (~49 days)}
+  AInterval: int64;
+  Diff: Extended;
+  
+Const
+  wrSignaled = 0;
+  wrTimeout  = 1;
+  wrAbandoned= 2;
+  wrError    = 3;
+  
+begin
+  WakeInterval := MaxInt;
+  Counter := 1;
+  AInterval := fInterval;
+  FStartTime := Now;
+  while not Terminated do
+    begin
+    if GetWakeTime(AInterval,Counter,WakeInterval,WakeTime) then 
+      Continue;
+    if not Terminated then
+      case BasicEventWaitFor(WakeInterval,fWaitEvent) of
+      wrTimeout:
+        begin
+        if Terminated then
+          Break
+        else
+          begin
+          try
+            if not Timer.UseTimerThread then
+              // If terminate is called while here, then the Synchronize will be
+              // queued while the stoptimer is being processed.
+              // StopTimer cannot wait until thread completion as this would deadlock
+              Synchronize(@Timer.Timer)  // Call user event
+            else
+              Timer.Timer;
+          except
+            // Trap errors to prevent this thread from terminating
+          end;
+          Inc(Counter);                // Next interval
+          end;
+        end;
+      wrSignaled:
+        begin
+        if Terminated then
+          Break
+        else 
+          begin                      // Interval has changed
+          Counter := 1;              // Restart timer without creating new thread
+          AInterval := fInterval;
+          FStartTime := Now; 
+          end;
+        end;
+      else
+        Break;
+      end
+    end;
+  BasicEventDestroy(fWaitEvent);
+end;
+
+{$ELSE Has_EventWait}
+
+procedure TFPTimerThread.Execute;
+
+var
+  WakeTime, StartTime: TDateTime;
+  WakeInterval: Integer;
+  Counter: int64; { use Int64 to avoid overflow with Counter*fInterval (~49 days)}
+  AInterval: int64;
+  Diff: Extended;
   S,Last: Cardinal;
   S,Last: Cardinal;
-  T : TFPCustomTimer;
+  RecheckTimeCounter: integer;
+  
+const
+  cSleepTime = 500;           // 0.5 second, better than every 5 milliseconds
+  cRecheckTimeCount = 120;    // Recheck clock every minute, as the sleep loop can loose time
   
   
 begin
 begin
-  while Not Terminated do
+  WakeInterval := MaxInt;
+  Counter := 1;
+  AInterval := fInterval;
+  FStartTime := Now;
+  while not Terminated do
     begin
     begin
-    Last := _GetTickCount;
-    T:=Timer;
-    If Assigned(T) then
+    if GetWakeTime(AInterval,Counter,WakeInterval,WakeTime) then
+      Continue;
+    if not Terminated then
       begin
       begin
-      SleepTime := T.FInterval - (_GetTickCount - Last);
-      if SleepTime < 10 then
-        SleepTime := 10;
-      Repeat  
-        S:=5;
-        If S>SleepTime then
-          S:=SleepTime;
-        Sleep(S);
-        Dec(Sleeptime,S);
-      until (SleepTime<=0) or Terminated;
-      T:=Timer;
-      If Assigned(T) and not terminated then
-        Synchronize(@T.Timer);
+      RecheckTimeCounter := cRecheckTimeCount;
+      s := cSleepTime;
+      repeat
+        if s > WakeInterval then
+          s := WakeInterval;
+        sleep(s);
+        if fSignaled then            // Terminated or interval has changed
+          begin
+          if not Terminated then
+            begin
+            fSignaled := False;
+            Counter := 1;            // Restart timer
+            AInterval := fInterval;
+            StartTime := Now;
+            end;
+          break;                     // Need to break out of sleep loop
+          end;
+
+        dec(WakeInterval,s);         // Update total wait time
+        dec(RecheckTimeCounter);     // Do we need to recheck current time
+        if (RecheckTimeCounter < 0) and (WakeInterval > 0) then
+          begin
+          Diff := (WakeTime - Now);
+          WakeInterval := Trunc(Diff * cMilliSecs);
+          RecheckTimeCounter := cRecheckTimeCount;
+          s := cSleepTime;
+          end;
+      until (WakeInterval<=0) or Terminated;
+      if WakeInterval <= 0 then
+        try
+          inc(Counter);
+          if not Timer.UseTimerThread then
+            // If terminate is called while here, then the Synchronize will be
+            // queued while the stoptimer is being processed.
+            // StopTimer cannot wait until thread completion as this would deadlock
+            Synchronize(@Timer.Timer)  // Call user event
+          else
+            Timer.Timer;
+        except
+          // Trap errors to prevent this thread from terminating
+        end;
       end
       end
-    else
-      Terminate;  
     end;
     end;
 end;
 end;
-
+{$ENDIF Has_EventWait}
 { ---------------------------------------------------------------------
 { ---------------------------------------------------------------------
     TFPThreadedTimerDriver
     TFPThreadedTimerDriver
   ---------------------------------------------------------------------}
   ---------------------------------------------------------------------}
 
 
-Procedure TFPThreadedTimerDriver.DoNilTimer(Sender : TObject);
-
+procedure TFPThreadedTimerDriver.SetInterval(const AValue: cardinal);
 begin
 begin
-  FThread:=Nil;
+  if FThread <> nil then
+    begin
+    if AValue > 0 then
+      FThread.SetInterval(AValue)
+    else
+      StopTimer;
+    end;
 end;
 end;
 
 
-Procedure TFPThreadedTimerDriver.StartTimer; 
+Procedure TFPThreadedTimerDriver.StartTimer;
 
 
 begin
 begin
-  FThread:=TFPTimerThread.CreateTimerThread(Self);
-  FThread.OnTerminate:=@DoNilTimer;
-  FThread.Start;
+  if FThread = nil then
+    begin
+    FThread:=TFPTimerThread.CreateTimerThread(Self);
+    FThread.Start;
+    FTimerStarted := True;
+    end;
 end;
 end;
 
 
 Procedure TFPThreadedTimerDriver.StopTimer;
 Procedure TFPThreadedTimerDriver.StopTimer;
 begin
 begin
-  FThread.FTimerDriver:=Nil;
-  FThread.Terminate; // Will free itself.
-  CheckSynchronize; // make sure thread is not stuck at synchronize call.
-  If Assigned(FThread) then
-    Fthread.WaitFor;  
+  if FThread <> nil then
+    begin
+    try
+      // Cannot wait on thread in case
+      // 1.  this is called in a Synchonize method and the FThread is
+      //     about to run a synchronize method. In these cases we would have a deadlock
+      // 2.  In a DLL and this is called as part of DLLMain, which never
+      //     returns endthread (hence WaitFor) until DLLMain is exited
+      FThread.Terminate;   // Will call FThread.Wake;
+    finally
+      FThread := nil;
+    end;
+    FTimerStarted := False;
+    end;
 end;
 end;
 
 
 
 
 Initialization
 Initialization
   DefaultTimerDriverClass:=TFPThreadedTimerDriver;
   DefaultTimerDriverClass:=TFPThreadedTimerDriver;
 end.
 end.
-