Timer Trees

You can use fritter.tree to organize your timers into group within a sub-scheduler that can be paused, resumed, and scaled together.

To understand why this is useful, consider a video game with a “pause” screen. Timers which do things like play the animations in the UI from button presses should keep running. However, timers in the game world need to stop as long as the pause screen is displayed, then start running again. Similarly, a “slow” or “freeze” spell might want to slow down or pause a sub-group of timers within the group of timers affected by pause and unpause.

This isn’t exclusively for games. You might have similar needs in vastly different applications. For example, if you have a deployment workflow system, a code freeze might want to pause all timers associated with pushing new deployments during a code freeze, but leave timers associated with monitoring and health checks running.

fritter.tree.branch() takes a scheduler and branches a new scheduler off of it, returning a 2-tuple of a fritter.tree.BranchManager that allows you to control the branched scheduler by pausing and unpausing it, and by changing the relative time scales between the trunk and and its, a new branched fritter.scheduler.Scheduler of the same type as its argument.

Let’s get set up with a tree scheduler; we’ll create a memory driver, a simple scheduler, and branch off a new scheduler:

from typing import Callable

from fritter.boundaries import PhysicalScheduler
from fritter.drivers.memory import MemoryDriver
from fritter.scheduler import schedulerFromDriver
from fritter.tree import branch

driver = MemoryDriver()
trunk: PhysicalScheduler = schedulerFromDriver(driver)
manager, branched = branch(trunk)

And a simple function that produces some schedulable work that will label and show our progress:

def show(name: str) -> Callable[[], None]:
    def _() -> None:
        print(f"{name} trunk={trunk.now()} branch={branched.now()}")

    return _


Now we can schedule some work, first in the branch…

branched.callAt(1.0, show("branch 1"))
branched.callAt(2.0, show("branch 2"))
branched.callAt(3.0, show("branch 3"))

… then in the trunk …

trunk.callAt(1.0, show("trunk 1"))
trunk.callAt(2.0, show("trunk 2"))
trunk.callAt(3.0, show("trunk 3"))

So now we have a branch that is going to run some code at 1, 2, and 3 seconds, and a trunk primed to do the same. But, we can pause the branch! So let’s see what happens if we let one call run, pause, let another one run, unpause, and run the rest.

driver.advance()
print("pause")
manager.pause()
driver.advance()
print("unpause")
manager.unpause()
driver.advance()
driver.advance()

Let’s have a look at the output that produces.

branch 1 trunk=1.0 branch=1.0
trunk 1 trunk=1.0 branch=1.0
pause
trunk 2 trunk=2.0 branch=1.0
unpause
trunk 3 trunk=3.0 branch=2.0
branch 2 trunk=3.0 branch=2.0
branch 3 trunk=4.0 branch=3.0

As you can see here, first, the branch runs, the trunk runs, the time is 1.0 in each.

Then we pause the leaf scheduler via its manager, and advance again.

The trunk runs, showing that the time in the trunk is now 2.0. But the branch has not advanced! It is frozen in time, paused at 1.0.

When we unpause, and advance again, the trunk and branch both run at trunk-time 3.0 and branch time 2.0. When we advance to complete the branch’s work, we are now at 4.0 in the trunk and 3.0 in the branch.

Scaling

You can also cause time to run slower or faster, using the changeScale method of the BranchManager object. Here’s an example that starts a branch scheduler at 3x faster than its trunk, and increases its speed as it goes along.

# setup
from fritter.drivers.memory import MemoryDriver
from fritter.scheduler import schedulerFromDriver
from fritter.tree import branch, timesFaster
from fritter.boundaries import PhysicalScheduler

driver = MemoryDriver()
trunk: PhysicalScheduler = schedulerFromDriver(driver)
rate = 3.0
manager, branched = branch(trunk, timesFaster(rate))
# end setup


# showfunc
def loop(scheduler: PhysicalScheduler, name: str, interval: float = 1.0) -> None:
    def _() -> None:
        print(name)
        scheduler.callAt(scheduler.now() + interval, _)

    _()


# end showfunc

# loops
loop(trunk, "trunk", 1.0)
loop(branched, "branch", 1.0)
# work
for again in range(10):
    driver.advance()
    rate += 1
    manager.changeScale(timesFaster(rate))
    print(f"time: trunk={trunk.now()} branch={branched.now()}")

As you run it, it looks like this:

trunk
branch
branch
time: trunk=0.3333333333333333 branch=1.0000000000000002
branch
time: trunk=0.5833333333333333 branch=2.000000000000001
branch
time: trunk=0.7833333333333332 branch=3.0
branch
time: trunk=0.9499999999999998 branch=4.0
trunk
time: trunk=1.0 branch=4.350000000000001
branch
time: trunk=1.0812499999999998 branch=4.999999999999999
branch
time: trunk=1.192361111111111 branch=5.999999999999999
branch
time: trunk=1.292361111111111 branch=6.999999999999998
branch
time: trunk=1.3832702020202021 branch=8.000000000000002
branch
time: trunk=1.4666035353535354 branch=8.999999999999998