Debounce using VarRef / Coarse Graining Blocking Calls / Synchronize values / Remove duplicate events / Filter events

Helpful script writing tricks and HowTo's
iseahound
Posts: 1451
Joined: 13 Aug 2016, 21:04
Contact:

Debounce using VarRef / Coarse Graining Blocking Calls / Synchronize values / Remove duplicate events / Filter events

13 Jul 2023, 23:35

Excuse the elaborate title.

Let's say that you have a function that interfaces with an external device. Every time you call the function there is a noticeable lag. If you check the value, it lags. If you set the value, it lags.

Assume that you'd like to change this value by adding +1 or subtracting -1. Since the function is so slow, if you press [+1] 3 times, it takes one second for the value to change.

Let's make this harder. You want this slow, blocking function to be controlled by the mouse wheel. Every time you scroll the mouse wheel, millions of events are generated. But since your function is so slow, it takes 1000 seconds for all events to be processed, and in the meantime if you scroll the mouse wheel, more events are generated, creating a very long queue of events waiting to be executed.

Q: How do you filter out the excess events in AutoHotkey v2?

A: Use a function of the following form: SetTimer fn.bind(value, &value), -1.
Here SetTimer fn, -1 means execute the func as soon as you can, asynchronously, by placing the function call in the queue.

fn.bind(value, &value)

This is the interesting part. The value is turned into a number, and the VarRef object &value is a reference to the value.

Example: The user scrolls their mouse generating 3 scrolling events, with each event incrementing by 1. The function fn receives:

Code: Select all

1, &value
2, &value
3, &value
The function fn looks like this:

Code: Select all

fn(value, valueRef := "") {
   ; Retrieve the latest value.
   current := %valueRef%

   ; Check if the latest value equals the value passed originally.
   if  current == value {

      ; Mimic a blocking call
      Critical 'On'
      Sleep(250)                  ; Simulate a blocking call that needs 250 milliseconds to return
      global slow := current      ; Set the slow value to the latest value.
      Critical 'Off'
   }
}
where the current value can be found using &value from the previous step (the line: current := %valueRef%).
And the current value is checked against the originally passed value to determine whether the event should be discarded.

So now it becomes:

Code: Select all

1, 1 ; executes slow function because the current value is equal to the passed value (which is one above zero)
2, 3 ; not executed, as the slow function is blocking...
3, 3 ; executes slow function because the newly set current value is equal to the passed value (because the current value has been set to 3)
value is the value that will never change, and &value will always carry the latest value because it always changes.
When value == %valueRef%, this describes exactly when the mouse wheel stops scrolling. If you scrolled from 1—50, then the last value will be 50, therefore the 50th function call is executed, 1—49 fail the equality and are discarded.

If you scroll really slow, every line synchronizes.

Here's a script for you guys to play with:

Code: Select all

#Requires AutoHotkey v2.0-beta

global fast := 0  ; Non-blocking. Instant. Is a local copy of the slow value.

; The purpose of this script is to examine the best way to deal with a blocking call that takes 250 ms to return.
; When the wheel is scrolled, a million events are queued. How do we deal with this such that

WheelUp:: {
   global fast := fast + 1
   SetTimer fn.bind(fast, &fast), -1 ; Note that the current value is passed, as well as a reference to the current value
}

WheelDown:: {
   global fast := fast - 1
   SetTimer fn.bind(fast, &fast), -1 ; Note that the current value is passed, as well as a reference to the current value
}

fn(value, valueRef := "") {
   static log := ""

   ; Retrieve the latest value.
   current := %valueRef%

   ; Check if the latest value equals the value passed originally.
   if  current == value {

      ; Mimic a blocking call
      Critical 'On'
      Sleep(250)                  ; Simulate a blocking call that needs 250 milliseconds to return
      Critical 'Off'

      log .= value ", " current " synchronize `t" current "`n"
   }
   else {
      log .= value ", " current "`n"
   }

   Tooltip log
}

MButton:: Reload
Esc:: ExitApp
Here's a link to the problem that led to this solution: viewtopic.php?f=83&t=108867
Getting and setting the monitor brightness is very hard. The programatic way is to do it yourself!!!
Last edited by iseahound on 14 Jul 2023, 16:22, edited 1 time in total.
User avatar
kczx3
Posts: 1648
Joined: 06 Oct 2015, 21:39

Re: Coarse Graining Blocking Calls / Synchronize values / Remove duplicate events / Filter events

14 Jul 2023, 14:09

Other common approaches are to debounce the async call or you could maybe try and use a stack. If an event comes in while the callback is still executing then insert the latest event so it processes next in the queue.
iseahound
Posts: 1451
Joined: 13 Aug 2016, 21:04
Contact:

Re: Coarse Graining Blocking Calls / Synchronize values / Remove duplicate events / Filter events

14 Jul 2023, 16:15

Thank you! That's what it's called, debounce. I was thinking from the perspective of entropy, hence coarse graining.

https://www.freecodecamp.org/news/javascript-debounce-example/
iseahound
Posts: 1451
Joined: 13 Aug 2016, 21:04
Contact:

Re: Debounce using VarRef / Coarse Graining Blocking Calls / Synchronize values / Remove duplicate events / Filter event

10 Sep 2023, 10:45

You can also avoid using a VarRef and use a closure instead:

Code: Select all

somefunction() {
   ; Show tooltip
   Tooltip('hello')
   
   ; Clear tooltip after 7 seconds.
   static tick          ; Using a static allows changes in value to affect tick inside the closure.
   tick := A_TickCount  ; Change and update tick. tock is set only once using bind to eval A_TickCount.
   SetTimer (tock => (tick == tock) && Tooltip()).Bind(A_TickCount), -7000   
}
lexikos
Posts: 9625
Joined: 30 Sep 2013, 04:07
Contact:

Re: Debounce using VarRef / Coarse Graining Blocking Calls / Synchronize values / Remove duplicate events / Filter event

15 Sep 2023, 19:49

I think you are overcomplicating things, not defining the problem clearly, and that part of the problem is created by your method of dealing with the problem...

The delay of the "blocking call" is irrelevant, because hotkey messages are not handled while the blocking call is in progress. Using SetTimer with a bound function/closure means that each call creates a new timer, effectively queueing a new call which will occur not after 250ms, but after 15.6ms on average (SetTimer -1). You then solve this artificial problem by checking the value to filter out all but the last queued event, before the blocking call begins. If you used a normal function instead of a bound function/closure, there would be no queueing of multiple calls and no need to filter out anything.

As far as I can tell, this achieves the same purpose:

Code: Select all

#Requires AutoHotkey v2.0

global fast := 0

WheelUp:: {
    global fast += 1
    SetTimer WheelAct, -1
}

WheelDown:: {
    global fast -= 1
    SetTimer WheelAct, -1
}

WheelAct() {
    static tip := ""
    pre := fast
    Critical 'On'
    Sleep 250
    Critical 'Off'
    ToolTip tip .= pre " " fast "`n"
}
Each time SetTimer is called, the timer is reset. WheelAct can never interrupt itself, because there is only one instance of the function, and therefore only one timer.

A single timer call can be queued while WheelAct is running, but won't execute until it returns. You would probably want it to execute again to make another "blocking call" which uses the new value of pre. If not, you can delete any pending timer with SetTimer at the end of the call.

Sometimes I would be using a higher timer value to execute an action only after the value has remained static for the given duration. For instance, a timer period of -50 would prevent WheelAct from executing as long as each WheelUp/Down occurs within 50ms of the last (because the timer keeps resetting). When the wheel eventually stops moving, the function is called once with the final value.
iseahound
Posts: 1451
Joined: 13 Aug 2016, 21:04
Contact:

Re: Debounce using VarRef / Coarse Graining Blocking Calls / Synchronize values / Remove duplicate events / Filter event

15 Sep 2023, 23:44

Yes, I am definitely over-complicating things! I did look back at the motivations to write the code and saw this:

Code: Select all

      ; Somehow b goes from a relative to a fixed value...

      ; lmao you need to create a named function, and use named_func.bind or else SetTimer will just reset the timer!
      set_brightness(b, bref, xd2) {
         if (b == %bref%)
            DllCall("dxva2\SetVCPFeature", "Ptr",hPhysMon, "uchar", 0x10, "UInt", b)
         DllCall("dxva2\DestroyPhysicalMonitors", "uint", PhysMons, "ptr", xd2)
      }

      ; Sending a reference to b in case it changes is pretty important, as it allows updating the timer's function queue
      ; by discarding the unnecessary changes. For example, when going from 30 → 50, it's not necessary to set 31, 32, 33,
      ; if the current value of b is already at 50. Instead it may set 31, return, and see a new value of 50, skip 32-49 and set 50.
      SetTimer set_brightness.bind(b, &b, PHYS_MONITORS), -1
I know that SetTimer holds a reference to its bound arguments, so perhaps I could refactor this to use a "smart pointer" for PHYS_MONITORS/xd2, such that calling __Delete would call DestroyPhysicalMonitors when the timer is reset. I think since set_brightness is a nested function it should be static to avoid automatic closures OR properly catch the upvar b using a closure.

But yes, you are certainly right for refactoring my last snippet to:

Code: Select all

; Destroy tooltip after 7 seconds of the last showing.
SetTimer Tooltip, -7000
and

Code: Select all

#Requires AutoHotkey v1.1.33+
; Destroy tooltip after 7 seconds of the last showing.
SetTimer Tooltip, -7000
return

Tooltip:
   Tooltip
return
Given the difference in behavior of SetTimer with boundfuncs / named funcs should the help files for v1 and v2 be synchronized?
https://www.autohotkey.com/docs/v1/lib/ToolTip.htm wrote:

Code: Select all

#Persistent
ToolTip, Timed ToolTip`nThis will be displayed for 5 seconds.
SetTimer, RemoveToolTip, -5000
return

RemoveToolTip:
ToolTip
return
https://www.autohotkey.com/docs/v2/lib/ToolTip.htm wrote:

Code: Select all

ToolTip "Timed ToolTip`nThis will be displayed for 5 seconds."
SetTimer () => ToolTip(), -5000
iseahound
Posts: 1451
Joined: 13 Aug 2016, 21:04
Contact:

Re: Debounce using VarRef / Coarse Graining Blocking Calls / Synchronize values / Remove duplicate events / Filter event

16 Sep 2023, 23:07

another example where using SetTimer's built-in debounce won't work:

Code: Select all

#Space:: Peek("A", 65)

; Creates a fade out animation allowing the user to interact with the window below it. 
Peek(hwnd, alpha := 127) {              ; 3D drag and drop!
   hwnd := WinExist(hwnd)               ; Last Found Window
   ExStyle := WinGetExStyle()
   Transparent := WinGetTransparent()
   if (Transparent = "")
      Transparent := "Off"              ; Necessary as an empty string is acually 'Off'
   WinSetAlwaysOnTop 1
   WinSetExStyle "+0x80020"

   static tick
   tick := A_TickCount
   loop 255 - alpha
      SetTimer ((tock, i) => (tick == tock) && WinSetTransparent(255-i, hwnd))
        .bind(tick, A_Index), -A_Index*2

   KeyWait RegExReplace(A_ThisHotKey, ".*?(\W|\w*)$", "$1")
   tick := A_TickCount
   if !(ExStyle & 0x8)
      WinSetAlwaysOnTop 0
   WinSetExStyle ExStyle
   WinSetTransparent Transparent
   WinActivate
}

Return to “Tutorials (v2)”

Who is online

Users browsing this forum: No registered users and 4 guests