• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[vJASS] [Snippet] MasterTimer

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
This system uses one timer to manage all your timer needs (that means all timeout). It bundles all codes that will expire at the same time using Nestharus' Trigger (This provides some advantages namely, better performance compared to coupling codes using native trigger, and it also allows you to remove a code from a currently executing/expiring timer - which doesn't work correctly when using timer + native trigger).


Script
JASS:
library MasterTimer /*


    */uses /*

    */Trigger            /*   https://github.com/nestharus/JASS/blob/master/jass/Systems/Trigger/script.j
    */Table              /*   https://www.hiveworkshop.com/threads/snippet-new-table.188084/
    */ListT              /*   https://github.com/nestharus/JASS/blob/master/jass/Data%20Structures/ListT/script.j
    */StaticUniqueList   /*   https://github.com/nestharus/JASS/blob/master/jass/Data%20Structures/StaticUniqueList/script.j
    */ErrorMessage       /*   https://github.com/nestharus/JASS/blob/master/jass/Systems/ErrorMessage/main.j

    *///! novjass

    |-------------|
    | Description |
    |-------------|
    /*
        This system uses a single timer to handle all timeouts. It does it by binding all
        the codes that will expire at the same time into a bucket. Each bucket will have
        its own Trigger and a remaining time. The timer runs as a one-shot timer to execute
        the bucket with the least remaining time and then it resets the time of that bucket
        to its timeout/period while the remaining time of the other buckets are updated.
        Then it proceeds to run the next bucket with the least remaining time and this
        process is repeated over again.

    */
    |-----|
    | API |
    |-----|

        struct MasterTimer/*

          */static method operator [] takes real timeout returns MasterTimer/*
            - Returns a timeout instance for registration
          */method clear takes nothing returns nothing/*
            - Clears all registered codes and Triggers from this timeout

          */method register takes code c returns nothing/*
          */method unregister takes code c returns nothing/*
            - Registers/Unregisters a code to a timeout instance

          */method registerTrigger takes Trigger whichTrigger returns nothing/*
          */method unregisterTrigger takes Trigger whichTrigger returns nothing/*
            - Registers/Unregisters a Trigger to a timeout instance


      */module MasterTimer/* (Optional)
            - Implement this module inside your struct below your static methods
              named 'periodic' and 'period'

            Interfaces (Optional):

              */static method period takes nothing returns real/*
                - Must return the desired periodic method execution timeout
                - If not found, the timeout would be set to 0.03125 by default
              */static method periodic takes nothing returns nothing/*
                - The periodic method

            Methods (Provided by the Module):

              */static method startPeriodic takes nothing returns nothing/*
                - Starts the periodic execution
              */static method stopPeriodic takes nothing returns nothing/*
                - Stops the periodic execution

    */
    |---------|
    | Example |
    |---------|

        call MasterTimer[0.03125].register(function Periodic)
        call MasterTimer[0.03125].unregister(function Periodic)

    //! endnovjass

    globals
    /*============================================================*/
    /*                    SYSTEM CONFIGURATION                    */
    /*============================================================*/

    /*
        The number of decimal places that will be considered
        for the timeout                                           */
        private constant integer TIMEOUT_PRECISION          = 5
    /*
        When looking for a bucket to register the code, the
        system will check if there is any that will expire at
        the same time as the input code. This value is the
        maximum allowance for the difference in their expiration.
        If the system can't find any existing bucket that meets
        this condition, a new bucket will be created              */
        private constant real EXPIRATION_OFFSET_TOLERANCE   = 0.05

    /*============================================================*/
    /*                    SYSTEM CONFIGURATION                    */
    /*============================================================*/
    endglobals

    private keyword Init

    private struct Bucket extends array

        Trigger trigger
        real timeout
        real remaining
        integer count
        readonly static TableArray bucket
        private static TableArray condition

        implement ListT

        method getBucket takes nothing returns thistype
            local thistype node = this.first
            loop
                exitwhen node == 0
                if node.remaining < EXPIRATION_OFFSET_TOLERANCE then
                    set node.count = node.count + 1
                    return node
                endif
                set node = node.next
            endloop
            set node = this.enqueue()
            set node.count = 1
            set node.timeout = this.timeout
            set node.remaining = node.timeout
            set node.trigger = Trigger.create(false)
            return node
        endmethod

        method removeFromBucket takes integer index returns nothing
            local thistype node = bucket[this][index]
            call bucket[this].remove(index)
            call condition[this].remove(index)
            set node.count = node.count - 1
            if node.count == 0 then
                call node.remove()
                call node.trigger.destroy()
            endif
        endmethod

        method register takes boolexpr expr returns nothing
            local thistype node = this.getBucket()
            local integer id = GetHandleId(expr)
            set bucket[this][id] = node
            set condition[this][id] = node.trigger.register(expr)
        endmethod

        method unregister takes boolexpr expr returns nothing
            local integer id = GetHandleId(expr)
            call TriggerCondition(condition[this][id]).destroy()
            call this.removeFromBucket(id)
        endmethod

        method registerTrigger takes Trigger trig returns nothing
            local thistype node = this.getBucket()
            set bucket[this][trig] = node
            set condition[this][trig] = node.trigger.reference(trig)
        endmethod

        method unregisterTrigger takes Trigger trig returns nothing
            call TriggerReference(condition[this][trig]).destroy()
            call this.removeFromBucket(trig)
        endmethod

        private static method init takes nothing returns nothing
            set bucket = TableArray[0x2000]
            set condition = TableArray[0x2000]
        endmethod
        implement Init

    endstruct

    private struct PeriodList extends array

        private static timer timer = CreateTimer()

        implement StaticUniqueList

        private static method periodic takes nothing returns nothing
            local real timeout = TimerGetTimeout(timer)
            local real leastRemaining = 0.00
            local thistype this = first
            local Bucket node
            loop
                exitwhen this == sentinel
                set node = Bucket(this).first
                loop
                    exitwhen node == 0
                    set node.remaining = node.remaining - timeout
                    if node.remaining == 0.00 then
                        set node.remaining = node.timeout
                        call node.trigger.fire()
                    endif
                    if leastRemaining == 0.00 or node.remaining < leastRemaining then
                        set leastRemaining = node.remaining
                    endif
                    set node = node.next
                endloop
                set this = this.next
            endloop
            call TimerStart(timer, leastRemaining, false, function thistype.periodic)
        endmethod

        method register takes nothing returns nothing
            if not (Bucket(this).first == 0) and (Bucket(this).last == Bucket(this).first) then
                call enqueue(this)
                if first == this or Bucket(this).timeout < TimerGetRemaining(timer) then
                    call TimerStart(timer, Bucket(this).timeout, false, function thistype.periodic)
                endif
            endif
        endmethod

        method unregister takes nothing returns nothing
            if Bucket(this).first == 0 then
                call this.remove()
                if first == sentinel then
                    call PauseTimer(timer)
                endif
            endif
        endmethod

    endstruct

    struct MasterTimer extends array

        debug private static TableArray registered
        private static Table bucket
        private static real factor

        static method operator [] takes real timeout returns thistype
            local integer id = R2I(timeout*factor)
            local Bucket this = bucket[id]
            if this == 0 then
                set this = Bucket.create()
                set this.timeout = timeout
                set bucket[id] = this
            endif
            return this
        endmethod

        method register takes code c returns nothing
            debug call ThrowError(this == 0, "MasterTimer", "register()", "thistype", 0, "Attempted to use a null instance")
            debug call ThrowError(c == null, "MasterTimer", "register()", "thistype", this, "Attempted to register a null code")
            debug call ThrowError(registered[this].boolean[GetHandleId(Filter(c))], "MasterTimer", "register()", "thistype", this, "Attempted to register an already registered code")
            debug set registered[this].boolean[GetHandleId(Filter(c))] = true
            call Bucket(this).register(Filter(c))
            call PeriodList(this).register()
        endmethod

        method unregister takes code c returns nothing
            debug call ThrowError(this == 0, "MasterTimer", "unregister()", "thistype", 0, "Attempted to use a null instance")
            debug call ThrowError(c == null, "MasterTimer", "unregister()", "thistype", this, "Attempted to unregister a null code")
            debug call ThrowError(not registered[this].boolean[GetHandleId(Filter(c))], "MasterTimer", "unregister()", "thistype", this, "Attempted to unregister an unregistered code")
            debug call registered[this].boolean.remove(GetHandleId(Filter(c)))
            call Bucket(this).unregister(Filter(c))
            call PeriodList(this).unregister()
        endmethod

        method registerTrigger takes Trigger trig returns nothing
            debug call ThrowError(this == 0, "MasterTimer", "registerTrigger()", "thistype", 0, "Attempted to use a null instance")
            debug call ThrowError(trig == 0, "MasterTimer", "registerTrigger()", "thistype", this, "Attempted to register a null Trigger")
            debug call ThrowError(registered[this].boolean[trig], "MasterTimer", "registerTrigger()", "thistype", this, "Attempted to register an already registered Trigger")
            debug set registered[this].boolean[trig] = true
            call Bucket(this).registerTrigger(trig)
            call PeriodList(this).register()
        endmethod

        method unregisterTrigger takes Trigger trig returns nothing
            debug call ThrowError(this == 0, "MasterTimer", "unregisterTrigger()", "thistype", 0, "Attempted to use a null instance")
            debug call ThrowError(trig == 0, "MasterTimer", "unregisterTrigger()", "thistype", this, "Attempted to unregister a null Trigger")
            debug call ThrowError(not registered[this].boolean[trig], "MasterTimer", "unregisterTrigger()", "thistype", this, "Attempted to unregister an unregistered Trigger")
            debug call registered[this].boolean.remove(trig)
            call Bucket(this).unregisterTrigger(trig)
            call PeriodList(this).unregister()
        endmethod

        method clear takes nothing returns nothing
            local Bucket node = Bucket(this).first
            debug call ThrowWarning(this == node, "MasterTimer", "clear()", "thistype", this, "Attempted to clear an empty instance")
            debug call ThrowError(this == 0, "MasterTimer", "clear()", "thistype", 0, "Attempted to clear a null instance")
            loop
                exitwhen node == 0
                set node.count = 0
                call node.trigger.destroy()
                call node.remove()
                set node = node.next
            endloop
            call PeriodList(this).unregister()
            call Bucket(this).destroy()
            call bucket.remove(R2I(Bucket(this).timeout*factor))
            call Bucket.bucket[this].flush()
            debug call registered[this].flush()
        endmethod

        private static method init takes nothing returns nothing
            debug set registered = TableArray[0x2000]
            set bucket = Table.create()
            set factor = Pow(10, TIMEOUT_PRECISION)
        endmethod
        implement Init

    endstruct

    private module Init
        private static method onInit takes nothing returns nothing
            call init()
        endmethod
    endmodule

    module MasterTimer

        static if thistype.periodic.exists then
            static method startPeriodic takes nothing returns nothing
                static if thistype.period.exists then
                    call MasterTimer[period()].register(function thistype.periodic)
                else
                    call MasterTimer[0.031250000].register(function thistype.periodic)
                endif
            endmethod

            static method stopPeriodic takes nothing returns nothing
                static if thistype.period.exists then
                    call MasterTimer[period()].unregister(function thistype.periodic)
                else
                    call MasterTimer[0.031250000].unregister(function thistype.periodic)
                endif
            endmethod
        endif

    endmodule


endlibrary



v1.0
- First Release
 
Last edited:
Level 13
Joined
Nov 7, 2014
Messages
571
Not sure why you switched to using function interfaces (function interface LoopCode takes nothing returns nothing) from using code, because function interfaces result in code duplication and trigger evaluations.

I don't know why you are not using structs (for readability) and I still don't know why you want to be so stingy with using timers/handles.

It seems to me that something like this is both easier to write and [arguably] use:
JASS:
library DelayedCountedExec

struct DelayedCountedExec
    static thistype instance
    static constant integer NO_COUNT = -1

    integer data // user data
    real delay
    integer eval_count = 0
    integer max_eval_count

    public boolexpr be
    public triggercondition tc
    public trigger trg
    public timer tmr

    method destroy takes nothing returns nothing
        call TimerStart(this.tmr, 0.0, false, null)
        call TriggerRemoveCondition(this.trg, this.tc)
        call DestroyBoolExpr(this.be)
        call this.deallocate()
    endmethod

    private static method dispatch takes nothing returns nothing
        local thistype this = R2I(TimerGetRemaining(GetExpiredTimer()) + 0.5)

        set this.eval_count = eval_count + 1
        set instance = this
        call TriggerEvaluate(this.trg)

        if this.max_eval_count != NO_COUNT and this.eval_count >= this.max_eval_count then
            call this.destroy()
        else
            call TimerStart(this.tmr, this, false, null)
            call PauseTimer(this.tmr)
            call TimerStart(this.tmr, this.delay, false, function thistype.dispatch)
        endif
    endmethod

    static method create takes integer data, real delay, integer count, code callback returns thistype
        local thistype this = allocate()

        set this.data = data
        set this.delay = delay
        set this.max_eval_count = count

        if this.trg == null then
            set this.trg = CreateTrigger()
        endif
        if this.tmr == null then
            set this.tmr = CreateTimer()
        endif

        set this.be = Condition(callback)
        set this.tc = TriggerAddCondition(this.trg, this.be)

        // could use a hashtable but meh...
        call TimerStart(this.tmr, this, false, null)
        call PauseTimer(this.tmr)
        call TimerStart(this.tmr, this.delay, false, function thistype.dispatch)

        return this
    endmethod
endstruct

endlibrary

silly example usage:
JASS:
function create_footmen takes nothing returns boolean
    local DelayedCountedExec de = DelayedCountedExec.instance
    local integer i

    set i = 1
    loop
        exitwhen i > de.eval_count
        call CreateUnit(Player(de.data), 'hfoo', 0.0, 0.0, 270.0)
        set i = i + 1
    endloop

    set de.delay = de.delay + 1.0

    if de.eval_count == de.max_eval_count then
        call BJDebugMsg("no more footmen!")
    endif

    return false
endfunction

globals
    DelayedCountedExec footmen_de
endglobals
function use_delayed_counted_exec takes nothing returns nothing
    set footmen_de = DelayedCountedExec.create(0, 1.0, 3, function create_footmen)
endfunction
 

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
Not sure why you switched to using function interfaces (
function interface LoopCode takes nothing returns nothing
) from using code, because function interfaces result in code duplication and trigger evaluations.
With using code, I'm forced to save the data using Table/hashtable while with function interfaces, I can easily use the function as index for arrays. Although there is that downside you mentioned, it would not matter in-game and it would even be slightly faster than using code.

I still don't know why you want to be so stingy with using timers/handles.
Because if it could be minimized, why not?


JASS:
    public boolexpr be
    public triggercondition tc
    public trigger trg
    public timer tmr
Btw, aren't struct members public by default?
 
Level 13
Joined
Nov 7, 2014
Messages
571
Because if it could be minimized, why not?

You are trading execution time for RAM and I don't think that's a good trade-off. Because the amount of RAM used is kind of negligible and there's plenty of it, on the other hand the Jass VM is not exactly fast, it doesn't take that much operations for it to start lowering the frame rate and/or freeze the game. So I think the operations that LoopCode would take can be better spent someplace else.

Btw, aren't struct members public by default?
I think they are, but when I was doing some tests I changed the visibility from private to public and forgot to set it back to private, these:
JASS:
    public boolexpr be
    public triggercondition tc
    public trigger trg
    public timer tmr
were meant to be private.
 
JASS:
        function interface LoopCode takes nothing returns nothing/*
            - Interface of functions to be registered in the loop

        */function RegisterLoopCode takes LoopCode c, real timeout returns boolean/*
            - Registers a code to run every <timeout> seconds and returns a boolean value depending
              on the success of the operation

        */function RegisterLoopCodeCounted takes LoopCode c, real timeout, integer executionCount returns boolean/*
            - Registers a code to run every <timeout> seconds <executionCount> times

        */function RegisterLoopCodeTimed takes LoopCode c, real timeout, real duration returns boolean/*
            - Registers a code to run every <timeout> seconds for <duration> seconds

        */function RemoveLoopCode takes LoopCode c returns boolean/*
            - Unregisters a code from the loop and returns a boolean value depending
              on the success of the operation

        */function SetCodeTimeout takes LoopCode c, real timeout returns boolean/*
            - Sets a new loop timeout for a code

function RegisterLoopCode takes LoopCode c, real timeout returns boolean
Can be achieved very straight forward with single timer by user.

JASS:
function RegisterLoopCodeCounted takes LoopCode c, real timeout, integer executionCount returns boolean
function RegisterLoopCodeTimed takes LoopCode c, real timeout, real duration returns boolean
Those may be useful, but are a bit restricted in vJASS, I believe. For example I can't really use them for my struct instances, as I can't assosiate my instance in the callback, meaning I have to loop through all instances in the callback, meaning they are foreced to share the same remaining time/counter. If I want each registred instance to run X times / or Y seconds, it won't work.

And as Aniki questioned, interface is maybe a lose here.

Maybe trigger conditions + possibility to bind an integer (struct instance) would be a good soluition? : )
 

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
Atm, I'm thinking of having this graveyarded. I think this implementation of 1 timer for all different timeout is not really a good idea. This will run 32 FPS even when the registered codes all have low frequency. Plus, when a user wants a really high frequency execution, it would not run evenly which would maybe cause some unexpected problems.
 

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
I was able to make a new design that still uses a single timer for all timeouts but unlike this one, it runs evenly for all timeouts. It does not have a minimum timeout limit and it does not run 33 fps regardless of the frequency of the registered codes' timeout. However, I'm not good in benchmarks and stress tests so I don't know the difference in performance between this, other timer systems, and with native timers =).


Here's the new design
JASS:
library MasterTimer /*


    */uses /*

    */Trigger            /*
    */Table              /*
    */ListT              /*
    */StaticUniqueList   /*
    */ErrorMessage       /*

    *///! novjass

    |-------------|
    | Description |
    |-------------|
    /*
        This system uses a single timer to handle all timeouts. It does it by binding all
        the codes that will expire at the same time into a bucket. Each bucket will have
        its own Trigger and a remaining time. The timer runs as a one-shot timer to execute
        the bucket with the least remaining time and then it resets the time of the bucket
        to its timeout/period and then it proceeds to run the next bucket with the least
        remaining time and this process is repeated over again.

    */
    |-----|
    | API |
    |-----|

        struct MasterTimer/*

          */static method operator [] takes real timeout returns MasterTimer/*
          */method clear takes nothing returns nothing/*
          */method register takes code c returns nothing/*
          */method unregister takes code c returns nothing/*
          */method registerTrigger takes Trigger whichTrigger returns nothing/*
          */method unregisterTrigger takes Trigger whichTrigger returns nothing/*


      */module MasterTimer/*

    */
    |---------|
    | Example |
    |---------|

        call MasterTimer[0.03125].register(function Periodic)
        call MasterTimer[0.03125].unregister(function Periodic)

    //! endnovjass

    globals
        private constant integer TIMEOUT_PRECISION = 5
        private constant real TIMEOUT_OFFSET_TOLERANCE = 0.05
    endglobals

    private keyword Init

    private struct Bucket extends array

        Trigger trigger
        real timeout
        real remaining
        integer count
        readonly static TableArray bucket
        private static TableArray condition

        implement ListT

        method getBucket takes nothing returns thistype
            local thistype node = this.first
            loop
                exitwhen node == 0
                if node.remaining < TIMEOUT_OFFSET_TOLERANCE then
                    set node.count = node.count + 1
                    return node
                endif
                set node = node.next
            endloop
            set node = this.enqueue()
            set node.count = 1
            set node.timeout = this.timeout
            set node.remaining = node.timeout
            set node.trigger = Trigger.create(false)
            return node
        endmethod

        method removeFromBucket takes integer index returns nothing
            local thistype node = bucket[this][index]
            call bucket[this].remove(index)
            call condition[this].remove(index)
            set node.count = node.count - 1
            if node.count == 0 then
                call node.remove()
                call node.trigger.destroy()
            endif
        endmethod

        method register takes boolexpr expr returns nothing
            local thistype node = this.getBucket()
            local integer id = GetHandleId(expr)
            set bucket[this][id] = node
            set condition[this][id] = node.trigger.register(expr)
        endmethod

        method unregister takes boolexpr expr returns nothing
            local integer id = GetHandleId(expr)
            call TriggerCondition(condition[this][id]).destroy()
            call this.removeFromBucket(id)
        endmethod

        method registerTrigger takes Trigger trig returns nothing
            local thistype node = this.getBucket()
            set bucket[this][trig] = node
            set condition[this][trig] = node.trigger.reference(trig)
        endmethod

        method unregisterTrigger takes Trigger trig returns nothing
            call TriggerReference(condition[this][trig]).destroy()
            call this.removeFromBucket(trig)
        endmethod

        private static method init takes nothing returns nothing
            set bucket = TableArray[0x2000]
            set condition = TableArray[0x2000]
        endmethod
        implement Init

    endstruct

    private struct PeriodList extends array

        private static timer timer = CreateTimer()

        implement StaticUniqueList

        private static method leastRemaining takes nothing returns real
            local thistype this = first
            local Bucket node
            local real least = -1.00
            loop
                exitwhen this == sentinel
                set node = Bucket(this).first
                loop
                    exitwhen node == 0
                    if least < 0.00 or node.remaining < least then
                        set least = node.remaining
                    endif
                    set node = node.next
                endloop
                set this = this.next
            endloop
            return least
        endmethod

        private static method periodic takes nothing returns nothing
            local real timeout = TimerGetTimeout(timer)
            local real nextTimeout = 0.00
            local thistype this = first
            local Bucket node
            loop
                exitwhen this == sentinel
                set node = Bucket(this).first
                loop
                    exitwhen node == 0
                    set node.remaining = node.remaining - timeout
                    if node.remaining == 0.00 then
                        set node.remaining = node.timeout
                        call node.trigger.fire()
                    endif
                    if nextTimeout == 0.00 or node.remaining < nextTimeout then
                        set nextTimeout = node.remaining
                    endif
                    set node = node.next
                endloop
                set this = this.next
            endloop
            call TimerStart(timer, nextTimeout, false, function thistype.periodic)
        endmethod

        method register takes nothing returns nothing
            if not (Bucket(this).first == 0) and (Bucket(this).last == Bucket(this).first) then
                call enqueue(this)
                if first == this then
                    call TimerStart(timer, leastRemaining(), false, function thistype.periodic)
                endif
            endif
        endmethod

        method unregister takes nothing returns nothing
            if Bucket(this).first == 0 then
                call this.remove()
                if first == sentinel then
                    call PauseTimer(timer)
                endif
            endif
        endmethod

    endstruct

    struct MasterTimer extends array

        debug private static TableArray registered
        private static Table bucket
        private static real factor

        static method operator [] takes real timeout returns thistype
            local integer id = R2I(timeout*factor)
            local Bucket this = bucket[id]
            if this == 0 then
                set this = Bucket.create()
                set this.timeout = timeout
                set bucket[id] = this
            endif
            return this
        endmethod

        method register takes code c returns nothing
            debug call ThrowError(this == 0, "MasterTimer", "register()", "thistype", 0, "Attempted to use a null instance")
            debug call ThrowError(c == null, "MasterTimer", "register()", "thistype", this, "Attempted to register a null code")
            debug call ThrowError(registered[this].boolean[GetHandleId(Filter(c))], "MasterTimer", "register()", "thistype", this, "Attempted to register an already registered code")
            debug set registered[this].boolean[GetHandleId(Filter(c))] = true
            call Bucket(this).register(Filter(c))
            call PeriodList(this).register()
        endmethod

        method unregister takes code c returns nothing
            debug call ThrowError(this == 0, "MasterTimer", "unregister()", "thistype", 0, "Attempted to use a null instance")
            debug call ThrowError(c == null, "MasterTimer", "unregister()", "thistype", this, "Attempted to unregister a null code")
            debug call ThrowError(not registered[this].boolean[GetHandleId(Filter(c))], "MasterTimer", "unregister()", "thistype", this, "Attempted to unregister an unregistered code")
            debug call registered[this].boolean.remove(GetHandleId(Filter(c)))
            call Bucket(this).unregister(Filter(c))
            call PeriodList(this).unregister()
        endmethod

        method registerTrigger takes Trigger trig returns nothing
            debug call ThrowError(this == 0, "MasterTimer", "registerTrigger()", "thistype", 0, "Attempted to use a null instance")
            debug call ThrowError(trig == 0, "MasterTimer", "registerTrigger()", "thistype", this, "Attempted to register a null Trigger")
            debug call ThrowError(registered[this].boolean[trig], "MasterTimer", "registerTrigger()", "thistype", this, "Attempted to register an already registered Trigger")
            debug set registered[this].boolean[trig] = true
            call Bucket(this).registerTrigger(trig)
            call PeriodList(this).register()
        endmethod

        method unregisterTrigger takes Trigger trig returns nothing
            debug call ThrowError(this == 0, "MasterTimer", "unregisterTrigger()", "thistype", 0, "Attempted to use a null instance")
            debug call ThrowError(trig == 0, "MasterTimer", "unregisterTrigger()", "thistype", this, "Attempted to unregister a null Trigger")
            debug call ThrowError(not registered[this].boolean[trig], "MasterTimer", "unregisterTrigger()", "thistype", this, "Attempted to unregister an unregistered Trigger")
            debug call registered[this].boolean.remove(trig)
            call Bucket(this).unregisterTrigger(trig)
            call PeriodList(this).unregister()
        endmethod

        method clear takes nothing returns nothing
            local Bucket node = Bucket(this).first
            debug call ThrowError(this == node, "MasterTimer", "clear()", "thistype", this, "Attempted to clear an empty instance")
            debug call ThrowError(this == 0, "MasterTimer", "clear()", "thistype", 0, "Attempted to clear a null instance")
            loop
                exitwhen node == 0
                set node.count = 0
                call node.trigger.destroy()
                call node.remove()
                set node = node.next
            endloop
            call Bucket(this).destroy()
            call bucket[this].remove(R2I(Bucket(this).timeout*factor))
            call Bucket.bucket[this].flush()
            debug call registered[this].flush()
        endmethod

        private static method init takes nothing returns nothing
            debug set registered = TableArray[0x2000]
            set bucket = Table.create()
            set factor = Pow(10, TIMEOUT_PRECISION)
        endmethod
        implement Init

    endstruct

    private module Init
        private static method onInit takes nothing returns nothing
            call init()
        endmethod
    endmodule

    module MasterTimer

        static if thistype.periodic.exists then
            static method startPeriodic takes nothing returns nothing
                static if thistype.period.exists then
                    call MasterTimer[period()].register(function thistype.periodic)
                else
                    call MasterTimer[0.031250000].register(function thistype.periodic)
                endif
            endmethod

            static method stopPeriodic takes nothing returns nothing
                static if thistype.period.exists then
                    call MasterTimer[period()].unregister(function thistype.periodic)
                else
                    call MasterTimer[0.031250000].unregister(function thistype.periodic)
                endif
            endmethod
        endif

    endmodule


endlibrary
I'm still not done with the documentation.


Maybe trigger conditions + possibility to bind an integer (struct instance) would be a good soluition? : )
My only problem with this is that I don't know of a way to get the handle of the executing triggercondition/boolexpr from inside the executing code in the same manner as GetTriggeringTrigger().
 
Just a thought. If a bucket expires/gets fired, a check for a potential bucket combination could be applied, as in theory they could be in allowed epsilon/tollerance after the run, while they were not before with current check.

JASS:
        private static method init takes nothing returns nothing
            set bucket = TableArray[0x2000]
            set condition = TableArray[0x2000]
        endmethod
        implement Init
Maybe implement InitTimer would be cool, because always so many operations onInits probably should be tried to be avoided.

JASS:
if node.remaining == 0.00 then
   set node.remaining = node.timeout
   call node.trigger.fire()
endif
Why == instead of <= ?
forget it

JASS:
        method register takes nothing returns nothing
            if not (Bucket(this).first == 0) and (Bucket(this).last == Bucket(this).first) then
                call enqueue(this)
                if first == this or Bucket(this).timeout < TimerGetRemaining(timer) then
                    call TimerStart(timer, Bucket(this).timeout, false, function thistype.periodic)
                endif
            endif
        endmethod
The first check, ehm, means if (Bucket(this).first != 0 or Bucket(this).last != 0)
Can the last be not "0", while first is "0", or other way around? Why the check?

Sometimes ifs compare with 0, sometimes with sentinel, while it's sematically the same, is there a reason?

JASS:
loop
    exitwhen node == 0
    set node.count = 0
    call node.trigger.destroy()
    call node.remove()
    set node = node.next
endloop
Are you sure this works? I haven't tried it with a demo, and didn't check the list logics, but I might imagine this would be required:

JASS:
loop
    exitwhen node == 0
    temp = node.next
    set node.count = 0
    call node.trigger.destroy()
    call node.remove()
    set node = temp
endloop

Why not letting assosciate an integer with registration to allow struct binding? Something alike is probably very important.
 
Last edited:

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
Just a thought. If a bucket expires/gets fired, a check for a potential bucket combination could be applied, as in theory they could be in allowed epsilon/tollerance after the run, while they were not before with current check.
Right, I'll make it configurable next update.


Maybe implement InitTimer would be cool, because always so many operations onInits probably should be tried to be avoided.
Ahh, you mean the two TableArray allocations? They actually are O(1) operations despite the sizes specified. Creating 1 TableArray of size 0x2000 is just about as light as creating 1 Table. It's one reason why I prefer using TableArrays instead of having array of Tables (instance member Tables) which needed to be dynamically created/destroyed along with struct object creation/destruction because the former only needs to be created once (at init) and accessing the Table from a TableArray is just a simple arithmetic (addition; Table instance + index) operation.


The first check, ehm, means if (Bucket(this).first != 0 or Bucket(this).last != 0)
Can the last be not "0", while first is "0", or other way around? Why the check?
if not (Bucket(this).first == 0) and (Bucket(this).last == Bucket(this).first) then
What I'm actually trying to do here is to check if there is only one node inside the Bucket(this) list. To do it I imagine there are two cases where the first and last node is equal. The first case is if the list is empty (first and last node is 0) and the second case is if there is only one node in the list (first and last node is non-zero but equal). The first expression Bucket(this).first != 0 eliminates the first case (ensures that the list not empty) while the second expression Bucket(this).last == Bucket(this).first checks if it satisfies the other case, which ensures that there is only one node inside the list.


Sometimes ifs compare with
0
, sometimes with
sentinel
, while it's sematically the same, is there a reason?
Ah that's because sentinel is only found on the StaticUniqueList module and not on ListT module so I decided that only when dealing with StaticUniqueList should I use it, but yeah I can make it uniform by using 0 in both.


Are you sure this works? I haven't tried it with a demo, and didn't check the list logics,
Alright, I forgot about these because I was accustomed to using my own lists which doesn't break the prev & next links of the node even when it is removed. I'll fix it.


Why not letting assosciate an integer with registration to allow struct binding? Something alike is probably very important.
Still can't find a way to do these because I don't know where to attach my integer. I can't attach it to the timer cause there is only one. I can't also attach it to the Trigger since one trigger contains many codes. It should be attached to the triggercondition but then I have no way of accessing the current executing triggercondition inside the executing code =(. But I was thinking it's still preferable for users to loop through all there instances (as what is usually done with a single timer for all spell instances) with the difference that if you use this system, you don't need to have 1 Timer : 1 Spell but rather, all spell with the same timeout will have a chance of being bundled together into 1 trigger (only one trigger evaluation compared to many timer expiration, as what CTL for example do for similar cases, although in CTL the chance of them being bundled is 100%).
 
Right, I'll make it configurable next update.
Maybe it doesn't even make sense realistically, if implementation makes mit unnecessarily more complex in a way.. was just thinking loud.^^

List seems to have:
JASS:
static method operator sentinel takes nothing returns integer
return 0
endmethod
but yeh, dosn't really matter.
What I'm actually trying to do here is to check if there is only one node
Ah, ok. I somehow didn't realize that.

But attaching data to [Bucket][Node] could work, or?
 
But nodes are enqueued to the bucket list with new registration. Why is the combination bucket and node not unique?
A node is unique for every Trigger (bucket) though
Hm, but a trigger is created for each node? I'm confused you say Trigger = Bucket.
JASS:
set node.trigger = Trigger.create(false)
What if I have timeout 1 second for all my instances. Why would I want to loop through all my instances each time when I need to run only my expiring one. Getting struct instance seems like much bigger win for such system than reducing timer usage. ;s
 

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
But nodes are enqueued to the bucket list with new registration. Why is the combination bucket and node not unique?
I mean yes, the combination for [bucket][node] corresponds to a unique trigger, but not to a unique code since a that trigger contains many codes that will expire at the same time. In fact node in itself is already unique since no bucket contains the same nodes.

Hm, but a trigger is created for each node? I'm confused you say Trigger = Bucket.
My bad, I meant trigger == node

What if I have timeout 1 second for all my instances. Why would I want to loop through all my instances each time when I need to run only my expiring one. Getting struct instance seems like much bigger win for such system than reducing timer usage. ;s
Yeah, this is quite important if you want to be able to do things like when using TimerUtils and you want to avoid additional data structures. However, for this to work, I first need to be able to solve the first problem (discussed above), and then be able to add the same code more than once to the same timeout (which currently doesn't work).
Maybe I can find a way soon.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Just going to throw out some thoughts here from when I was working on timers.

I had personally found that a single timer system wasn't feasible from a performance perspective ; ). What I ended up doing was creating 1 timer for each timeout. For a given timeout, iterate over a list as the order will always be the same ^_^. Now there is only 1 GetHandleId call/hashtable read for a given timeout (or do some math, whatever ends up being faster) and then N array reads. Fewer timers and lighter calls. Simplifies the code as well.

In the case of a bucket per timeout, I would argue against the use of Trigger. Timer code is going to be changing a lot. It is always in flux. Trigger will simplify removing conditions on an executing native trigger, but the cost is that it's going to have a lot of overhead in terms of creation/destruction and adding/removing. Timers add/remove a lot. If you are going for performance, it doesn't exactly make sense to use Trigger = ). I think in my little Timer lib, I ended up going for native triggers. I just removed the code after the trigger execution or something. If I called the trigger explicitly, it was in the cleanup of the caller method. If it was all on one trigger, it was on the next cycle.

Using one timer for everything is a cool and novel idea. I just don't think that it's particularly useful ^_^. It's more of an exercise in thought in my opinion : ).
 

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
What I ended up doing was creating 1 timer for each timeout. For a given timeout, iterate over a list as the order will always be the same ^_^. Now there is only 1 GetHandleId call/hashtable read for a given timeout (or do some math, whatever ends up being faster) and then N array reads. Fewer timers and lighter calls. Simplifies the code as well.
So If I understand correctly, there's exactly one timer per timeout. For each timeout, there are a list of buckets/triggers that will expire at different times. When a timer expires, the first trigger in its list will be poped, executed, and enqueued. Where I'm a bit confused is here: The timers I'm going to use will be non-periodic right? And after doing the things to the executing trigger, I still need to iterate over the whole list to update each trigger's remaining time then set the next timeout of the timer to the remaining time of the new head of the list, which is also like in a single timer at least in complexity (O(n)). So I think I didn't understood fully coz I think what you have in mind is something that's O(1).


In the case of a bucket per timeout, I would argue against the use of Trigger. Timer code is going to be changing a lot. It is always in flux. Trigger will simplify removing conditions on an executing native trigger, but the cost is that it's going to have a lot of overhead in terms of creation/destruction and adding/removing. Timers add/remove a lot.
Indeed. I have not thought of this.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Well, it's actually more than 1 timer per timeout, oops ;D. It's 1 timer per function that is executing at the precise same moment : O.


If you have a timeout of 5 and 4 functions that are executing on that timeout at the following times

+1, +2, +3, +4

Then it stands to reason that you have a list

[ 1, 2, 3, 4 ]


Really, a list of lists

[ [ 1 ], [ 2 ], [ 3 ], [ 4 ] ]

Whenever a timer of the given timeout expires, pop from the list and treat that as the thing that just expired. Push it to the back of the list if it repeats : ).

[ [ 1 ], [ 2 ], [ 3 ], [ 4 ] ] -> [ 1 ] expired

[ [ 1 ], [ 2 ], [ 3 ], [ 4 ] ] -> [ [ 2 ], [ 3 ], [ 4 ], [ 1 ] ] ( [ 2 ] will expire next ) ( system doesn't care when )

It doesn't really save anything on the timer handle count front : /. The only time anything is really going to be saved is when two functions that happen to expire at the same moment are run as they will be merged into one timer. As such, it only really works well with small timeouts. I think in my little timer library I had some crazy merging algorithm that would fudge up the expiration times. It guaranteed that the initial expiration of a timer would only be accurate to a certain point. This allowed it to merge timers more often, thus saving on the total number of timers active in the system : ).

I never really went anywhere with my own implementation of the idea. For some reason, it broke Warcraft 3 native timers, lmao. Don't ask me how I managed to pull that one off. It was enough to copy and paste the system into a map to make it so that TimerStart(CreateTimer(), ...) would not work. I don't really have any plans to take it anywhere as well... a user wouldn't really gain that much : ). Seems smarter to just run independent timers or very specific timer queues.

A timer queue takes the idea I had above but only supports 1 timeout. As a result, no GetHandleId or anything is required. Simply pop from the list and push it to the back and you've got your ids!

I actually used a timer queue module implementation when I was working on a footmen wars map way back when. Makes sense right? The map is only going to have a few timeouts at any given time. Why not just have 1 timer for each timeout? : ).


I hope that my findings when I worked on this years ago helps you figure out where to go ^_^. My general purpose timer system ended up being nothing more than a novel idea, but atleast got timer queues out of it :3.
 
  • Like
Reactions: AGD
Top