Coalescable Timers in Typescript

Not too many people may be familiar with the concept of coalescable timers, but they an be very useful for a power conscious application without hard real time requirements. Coalesce literally means "to grow together". In the case of timers, this means that multiple timers are allowed to be combined into one. This is usually done to reduce the number of times a computer is woken from a low power state to perform some action. This can be incredibly useful with mobile processors where power is very critical. As more and more applications move towards web applications to reduce re-implementing code in multiple mobile SDKs, it becomes necessary to introduce an implementation of coalescable timers into JavaScript, the client side logic language of modern web applications.

The idea is that when creating a timer, a tolerance value is specified that states how much variability is allowed in the timer. The tolerance allows timers with similar values to "grow together" into one timer. Examine the following example:

Timer 1: Trigger every 1 minutes with tolerance of 10 seconds
Timer 2: Trigger every 50 seconds with tolerance of 10 seconds

Coalesced Timer: Trigger every 60 seconds

Now becomes time to describe an algorithm for implementing coalescable timers. The goal is to minimize the number of times a timer is triggered. When a new timer is created, examine the existing timers, if any of the existing coalesced timers satisfy the requirements, add the new timer to the existing ones. If no existing coalesced timer handles the requirements specified, then create a new coalesced timer. Below is an implementation in TypeScript, which compiles down to basic JavaScript:

// Module containing all logic for coalescable timers
module Coalescable {
    // Variable holding all coalesced timers that are aggregating the individual timers
    var timers: CoalescedTimer[] = new CoalescedTimer[];

    export function SetCoalescableTimeout(expression: any, msec: number, tolerance: number): void {
        // Search existing coalesced timers for timers that can accomodate this request
        for (var index: number = 0; index < timers.length; index++) {
            var coalescedTimer: CoalescedTimer = timers[index];
            if (msec - tolerance < coalescedTimer.msec &&
                msec + tolerance > coalescedTimer.msec) {
                coalescedTimer.Timers.push(expression);
                return;
            }
        }

        // Create a new coalesced timer since none can accomodate this request
        var coalescedTimer: CoalescedTimer = new CoalescedTimer(msec)
        coalescedTimer.Timers.push(expression);
        timers.push(coalescedTimer);
    }

    class CoalescedTimer {
        constructor(public msec: number) {
            setInterval(function () => {
                for (var index: number = 0; index < this.Timers.length; index++) {
                    new Function(this.Timers[index])();
                }
            }, msec);
        }

        public Timers: any[] = new any[];
    }
}

Coalescable.SetCoalescableTimeout("alert('1')", 5000, 10); // Will create a new coalesced timer since none exist.
Coalescable.SetCoalescableTimeout("alert('2')", 6000, 2000); // Will be coalesced into the existing timer to run every 5000ms.


Obviously this is a first implementation. Many improvements could be made, such as minimizing distance from threshold when more than one coalesced timer exists to service a new timer request. There are also a few other interesting scenarios. Say creating a new timer means that a new coalesced timer will be created. In this case, it may make sense to move an existing timer to this new coalesced timer to reduce the difference from the requested interval and the coalesced interval. Also, cancelling or clearing a timeout should be supported.

No comments: