Jump to content
johnnydp

More precise countdown

Recommended Posts

Hello,

 

I need to fire some action very precisely AFAIK I cannot fully trust to Ttimer(accuracy)

Maybe question is stupid or trivial but how get something similar like below but using the most precise method(TStopwatch)

 

procedure TForm1.Timer1Timer(Sender: TObject);
begin
// Do something every 1 second(timer interval set to 1000ms)
end;


 

 

 

Share this post


Link to post

try this for verify your time:

implementation

{$R *.dfm}

uses
  System.DateUtils;

var
  LOldNow: TDateTime = 0;
  LText  : string;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Timer1.Interval := 1 { ms }; // 15ms mininum to return a value!
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  LText          := '';
  LOldNow        := now;
  Timer1.Enabled := true;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  Timer1.Enabled := false;
  //
  Memo1.Lines.Delimiter     := '=';
  Memo1.Lines.DelimitedText := LText.Remove(0, 1);
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  // processing time "without" visual interference!!! in release mode
  LText := LText + '=' + MilliSecondSpan(now, LOldNow).ToString;
  //
  LOldNow := now;
end;

end.

I think that you'll need some "library" 3rd-party for that or you can read this article  https://www.thedelphigeek.com/2007/10/calculating-accurate.html

 

Project1_cMV8XeNNlb.gif     image.png

Edited by programmerdelphi2k
  • Thanks 1

Share this post


Link to post

You could try your own "Timer", but maybe you could get a little frustrated if you don't have much knowledge about "messages" from system....
In the end, the "Timer" just keeps listening and receiving messages from the operating system and filtering to see if it's a "timer" message, and, if true, it just calls a procedure to execute its "OnTimer" event. It's a session of: KillTimer(), SetTimer() non-stop.
You could try to create a new class using a "Thread", and do your own calculation, and call your preferred procedure, but if Embarcadero hasn't done it better yet, it's adequate for most needs!
Don't forget that every iteration with the UI will determine the delay of the triggered event, that is, its final result.

Share this post


Link to post

Thanks for your input, I doubt I will do that better than anyone did already,  I do not feel much strong  to make it good enough.

so after only quick initial research seems that topic  with meassuring time is definetely not trivial.

 

Edited by johnnydp

Share this post


Link to post
27 minutes ago, programmerdelphi2k said:

you can try use "TStopWatch" in System.Diagnostic.pas... works like a timer to measure a time, with more precision because dont use any UI event!

https://www.thoughtco.com/accurately-measure-elapsed-time-1058453

This was my initial question...;) Still don't know how to know do effecient check if X time was passed to fire action using stopwatch

Edited by johnnydp

Share this post


Link to post
unit Unit1;

interface

uses
  Winapi.MMSystem,
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    btnTimerOn: TButton;
    btnTimerOff: TButton;
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure btnTimerOffClick(Sender: TObject);
    procedure btnTimerOnClick(Sender: TObject);
  strict private
    FMMRESULT: MMRESULT;
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TimerCallBack(TimerID, Msg: Uint; dwUser, dw1, dw2: DWORD); pascal;
begin
  Form1.Memo1.Lines.Add('Hello from Multimedia Timer!');
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FMMRESULT := DWORD(-1);
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  btnTimerOffClick(Sender);
  Action := caFree;
end;

procedure TForm1.btnTimerOnClick(Sender: TObject);
var
  LDelay: UINT;
begin
  if (FMMRESULT <> DWORD(-1)) then
    btnTimerOffClick(Sender);
  LDelay := 1000; // Timer in millisecond (1000 = 1 second)
  FMMRESULT := TimeSetEvent(LDelay, 0, @TimerCallBack, 0, TIME_PERIODIC);
end;

procedure TForm1.btnTimerOffClick(Sender: TObject);
begin
  if (FMMRESULT <> DWORD(-1)) then
  begin
    TimeKillEvent(FMMRESULT);
    FMMRESULT := DWORD(-1);
  end;
end;

end.

In my experience, nothing beats a multimedia timer.
I hope it helps.

Quote

The multimedia timer runs in its own thread

 

Edited by KodeZwerg
corrected.
  • Like 1

Share this post


Link to post

@johnnydp - What is the time precision that you need? 

Do you need start/one-shot/stop precision, or do you need start/repeat task every interval until/stop?

If you are talking more accurate than 15.6ms (Windows default), you are most likely looking at highly CPU intensive custom code or kernel driver assisted timing.

In addition, you need to factor in the code required to "complete" the event.

 

  • Like 1

Share this post


Link to post

You can give a try to TThreadedTimer. Not sure if it's that precise at millisecond level but it beats TTimer on everyday use.

 

Share this post


Link to post

Whats the benefit compared to multimedia timer that already run in its own thread? @aehimself

(No complain against your timer project!!! Lots of kudos for you for sharing it!)

Share this post


Link to post

Precision-wise probably nothing, multimedia timers are supposed to be the closest you can get.

It is transparent though (you can see what is happening under the hood) and it is a drop-in replacement for TTimer which means 0 code change is required.

 

It's also good to leave a variety of options so OP and future visitors can choose their favorite flavor.

  • Like 1

Share this post


Link to post
Quote

REMARK: in case, it's specific for mSwInDoWs

timeSetEvent function  https://learn.microsoft.com/en-us/previous-versions/dd757634(v=vs.85)
Article 06/06/2016  2 minutes to read
The timeSetEvent function starts a specified timer event. The multimedia timer runs in its own thread. After the event is activated, it calls the specified callback function or sets or pulses the specified event object.

 

Note  This function is obsolete. New applications should use CreateTimerQueueTimer to create a timer-queue timer. 

https://learn.microsoft.com/en-us/windows/win32/api/threadpoollegacyapiset/nf-threadpoollegacyapiset-createtimerqueuetimer?redirectedfrom=MSDN

Article 10/13/2021  4 minutes to read

 

Edited by programmerdelphi2k

Share this post


Link to post
1 hour ago, KodeZwerg said:

Here is my try to do a "drop-in replacement", just by multimedia timer 😛


implementation

var
  IOnTimer: TNotifyEvent;

This will work as long as you have one instance in your application, or all your timers have the same handler.

Share this post


Link to post

@programmerdelphi2k updated to the method you mentioned, thanks
@aehimself updated to a more stable version that can run multiple times

unit kz.Windows.Timer;

interface

uses
  Winapi.Windows, Winapi.Messages,
  Vcl.Forms,
  System.Classes;

type
  TkzTimer = class(TComponent)
  strict private
    FEnabled: Boolean;
    FInterval: DWORD;
    FOnTimer: TNotifyEvent;
    FHandle: THandle;
  private
    FHWND: HWND;
  private
    procedure EnableTimer;
    procedure DisableTimer;
    procedure SetEnabled(const AValue: Boolean);
    procedure SetInterval(const AValue: DWORD);
  protected
    procedure WndProc(var Message: TMessage);
    procedure DoTimer; virtual;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    property Enabled: Boolean read FEnabled write SetEnabled default False;
    property Interval: DWORD read FInterval write SetInterval default 1000;
    property OnTimer: TNotifyEvent read FOnTimer write FOnTimer;
  end;

implementation

procedure TimerCallback(lpParameter: TkzTimer; TimerOrWaitFired: Boolean); StdCall;
begin
  Winapi.Windows.PostMessage(lpParameter.FHWND,
                             (WM_APP + 666),
                             0,
                             0);
end;

procedure TkzTimer.DoTimer;
begin
  if Assigned(FOnTimer) then
    FOnTimer(Self);
end;

procedure TkzTimer.WndProc(var Message: TMessage);
begin
  case Message.Msg of
    (WM_APP + 666): try
                      DoTimer;
                    except
                      Vcl.Forms.Application.HandleException(Self);
                    end
    else
      Message.Result := Winapi.Windows.DefWindowProc(FHWND,
                                                     Message.Msg,
                                                     Message.WParam,
                                                     Message.LParam);
  end;
end;

constructor TkzTimer.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FEnabled  := False;
  FInterval := 1000;
  FOnTimer  := nil;
  FHandle   := Winapi.Windows.INVALID_HANDLE_VALUE;
  FHWND     := System.Classes.AllocateHWnd(WndProc);
end;

destructor TkzTimer.Destroy;
begin
  FOnTimer := nil;
  DisableTimer;
  System.Classes.DeallocateHWnd(FHWND);
  inherited Destroy;
end;

procedure TkzTimer.SetEnabled(const AValue: Boolean);
begin
  if AValue then
    EnableTimer
  else
    DisableTimer;
end;

procedure TkzTimer.SetInterval(const AValue: DWORD);
begin
  FInterval := AValue;
  if (FInterval < 1) then
    FInterval := 1;
  if FEnabled then
    EnableTimer;
end;

procedure TkzTimer.EnableTimer;
begin
  if (FHandle <> Winapi.Windows.INVALID_HANDLE_VALUE) then
    DisableTimer;
  FEnabled := Winapi.Windows.CreateTimerQueueTimer(FHandle,
                                                   0,
                                                   @TimerCallback,
                                                   Self,
                                                   FInterval,
                                                   FInterval,
                                                   Winapi.Windows.WT_EXECUTEDEFAULT or Winapi.Windows.WT_EXECUTELONGFUNCTION);
end;

procedure TkzTimer.DisableTimer;
begin
  if Winapi.Windows.DeleteTimerQueueTimer(0, FHandle, 0) then
    begin
      FHandle  := Winapi.Windows.INVALID_HANDLE_VALUE;
      FEnabled := False;
    end;
end;

end.

Alpha version 🙂

Edited by KodeZwerg
  • Thanks 1

Share this post


Link to post

Apologize delay(illness)

Thank you guys for all of your input and comments, CreateTimerQueueTimer seems to be the best method (cpu time vs accuracy)

 

@KodeZwerg 

 Multimedia timer(your first code posted) works very precise too, however I'm getting AV on form close(with low intervals <=50ms) - not always.

 

As for the latest method, Can you show some basic implementatio like for the first one? Fire action every interval set mean for unit kz.Windows.Timer ;

Probably it would be good to add procedures and properties for Period and DueTime(SetPeriod, SetDueTime)
 

QFP, QPC seems to be non go for such usage like I first wanted, it's very good for meassure execution time for some portion of code e.g. for profiling/optimizations.

But for other purposes CreateTimerQueueTimer seems to be the king.

 

@Lars Fosdal Fire and run until app will be closed/destroyed (launch tasks every X time with precision where ttimer is too weak)

 

Edited by johnnydp

Share this post


Link to post
7 hours ago, johnnydp said:

Apologize delay(illness)

Thank you guys for all of your input and comments, CreateTimerQueueTimer seems to be the best method (cpu time vs accuracy)

 

@KodeZwerg 

As for the latest method, Can you show some basic implementatio like for the first one? Fire action every interval set mean for unit kz.Windows.Timer ;

Probably it would be good to add procedures and properties for Period and DueTime(SetPeriod, SetDueTime)

I just used it like I would use a standard TTimer.
Here is a sample usage, in attachment a full project you can load in.
 

procedure TForm11.FormCreate(Sender: TObject);
begin

  Timer1 := TkzTimer.Create(Self);
  try
    Timer1.OnTimer := DoTimer1;
    Timer1.LinkIntervalDueTime := False;
    Timer1.DueTime := 5000;
    Timer1.Interval := 1;
  finally
    Timer1.Enabled := True;
  end;

  Timer2 := TkzTimer.Create(Self);
  try
    Timer2.OnTimer := DoTimer2;
    Timer2.Interval := 750;
  finally
    Timer2.Enabled := True;
  end;

end;

I've tested with Delphi Alexandria, 32 and 64 bit.
When you load project and build it, answer the question about "remove does not have corresponding component" with "No"
As you can see and liked to have, DueTime is added, by default it act like the basic TTimer act with a lag of 1000ms or whatever Interval you set.
In demo i used 1ms with a waiting time of 5s and 750ms, works for me flawless and nothing bad happen on closing the demo.
I added a LinkIntervalDueTime boolean that control if values should be in balance (like a basic TTimer would be) or complete seperated from each other.
Anyway, 1ms is in my demo too fast to process for Vcl, lags are on Vcl side, not my Component.

I hope you like it and find it useful for your needs.

kzTimer.zip

  • Thanks 1

Share this post


Link to post

@KodeZwerg Thanks, yes it's useful 

as for crashing I meant your previous code(Multimedia Timer) when set to very low interval. Thanks it's really alpha and ver(lack of Period and some checks) anyway great start.

Thank you

Share this post


Link to post

You still haven't explicitly defined the required precision?

Anything timing related on Windows is a gamble err, calculated risk, unless you go to driver-level, and even then it depends on Windows not being starved for resource.

 

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×