Introduction¶
Fritter (the Frame-Rate Independent Timer Tree-er) is a generalizable scheduling library.
If you’re writing some code, and you need to schedule some part of that code to happen in the future, there are a lot of different ways to do it.
In an event-driven framework like Asyncio or Twisted, you might use an API like call_at. This works fine, for some applications, but time is a deep and complex field, and you may encounter a variety of different problems as you interact with it.
The Problems¶
Not Using A Framework¶
You might need to schedule such work when you’re not using such a framework. How do you push that work off into the future, then? You might need to use some sort of external system such as cron; but now, you need to put that work into a totally separate script, which changes the way you need to interface with it.
Saving Work For The Future¶
You may need the work to be persistent; it might be important to make sure this work happens at some point in the future, even if your program needs to re-start, whether due to a crash or user interaction. In that case you need to embed your code within a framework like Celery, which requires you to set up a bunch of infrastructure like brokers and queues before you can even define your functions.
Maintaining Time-Accuracy For Repeating Tasks¶
You might need the work to be time-accurate (or “soft real-time”). If you’re a game developer, you might be familiar with a concept like deltaTime (a source of some famously tricky bugs); if you have done any audiovisual work, you’ve probably dealt with issues of audio drift; if you’ve done A/V programming, you might have needed to maintain a jitter buffer. Calculating those deltas accurately in such a way as to avoid accumulating floating-point inaccuracy can be tricky.
One of the inspirations for Fritter, from which it takes the first part of its name, is “Frame Rate Independence” which is what some game developers call time-accuracy since it means that a game will run correctly regardless of whatever frame rate it is able to achieve on your hardware. Twisted’s LoopingCall provides a nice interface for doing this in the context of that framework.
Honoring Civil Intervals¶
You might need the work to happen on a civil rather than a physical schedule. Physical time is time as measured by a clock from a specific reference point, recorded by something like your computer’s monotonic clock or a caesium atomic clock. An interval of physical time can be always be expressed as a number of SI seconds. However, a civil time delta can be expressed in terms of days, weeks, months, or years. If it is 2:15 PM on March 10th, you say that something should happen “in 10 days”, you would expect everyone’s clocks to say “March 20, 2:15” when it next occurs, regardless of what local legislatures have said about time zones, daylight savings time, and so on. This sometimes necessitates updates to the time zone database, which implicitly requires the second point above; as far as I know, at least, there is not any way to update a program’s timezone database without restarting it, if not rebooting the whole computer. However else you’re scheduling your work, you will need to write your own translation to and from civil time, and doing so is probably more complicated than you think.
In Python, there’s a wonderful little utility for a very flexible array of civil intervals: dateutil’s relativedelta.
Fritter: A One Stop Clock Shop¶
You may have noticed that all the problems I mentioned above already have solutions: cron for scheduling code outside of your current process, Celery for persisting work into a queue that can be persisted later, LoopingCall for time-accurate frame rate advancement, relativedelta for correctly honoring complex civil intervals. So if all the problems are solved, what is Fritter for?
The goal of Fritter is to provide a uniform, type-safe interface to all this functionality, allowing code to be written as generically as possible, to interface between multiple types of time.
For example, you can’t take a relativedelta
object and have it give you a
working cron
rule. You can’t use LoopingCall
without bringing in all
of Twisted, which means you can’t use it in Asyncio or Trio without a bunch of
awkward bridging.
LoopingCall
also operates exclusively in terms of seconds, which means that
if you need time-accuracy and persistence and civil time - say, for
example, you have a weekly task with associated state which needs to be invoked
manually by a system administrator, and it might get skipped if that operator
is on vacation so that twice as much work needs to be done, LoopingCall
can’t help you there.
In combining these things together, Fritter also provides some unique features.
Type Safety¶
Schedulers within Fritter are generic types on both when (what represents time) and what (what represents a callable).
By allowing a given scheduler to constrain what types of work may be scheduled
on them, you can tell mypy that x
is a Scheduler[datetime,
MyPersistentWork, int]
, and any attempt to
schedule a generic, non-persistent callable on it will give you a (somewhat)
readable type-checking error. This means that you can specify your desired
times in terms of datetime
, in terms of float
, or indeed in terms of
whatever custom time-keeping mechanism you have invented, if you want to work
in terms of, for example, an int
of microseconds rather than a float
of
seconds.
Putting The Name Together¶
Now that you know what its purpose is, I can explain the name is meaningful:
Frame-Rate Independent:
fritter.repeat.Repeater
provides an integer interval-count to itscallable
, so it counts frames for you and allows you to easily discretize whatever work you’re performing, assuming you can eventually catch up to real time.Timer Tree: Timers can be grouped together, and that group of timers can be embedded into a scheduler that is then scheduled on another scheduler, and so on.
Now that you know why you want to use it, let’s move on to actually using it.