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
)
}