Skip to content

[MNT]: TimedAnimation event loop interval #28647

Open

Description

@thangleiter

Summary

TimedAnimation resets the event timer at the end of its _step() method:

self.event_source.interval = self._interval

As far as I can tell, this is by design. The original commit included this comment:

def _loop_delay(self, *args):
# Reset the interval and change callbacks after the delay.
self.event_source.remove_callback(self._loop_delay)
self.event_source.interval = self._interval
self.event_source.add_callback(self._step)

To me, this was suprising and it took me a while to get to the bottom of it. From the documentation I took that the event loop calls the animation target at exactly the interval specified. However, the current implementation means that it actually calls it at interval + time_in_callback. In case the callback does some heavy lifting, like processing data or communicating with an instrument, this can add significant overhead to the real interval between frames.

A MWE highlighting the effects of resetting the interval:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import animation

def update(frame):
    if len(frame) < 2:
        return []
    diff = np.diff(frame)
    avg.set_text(avg_txt.format(diff.mean()*1e3))
    cur.set_text(cur_txt.format(diff[-1]*1e3))
    return [avg, cur]


def frame():
    times = deque(maxlen=100)
    while True:
        time.sleep(0.1)
        times.append(time.perf_counter())
        yield times
        
fig, ax = plt.subplots(figsize=(1,1))
ax.axis('off')
avg_txt = 'avg: {:.3g} ms'
cur_txt = 'cur: {:.3g} ms'
avg = ax.text(0.5, 0.75, avg_txt.format(0), va='center', ha='center')
cur = ax.text(0.5, 0.25, cur_txt.format(0), va='center', ha='center')

anim = animation.FuncAnimation(fig, update, frame, repeat=False,
                               cache_frame_data=False, interval=200)

With the current behavior, this results in an effective interval of 300 ms. Commenting out the line resetting the interval results in the effective interval I would expect from interval=200, 200 ms.

Proposed fix

There might be reasonable usecases for resetting the interval, but I cannot see any right now. Hence, I would propose to leave the timer running uninterrupted, i.e., dropping

self.event_source.interval = self._interval

Activity

  1. tacaswell commented on Aug 2, 2024

    @tacaswell
    Member

    If the callback runs for longer than the internal and we do not reset it do we start getting "pileup"?

  2. thangleiter commented on Aug 2, 2024

    @thangleiter
    ContributorAuthor

    From what I can quickly tell, it basically coerces the interval to be as long as the callback takes. For instance, sleeping for twice the interval in this example updates the plot every 2*interval with two frames at once.

  3. QuLogic commented on Oct 25, 2024

    @QuLogic
    Member

    Note that the GTK docs say:

    After each call to the timeout function, the time of the next timeout is recalculated based on the current time and the given interval (it does not try to ‘catch up’ time lost in delays).

    So our doing so here appears to re-implement that and is redundant for that event loop.

    I could not get a definitive statement from QTimer docs on whether they do the same or not. The Accuracy and timer resolution section does say:

    All timer types may time out later than expected if the system is busy or unable to provide the requested accuracy. In such a case of timeout overrun, Qt will emit timeout() only once, even if multiple timeouts have expired, and then will resume the original interval.

    which could be read either way.

    For NSTimer, I found this statement:

    By contrast, a repeating timer fires and then reschedules itself on the same run loop. A repeating timer always schedules itself based on the scheduled firing time, as opposed to the actual firing time. For example, if a timer is scheduled to fire at a particular time and every 5 seconds after that, the scheduled firing time will always fall on the original 5-second time intervals, even if the actual firing time gets delayed. If the firing time is delayed so far that it passes one or more of the scheduled firing times, the timer is fired only once for that time period; the timer is then rescheduled, after firing, for the next scheduled firing time in the future.

    which is also a bit unclear to me, as it only talks about "actual firing time" and "scheduled firing time" which are concepts related to the event loop, but not the actual callback. I think this means the runtime of the callback doesn't matter, but "a repeating timer fires and then reschedules itself" could be read as scheduling it after the callback runs if that's what "fires" means.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions