Skip to content

MNT/FIX: macosx change Timer to NSTimer instance #26022

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -1164,7 +1164,8 @@ def interval(self):
def interval(self, interval):
# Force to int since none of the backends actually support fractional
# milliseconds, and some error or give warnings.
interval = int(interval)
# Some backends also fail when interval == 0, so ensure >= 1 msec
interval = max(int(interval), 1)
self._interval = interval
self._timer_set_interval()

Expand Down
1 change: 1 addition & 0 deletions lib/matplotlib/backends/backend_macosx.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def callback_func(callback, timer):
self._timers.remove(timer)
timer.stop()
timer = self.new_timer(interval=0)
timer.single_shot = True
timer.add_callback(callback_func, callback, timer)
self._timers.add(timer)
timer.start()
Expand Down
45 changes: 45 additions & 0 deletions lib/matplotlib/tests/test_backends_interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,3 +624,48 @@ def test_figure_leak_20490(env, time_mem):

growth = int(result.stdout)
assert growth <= acceptable_memory_leakage


def _impl_test_interactive_timers():
# A timer with <1 millisecond gets converted to int and therefore 0
# milliseconds, which the mac framework interprets as singleshot.
# We only want singleshot if we specify that ourselves, otherwise we want
# a repeating timer
import os
from unittest.mock import Mock
import matplotlib.pyplot as plt
# increase pause duration on CI to let things spin up
# particularly relevant for gtk3cairo
pause_time = 2 if os.getenv("CI") else 0.5
fig = plt.figure()
plt.pause(pause_time)
timer = fig.canvas.new_timer(0.1)
mock = Mock()
timer.add_callback(mock)
timer.start()
plt.pause(pause_time)
timer.stop()
assert mock.call_count > 1

# Now turn it into a single shot timer and verify only one gets triggered
mock.call_count = 0
timer.single_shot = True
timer.start()
plt.pause(pause_time)
assert mock.call_count == 1

# Make sure we can start the timer a second time
timer.start()
plt.pause(pause_time)
assert mock.call_count == 2
plt.close("all")


@pytest.mark.parametrize("env", _get_testable_interactive_backends())
def test_interactive_timers(env):
if env["MPLBACKEND"] == "gtk3cairo" and os.getenv("CI"):
pytest.skip("gtk3cairo timers do not work in remote CI")
if env["MPLBACKEND"] == "wx":
pytest.skip("wx backend is deprecated; tests failed on appveyor")
_run_helper(_impl_test_interactive_timers,
timeout=_test_timeout, extra_env=env)
73 changes: 16 additions & 57 deletions src/_macosx.m
Original file line number Diff line number Diff line change
Expand Up @@ -1811,93 +1811,53 @@ - (void)flagsChanged:(NSEvent *)event

typedef struct {
PyObject_HEAD
CFRunLoopTimerRef timer;
NSTimer* timer;

} Timer;

static PyObject*
Timer_new(PyTypeObject* type, PyObject *args, PyObject *kwds)
{
lazy_init();
Timer* self = (Timer*)type->tp_alloc(type, 0);
if (!self) { return NULL; }
if (!self) {
return NULL;
}
self->timer = NULL;
return (PyObject*) self;
}

static PyObject*
Timer_repr(Timer* self)
{
return PyUnicode_FromFormat("Timer object %p wrapping CFRunLoopTimerRef %p",
return PyUnicode_FromFormat("Timer object %p wrapping NSTimer %p",
(void*) self, (void*)(self->timer));
}

static void timer_callback(CFRunLoopTimerRef timer, void* info)
{
gil_call_method(info, "_on_timer");
}

static void context_cleanup(const void* info)
{
Py_DECREF((PyObject*)info);
}

static PyObject*
Timer__timer_start(Timer* self, PyObject* args)
{
CFRunLoopRef runloop;
CFRunLoopTimerRef timer;
CFRunLoopTimerContext context;
CFAbsoluteTime firstFire;
CFTimeInterval interval;
NSTimeInterval interval;
PyObject* py_interval = NULL, * py_single = NULL, * py_on_timer = NULL;
int single;
runloop = CFRunLoopGetMain();
if (!runloop) {
PyErr_SetString(PyExc_RuntimeError, "Failed to obtain run loop");
return NULL;
}
if (!(py_interval = PyObject_GetAttrString((PyObject*)self, "_interval"))
|| ((interval = PyFloat_AsDouble(py_interval) / 1000.), PyErr_Occurred())
|| !(py_single = PyObject_GetAttrString((PyObject*)self, "_single"))
|| ((single = PyObject_IsTrue(py_single)) == -1)
|| !(py_on_timer = PyObject_GetAttrString((PyObject*)self, "_on_timer"))) {
goto exit;
}
// (current time + interval) is time of first fire.
firstFire = CFAbsoluteTimeGetCurrent() + interval;
if (single) {
interval = 0;
}
if (!PyMethod_Check(py_on_timer)) {
PyErr_SetString(PyExc_RuntimeError, "_on_timer should be a Python method");
goto exit;
}
Py_INCREF(self);
context.version = 0;
context.retain = NULL;
context.release = context_cleanup;
context.copyDescription = NULL;
context.info = self;
timer = CFRunLoopTimerCreate(kCFAllocatorDefault,
firstFire,
interval,
0,
0,
timer_callback,
&context);
if (!timer) {
PyErr_SetString(PyExc_RuntimeError, "Failed to create timer");
goto exit;
}
if (self->timer) {
CFRunLoopTimerInvalidate(self->timer);
CFRelease(self->timer);
}
CFRunLoopAddTimer(runloop, timer, kCFRunLoopCommonModes);
/* Don't release the timer here, since the run loop may be destroyed and
* the timer lost before we have a chance to decrease the reference count
* of the attribute */
self->timer = timer;

// hold a reference to the timer so we can invalidate/stop it later
self->timer = [NSTimer scheduledTimerWithTimeInterval: interval
repeats: !single
block: ^(NSTimer *timer) {
gil_call_method((PyObject*)self, "_on_timer");
}];
exit:
Py_XDECREF(py_interval);
Py_XDECREF(py_single);
Expand All @@ -1913,8 +1873,7 @@ static void context_cleanup(const void* info)
Timer__timer_stop(Timer* self)
{
if (self->timer) {
CFRunLoopTimerInvalidate(self->timer);
CFRelease(self->timer);
[self->timer invalidate];
self->timer = NULL;
}
Py_RETURN_NONE;
Expand All @@ -1935,7 +1894,7 @@ static void context_cleanup(const void* info)
.tp_repr = (reprfunc)Timer_repr,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_new = (newfunc)Timer_new,
.tp_doc = "A Timer object wraps a CFRunLoopTimerRef and can add it to the event loop.",
.tp_doc = "A Timer object that contains an NSTimer that gets added to the event loop when started.",
.tp_methods = (PyMethodDef[]){ // All docstrings are inherited.
{"_timer_start",
(PyCFunction)Timer__timer_start,
Expand Down