Repository navigation
Description
Summary
TimedAnimation resets the event timer at the end of its _step() method:
matplotlib/lib/matplotlib/animation.py
Line 1446 in f7b3def
| self.event_source.interval = self._interval |
As far as I can tell, this is by design. The original commit included this comment:
matplotlib/lib/matplotlib/animation.py
Lines 314 to 318 in 70e401f
| 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
matplotlib/lib/matplotlib/animation.py
Line 1446 in f7b3def
| self.event_source.interval = self._interval |
Activity
tacaswell commentedon Aug 2, 2024
If the callback runs for longer than the internal and we do not reset it do we start getting "pileup"?
thangleiter commentedon Aug 2, 2024
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*intervalwith two frames at once.QuLogic commentedon Oct 25, 2024
Note that the GTK docs say:
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
QTimerdocs on whether they do the same or not. The Accuracy and timer resolution section does say:which could be read either way.
For
NSTimer, I found this statement: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.