Discrepancy when using Sleep in a Loop Topic is solved

Get help with using AutoHotkey (v2 or newer) and its commands and hotkeys
User avatar
Noitalommi_2
Posts: 286
Joined: 16 Aug 2023, 10:58

Discrepancy when using Sleep in a Loop

27 Dec 2023, 13:26

Hi.

I'm using a Loop in a script as some sort of timer and I noticed that there is a discrepancy when using Sleep with less than 125ms.
The deviation is too big to work with and gets bigger the longer the loop runs.
For example, a 100 x 100ms Loop has a deviation of 937ms whereas a 400 x 100ms Loop already has a deviation of 3750ms.

And the deviations are almost constant.

100 x 100ms: 10000/10937ms ~ 8,56%
400 x 100ms: 40000/43750ms ~ 8,57%
800 x 100ms: 80000/87500ms ~ 8,57%

Here is my test script with the results:
Can someone please run this script and tell me the results?

Code: Select all

TickCount := A_TickCount
Loop Loop_ := 20
	Sleep Sleep_ := 500
MsgBox "Loop 2`n" Loop_ "x" Sleep_ "=" Loop_*Sleep_ " / " A_TickCount-TickCount "ms" ; 20x500=10000 / 10000ms

TickCount := A_TickCount
Loop Loop_ := 40
	Sleep Sleep_ := 250
MsgBox "Loop 3`n" Loop_ "x" Sleep_ "=" Loop_*Sleep_ " / " A_TickCount-TickCount "ms" ; 40x250=10000 / 10000ms

TickCount := A_TickCount
Loop Loop_ := 80
	Sleep Sleep_ := 125
MsgBox "Loop 4`n" Loop_ "x" Sleep_ "=" Loop_*Sleep_ " / " A_TickCount-TickCount "ms" ; 80x125=10000 / 10000ms

TickCount := A_TickCount
Loop Loop_ := 100
	Sleep Sleep_ := 100
MsgBox "Loop 5`n" Loop_ "x" Sleep_ "=" Loop_*Sleep_ " / " A_TickCount-TickCount "ms" ; 100x100=10000 / 10937ms <------ ?

TickCount := A_TickCount
Loop Loop_ := 200
	Sleep Sleep_ := 50
MsgBox "Loop 6`n" Loop_ "x" Sleep_ "=" Loop_*Sleep_ " / " A_TickCount-TickCount "ms" ; 200x50=10000 / 9375ms <------ ?

TickCount := A_TickCount
Loop Loop_ := 400
	Sleep Sleep_ := 25
MsgBox "Loop 7`n" Loop_ "x" Sleep_ "=" Loop_*Sleep_ " / " A_TickCount-TickCount "ms" ; 400x25=10000 / 12500ms <------ ?
User avatar
mikeyww
Posts: 27214
Joined: 09 Sep 2014, 18:38

Re: Discrepancy when using Sleep in a Loop

27 Dec 2023, 13:49

Sleep is not precise. You can read about it.
Due to the granularity of the OS's time-keeping system, Delay is typically rounded up to the nearest multiple of 10 or 15.6 milliseconds (depending on the type of hardware and drivers installed). To achieve a shorter delay, see Examples.
If your CPU is doing anything else, then a different amount of time may also elapse throughout the script.
coffee
Posts: 133
Joined: 01 Apr 2017, 07:55

Re: Discrepancy when using Sleep in a Loop

27 Dec 2023, 14:37

Noitalommi_2 wrote:
27 Dec 2023, 13:26
Hi.

I'm using a Loop in a script as some sort of timer and I noticed that there is a discrepancy when using Sleep with less than 125ms.
The deviation is too big to work with and gets bigger the longer the loop runs.
For example, a 100 x 100ms Loop has a deviation of 937ms whereas a 400 x 100ms Loop already has a deviation of 3750ms.

And the deviations are almost constant.

100 x 100ms: 10000/10937ms ~ 8,56%
400 x 100ms: 40000/43750ms ~ 8,57%
800 x 100ms: 80000/87500ms ~ 8,57%

Here is my test script with the results:
Can someone please run this script and tell me the results?

Code: Select all

TickCount := A_TickCount
Loop Loop_ := 20
	Sleep Sleep_ := 500
MsgBox "Loop 2`n" Loop_ "x" Sleep_ "=" Loop_*Sleep_ " / " A_TickCount-TickCount "ms" ; 20x500=10000 / 10000ms

TickCount := A_TickCount
Loop Loop_ := 40
	Sleep Sleep_ := 250
MsgBox "Loop 3`n" Loop_ "x" Sleep_ "=" Loop_*Sleep_ " / " A_TickCount-TickCount "ms" ; 40x250=10000 / 10000ms

TickCount := A_TickCount
Loop Loop_ := 80
	Sleep Sleep_ := 125
MsgBox "Loop 4`n" Loop_ "x" Sleep_ "=" Loop_*Sleep_ " / " A_TickCount-TickCount "ms" ; 80x125=10000 / 10000ms

TickCount := A_TickCount
Loop Loop_ := 100
	Sleep Sleep_ := 100
MsgBox "Loop 5`n" Loop_ "x" Sleep_ "=" Loop_*Sleep_ " / " A_TickCount-TickCount "ms" ; 100x100=10000 / 10937ms <------ ?

TickCount := A_TickCount
Loop Loop_ := 200
	Sleep Sleep_ := 50
MsgBox "Loop 6`n" Loop_ "x" Sleep_ "=" Loop_*Sleep_ " / " A_TickCount-TickCount "ms" ; 200x50=10000 / 9375ms <------ ?

TickCount := A_TickCount
Loop Loop_ := 400
	Sleep Sleep_ := 25
MsgBox "Loop 7`n" Loop_ "x" Sleep_ "=" Loop_*Sleep_ " / " A_TickCount-TickCount "ms" ; 400x25=10000 / 12500ms <------ ?
Since you are pursuing sleep/waits with better precision, you are going to have to use another method to yield the thread. Even if the script calls NtSetTimerResolution or timeBeginPeriod, autohotkey's sleep will still be ~15.6 ms inaccurate and can be as high as that number x2, ~32ms or so. Any sleep under 32 ms will be super inaccurate, and greater numbers will still have a gap.
If you need a better interrupt interval, the script has to use either of the two winapi functions above to request it and then cleanup after itself.
You will also have to drop a_tickCount, if i recall, this uses getTickCount internally and that is not precise. You are going to need a better ticker to calculate time deltas like QueryPerformanceCounter, or QueryInterruptTimePrecise, or QueryUnbiasedInterruptTimePrecise, or GetSystemTimePreciseAsFileTime.

Essentially you will have to combine a better event timer resolution, with a better tick counter, with a higher process priority on the script, with autohotkey's sleep only on >32 ms leaving a gap of like 25 or 30ms, then direct winapi sleep(1 or <6 ms) in a tight loop to make it interruptible depending on the interrupt delay you requested, and then do a CPU burn busywait loop on the last 1-2.5 ms or higher depending on the interrupt delay.
Looped winapi sleep even at 1 ms will keep your cpu cool, the last millisecond busywait will not, hence why it needs to be for the final digit precision. Otherwise you can make peace with a looped winapi's sleep(1) and overshoot by like 0.5 to 0.9 ms, if you set the event timer delay to like 0.5ms.

I saw a forum thread some years or months ago where somebody posted a dependency-less sample function doing just this. There are multiple solutions to this in the forum and other sites already, however.
User avatar
Noitalommi_2
Posts: 286
Joined: 16 Aug 2023, 10:58

Re: Discrepancy when using Sleep in a Loop

27 Dec 2023, 21:23

Thanks for the answers.
I think it seems like there is an inaccuracy for Sleep values that are not a multiple of a certain value?
But I found that DllCall-Sleep somehow has a better precision in my case, that's something I can deal with.

Code: Select all

TickCount := A_TickCount
Loop 100
	DllCall("Sleep", "UInt", 100)
MsgBox A_TickCount-TickCount  ; 10094ms

TickCount := A_TickCount
Loop 100
	Sleep 100
MsgBox A_TickCount-TickCount ; 10938ms
@coffee
I've read something about it before but didn't think of it in this context because I don't know how Sleep works internally, but I'll take a look at things like QueryPerformanceCounter if I need more precision.
There is also an example in the Docs -> https://www.autohotkey.com/docs/v2/lib/DllCall.htm#ExQPC
User avatar
Seven0528
Posts: 395
Joined: 23 Jan 2023, 04:52
Location: South Korea
Contact:

Re: Discrepancy when using Sleep in a Loop

28 Dec 2023, 03:35

Code: Select all

#Requires AutoHotkey v2.0
#SingleInstance Force

Loop (startTick:=A_TickCount, totalDelay:=10000, loopCount:=100)    {
    sleep(currDelay:=max(0,(startTick+totalDelay-A_TickCount)//(loopCount-A_Index+1)))
    /*
    ...
    */
} ;  until (tooltip(A_Index==loopCount?unset:"A_Index`t: " A_Index "`nCurrent delay`t: " currDelay), false)
Msgbox("Total elapsed TickCount`t: " A_TickCount-startTick)
 If the final elapsed time is critical, as you mentioned, using QueryPerformanceFrequency to enhance time measurement precision is a method.
However, the code execution time isn't considered.
Hence, dynamically adjusting the delay value in Sleep could also be a viable approach. You might want to consider this as well.

The resolution of the GetTickCount function is limited to the resolution of the system timer, which is typically in the range of 10 milliseconds to 16 milliseconds. The resolution of the GetTickCount function is not affected by adjustments made by the GetSystemTimeAdjustment function.
And TickCount is known to have an accuracy of approximately 10 milliseconds. (I remember it varied slightly depending on the Windows environment, but I'm not entirely certain.)
  • English is not my native language. Please forgive any awkward expressions.
  • 영어는 제 모국어가 아닙니다. 어색한 표현이 있어도 양해해 주세요.
User avatar
Noitalommi_2
Posts: 286
Joined: 16 Aug 2023, 10:58

Re: Discrepancy when using Sleep in a Loop

28 Dec 2023, 16:08

Hi @Seven0528.

Yes, the elapsed time is important but it only comes about if the individual sleeps are precise. The end result is precise with your script, but unfortunately the individual Sleeps are inaccurate, which is why I can't use it that way. My script above is just an example, I don't know the total duration beforehand, the duration is dynamic, thank you anyway.

And I created a class that uses QueryPerformanceCounter to measure times and use precise sleeps, in case anyone is interested, here is the script:

Code: Select all

#Requires AutoHotkey >=2.0
#Warn
#SingleInstance

Time := PerformanceCounter()


Start := Time.Start()
Time.Wait(100)
End := Time.End()

MsgBox Time.Result(Start, End) "ms"


Class PerformanceCounter {

	__new() => (DllCall("QueryPerformanceFrequency", "Int64*", &freq_ := 0), this.freq := freq_)
	Start() => (DllCall("QueryPerformanceCounter", "Int64*", &CounterBefore_ := 0), CounterBefore_)
	End() => (DllCall("QueryPerformanceCounter", "Int64*", &CounterAfter_ := 0), CounterAfter_)
	Wait(ms_) {

		ms_ := ms_ * 10000, Start_ := this.Start()
		Loop
			if this.End() - Start_ >= ms_
				break
	}
	Result(Start_, End_) => (End_ - Start_) / this.freq * 1000
}
coffee
Posts: 133
Joined: 01 Apr 2017, 07:55

Re: Discrepancy when using Sleep in a Loop  Topic is solved

28 Dec 2023, 18:26

Noitalommi_2 wrote:
28 Dec 2023, 16:08
Hi @Seven0528.

Yes, the elapsed time is important but it only comes about if the individual sleeps are precise. The end result is precise with your script, but unfortunately the individual Sleeps are inaccurate, which is why I can't use it that way. My script above is just an example, I don't know the total duration beforehand, the duration is dynamic, thank you anyway.

And I created a class that uses QueryPerformanceCounter to measure times and use precise sleeps, in case anyone is interested, here is the script:

Code: Select all

#Requires AutoHotkey >=2.0
#Warn
#SingleInstance

Time := PerformanceCounter()


Start := Time.Start()
Time.Wait(100)
End := Time.End()

MsgBox Time.Result(Start, End) "ms"


Class PerformanceCounter {

	__new() => (DllCall("QueryPerformanceFrequency", "Int64*", &freq_ := 0), this.freq := freq_)
	Start() => (DllCall("QueryPerformanceCounter", "Int64*", &CounterBefore_ := 0), CounterBefore_)
	End() => (DllCall("QueryPerformanceCounter", "Int64*", &CounterAfter_ := 0), CounterAfter_)
	Wait(ms_) {

		ms_ := ms_ * 10000, Start_ := this.Start()
		Loop
			if this.End() - Start_ >= ms_
				break
	}
	Result(Start_, End_) => (End_ - Start_) / this.freq * 1000
}
I quick-frankensteined spliced the class below from other larger files I had around. If you want to play around with it. It requests the minimum possible event timer for you and undoes it on exit/error and bumps the process priority of the script.
The problem with doing a contiguous dllcall sleep in your previous one is that it's a total thread yield/suspension and no autohotkey events will fire, that includes hotkeys, gui, menu (including tray), timers, hotstrings, etc. They will fire after the contiguous block finishes.
Doing a performance hard burn like above with QPC will chug your CPU at 15% - 20% just on that loop. It is however very accurate.
Combine both with a better event timer delay, and include autohotky sleep for higher durations, and you can get a cool cpu with sleep durations within 1% or 2% of the wanted value, if the request for a 0.5 or 1ms event timer resolution succeeds.

Code: Select all

/**
 * Frankenstein static func class. "bee zee" wait, "busy" wait.
 * 
 * Call as function:
 * 
 * BzWait(10) -> interruptible and tick spin.
 * BzWait(10,, true) -> interruptible, only dllcall sleep.
 * BzWait(10, false) -> uninterruptible
 * etc
 * 
 */
class BzWait
{
    /**
     * Even timer delays. Min, max, cur.
     * @type {object}
     */
    static evtDelays := this.queryTimerRes()

    /**
     * How much should kernel32\sleep sleep for inside the loop.
     * Default the lowest delay the platform can support. Changeable to other values.
     */
    static suspendPrecision := this.evtDelays.min < 1 ? 1 : this.evtDelays.min
    
    /**
     * Padding in milliseconds for the last stage of tick count spinning.
     */
    static tickSpinPadding := 2.5


    /**
     * Autohotkey loops are interruptible by default, hence why no sleep(-1) is
     * needed here and there. Even with a tick spin busy wait, script events can fire.
     * 
     * @param {number} waitTime - How many milliseconds to "wait" for.
     * @param {bool} [semiInterrupt=true] - Changes between dllcall(sleep...) in
     * a loop or a contiguous block of "script is dead" milliseconds.
     * @param {bool} [onlySuspend=false] - Whether to perform the last stage,
     * which is just churning cpu cycles until the last < 2.5 milliseconds
     * complete.
     * 
     */
    static call(waitTime, semiInterrupt := true, onlySuspend := false)
	{
        if ( waitTime = 0 )
            return 0
        
        local finalEndTick := this.getPerfTick() + waitTime
        
        this.ahkSleepOrNot(waitTime)
        
        local suspendEndTick := finalEndTick - (onlySuspend ? 0 : this.tickSpinPadding)
        if ( semiInterrupt )
        {
            ; Spin on a tight loop of dllcall(sleep), cpu cool. Events work.
            while ( this.getPerfTick() < suspendEndTick )
                this.yieldThread(this.suspendPrecision)
        }
        else
        {
            ; Contiguous block of dllcall(sleep), no events until finished.
            if ( this.getPerfTick() < suspendEndTick )
                this.yieldThread( suspendEndTick - this.getPerfTick() )
        }

        if ( onlySuspend )
            return 0

        ; Last < 2.5 ms stage. CPU busy wait.
        loop
            if ( this.getPerfTick() >= finalEndTick )
                return 0
    }


    /**
     * Setup the initial event timer resolution for cleanup. Request higher process
     * priority. Runs only once. Windows in theory should revert any timer changes
     * after the application closes, but just in case...
     */
    static __new()
    {
        processSetPriority("H")
        this.setTimerRes(this.evtDelays.min)
        
        local initDelay := this.evtDelays.cur
        onExit( (*) => (this.setTimerRes(initDelay), 0) )
		onError( (_, mode) => (mode = "ExitApp" && this.setTimerRes(initDelay), 0) )
    }


    /**
     * Performance counter wrapper normalized to millisecond units.
     * 
     * @returns {number} The performance counter as milliseconds. Allowing drop-in
     * replacement for a_tickCount for `end - start` calculations.
     */
    static getPerfTick()
    {
        static perf := 0
        static spfms := (dllCall("QueryPerformanceFrequency", "int64*", &perf), perf) / 1000
        return (dllCall("QueryPerformanceCounter", "int64*", &perf), perf) / spfms
    }


    /**
     * Autohotkey sleep under the platform precision error. That way the remaining
     * portion of the sleep is handled by more precise busy waiting or thread
     * suspension.
     * Autohotkey sleep seems to get a bit more precise at higher msecs. But it's
     * still inaccurate, and fluctuates too, 12 ~ 24 ms overshoot.
     * 
     * @param {int} msec - Milliseconds to sleep for. Under values of 32, it will do
     * sleep(-1)
     */
	static ahkSleepOrNot(msec)
    {
		sleep( msec < 32 ? -1 : (msec - (msec < 40 ? 31 : 25)) )
	}

    /**
     * Yields (suspends) the thread. Autohotkey timers, hotkeys or threads can't fire
     * while the thread is suspended. The more granular the loops, the more can
     * autohotkey fire its own pseudo threads. That sleep function is used internally
     * by autohotkey in its MsgSleep function.
     * 
     * @param {number} msec - Milliseconds to sleep for.
     */
	static yieldThread(msec)
    {
		dllCall("Sleep", "uint", round(msec))
	}


    /**
     * NtQueryTimerResolution. No error checking.
     * @returns {object} - Object containing the minimum, maximum and current event
     * timer delays. Default values are in 100s of nanoseconds, but they are converted
     * to milliseconds.
     */
    static queryTimerRes()
    {
		static maxDelay := 0, minDelay := 0, curDelay := 0

        dllCall("Ntdll.dll\NtQueryTimerResolution", "int*", &maxDelay, "int*", &minDelay, "int*", &curDelay)
		return {
			min: round(minDelay /= 10000, 4),
			max: round(maxDelay /= 10000, 4),
			cur: round(curDelay /= 10000, 4)
		}
    }
    

    /**
     * NtSetTimerResolution. No error checking.
     * @param {number} res - Event timer resolution in milliseconds. Converted to
     * 100's of nanoseconds.
     * @returns {number} - The resolution that was set in milliseconds, if successful.
     */
    static setTimerRes(res)
    {
        return (
            dllCall("Ntdll.dll\NtSetTimerResolution", "uint", res * 10000, "int", 1, "int*", &res),
            round(res / 10000, 4)
        )
    }
}
and test it with:

Code: Select all



#include <BzWait>

test_bzWait()
test_bzWait()
{
	
	local waitFor := 10
	; local waitFor := 10000
    local iterations := 1000
    ; local iterations := 1

	local maxx := 0
	local minn := 0
	local did := 0
	local avg := 0
    local avgTotal := 0
	
    local globalStart := 0
    local globalEnd := 0

	local start := 0
	local end := 0

	local resultList := []

    globalStart := BzWait.getPerfTick()

	loop iterations
	{
		start := BzWait.getPerfTick()
		
		BzWait(waitFor)
		; BzWait(waitFor, false, true)
		; BzWait(waitFor, true, true)
		; DllCall("kernel32.dll\Sleep", "Uint", floor(waitFor))
		; sleep(waitFor)
        

		end := BzWait.getPerfTick()
		did := end - start
		
		resultList.push(did)
		
		if ( a_index = 1 )
			maxx := minn := did
		
		avg += round(did, 8)
		maxx := maxx > did ? maxx : did
		minn := minn < did ? minn : did
	}

    globalEnd := BzWait.getPerfTick()

    avgTotal := avg
	avg := avg / iterations
	
	local within1pct := 0
	local within2pct := 0
	local within5pct := 0
    local within10pct := 0
    local beyond10pct := 0
	local errMargin := 0
	
	for k,v in resultList
	{
		errMargin := abs((v / waitFor) - 1) 

		if ( errMargin <= 0.01 )
			within1pct++
		else if ( errMargin <= 0.02 )
			within2pct++
		else if ( errMargin <= 0.05 )
			within5pct++
        else if ( errMargin <= 0.1 )
            within10pct++
        else if ( errMargin > 0.1 )
            beyond10pct++
	}

	
	msgbox(""
        "total elapsed: " round(globalEnd - globalStart, 4) "`n"
        "avg elapsed: " round(avgTotal, 4) "`n"
        "`n"
		; "start: " start "`n"
		; "end: " end "`n`n"
		; "diff: " end - start "`n`n"
		"max: " round(maxx, 4) . " -- " . "over want: " round(maxx - waitFor, 4) "`n"
		"min: " round(minn, 4) . " -- " . "under want: " round(waitFor - minn, 4) "`n"
		"range: " round(maxx-minn, 4) "`n"
        "`n"
		"avg: " round(avg, 4) "`n`n"
		"within 1%: " within1pct "`n"
		"within 2%: " within2pct "`n"
		"within 5%: " within5pct "`n"
		"within 10%: " within10pct "`n"
        "beyond 10%: " beyond10pct
	)
	
}
User avatar
Noitalommi_2
Posts: 286
Joined: 16 Aug 2023, 10:58

Re: Discrepancy when using Sleep in a Loop

29 Dec 2023, 06:55

@coffee

That's useful, the CPU usage is reduced by about 80% with this class and the precision is still accurate enough. Thanks!
User avatar
Noitalommi_2
Posts: 286
Joined: 16 Aug 2023, 10:58

Re: Discrepancy when using Sleep in a Loop

29 Dec 2023, 21:14

coffee wrote:
27 Dec 2023, 14:37
Essentially you will have to combine a better event timer resolution, with a better tick counter, with a higher process priority on the script, with autohotkey's sleep only on >32 ms leaving a gap of like 25 or 30ms, then direct winapi sleep(1 or <6 ms) in a tight loop to make it interruptible depending on the interrupt delay you requested, and then do a CPU burn busywait loop on the last 1-2.5 ms or higher depending on the interrupt delay.
Ok, I so tried this (100ms Sleep) and the result is good, hardly any CPU usage in a Loop and it's still precise.

Code: Select all

Loop 100
    _Sleep ; 10000ms

_Sleep() { ; 100ms


    DllCall("QueryPerformanceCounter", "Int64*", &Start_ := 0)
    Sleep 95
    DllCall("QueryPerformanceCounter", "Int64*", &End_ := 0)
    r_ := (1000000 - (End_ - Start_)) // 10000
    Loop r_ // 3
        DllCall("Sleep", "UInt", 1)
    Loop {
        DllCall("QueryPerformanceCounter", "Int64*", &End_ := 0)
        if End_ - Start_ > 1000000
            break
    }
}

Return to “Ask for Help (v2)”

Who is online

Users browsing this forum: FanaticGuru and 31 guests