Skip to content

Commit a0e50b2

Browse files
committed
MNT/FIX: macosx change Timer to NSTimer instance
Newer macos frameworks now support blocks, so we can take advantage of that and inline our method call while creating and scheduling the timer all at once. This removes some of the reference counting and alloc/dealloc record keeping. In the mac framework, an interval of 0 will only fire once, so if we aren't a singleshot timer we need to ensure that the interval is greater than 0.
1 parent bfaa6eb commit a0e50b2

File tree

3 files changed

+41
-57
lines changed

3 files changed

+41
-57
lines changed

lib/matplotlib/backends/backend_macosx.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def callback_func(callback, timer):
6969
self._timers.remove(timer)
7070
timer.stop()
7171
timer = self.new_timer(interval=0)
72+
timer.single_shot = True
7273
timer.add_callback(callback_func, callback, timer)
7374
self._timers.add(timer)
7475
timer.start()

lib/matplotlib/tests/test_backend_macosx.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
from unittest.mock import Mock
23

34
import pytest
45

@@ -44,3 +45,26 @@ def new_choose_save_file(title, directory, filename):
4445
# Check the savefig.directory rcParam got updated because
4546
# we added a subdirectory "test"
4647
assert mpl.rcParams["savefig.directory"] == f"{tmp_path}/test"
48+
49+
50+
@pytest.mark.backend('macosx')
51+
def test_timers():
52+
# A timer with <1 millisecond gets converted to int and therefore 0
53+
# milliseconds, which the mac framework interprets as singleshot.
54+
# We only want singleshot if we specify that ourselves, otherwise we want
55+
# a repeating timer
56+
fig = plt.figure()
57+
timer = fig.canvas.new_timer(0.1)
58+
mock = Mock()
59+
timer.add_callback(mock)
60+
timer.start()
61+
plt.pause(0.1)
62+
timer.stop()
63+
assert mock.call_count > 1
64+
65+
# Now turn it into a single shot timer and verify only one gets triggered
66+
mock.call_count = 0
67+
timer.single_shot = True
68+
timer.start()
69+
plt.pause(0.1)
70+
assert mock.call_count == 1

src/_macosx.m

Lines changed: 16 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1811,93 +1811,53 @@ - (void)flagsChanged:(NSEvent *)event
18111811

18121812
typedef struct {
18131813
PyObject_HEAD
1814-
CFRunLoopTimerRef timer;
1814+
NSTimer* timer;
1815+
18151816
} Timer;
18161817

18171818
static PyObject*
18181819
Timer_new(PyTypeObject* type, PyObject *args, PyObject *kwds)
18191820
{
18201821
lazy_init();
18211822
Timer* self = (Timer*)type->tp_alloc(type, 0);
1822-
if (!self) { return NULL; }
1823+
if (!self) {
1824+
return NULL;
1825+
}
18231826
self->timer = NULL;
18241827
return (PyObject*) self;
18251828
}
18261829

18271830
static PyObject*
18281831
Timer_repr(Timer* self)
18291832
{
1830-
return PyUnicode_FromFormat("Timer object %p wrapping CFRunLoopTimerRef %p",
1833+
return PyUnicode_FromFormat("Timer object %p wrapping NSTimerRef %p",
18311834
(void*) self, (void*)(self->timer));
18321835
}
18331836

1834-
static void timer_callback(CFRunLoopTimerRef timer, void* info)
1835-
{
1836-
gil_call_method(info, "_on_timer");
1837-
}
1838-
1839-
static void context_cleanup(const void* info)
1840-
{
1841-
Py_DECREF((PyObject*)info);
1842-
}
1843-
18441837
static PyObject*
18451838
Timer__timer_start(Timer* self, PyObject* args)
18461839
{
1847-
CFRunLoopRef runloop;
1848-
CFRunLoopTimerRef timer;
1849-
CFRunLoopTimerContext context;
1850-
CFAbsoluteTime firstFire;
1851-
CFTimeInterval interval;
1840+
NSTimeInterval interval;
18521841
PyObject* py_interval = NULL, * py_single = NULL, * py_on_timer = NULL;
18531842
int single;
1854-
runloop = CFRunLoopGetMain();
1855-
if (!runloop) {
1856-
PyErr_SetString(PyExc_RuntimeError, "Failed to obtain run loop");
1857-
return NULL;
1858-
}
18591843
if (!(py_interval = PyObject_GetAttrString((PyObject*)self, "_interval"))
18601844
|| ((interval = PyFloat_AsDouble(py_interval) / 1000.), PyErr_Occurred())
18611845
|| !(py_single = PyObject_GetAttrString((PyObject*)self, "_single"))
18621846
|| ((single = PyObject_IsTrue(py_single)) == -1)
18631847
|| !(py_on_timer = PyObject_GetAttrString((PyObject*)self, "_on_timer"))) {
18641848
goto exit;
18651849
}
1866-
// (current time + interval) is time of first fire.
1867-
firstFire = CFAbsoluteTimeGetCurrent() + interval;
1868-
if (single) {
1869-
interval = 0;
1870-
}
18711850
if (!PyMethod_Check(py_on_timer)) {
18721851
PyErr_SetString(PyExc_RuntimeError, "_on_timer should be a Python method");
18731852
goto exit;
18741853
}
1875-
Py_INCREF(self);
1876-
context.version = 0;
1877-
context.retain = NULL;
1878-
context.release = context_cleanup;
1879-
context.copyDescription = NULL;
1880-
context.info = self;
1881-
timer = CFRunLoopTimerCreate(kCFAllocatorDefault,
1882-
firstFire,
1883-
interval,
1884-
0,
1885-
0,
1886-
timer_callback,
1887-
&context);
1888-
if (!timer) {
1889-
PyErr_SetString(PyExc_RuntimeError, "Failed to create timer");
1890-
goto exit;
1891-
}
1892-
if (self->timer) {
1893-
CFRunLoopTimerInvalidate(self->timer);
1894-
CFRelease(self->timer);
1895-
}
1896-
CFRunLoopAddTimer(runloop, timer, kCFRunLoopCommonModes);
1897-
/* Don't release the timer here, since the run loop may be destroyed and
1898-
* the timer lost before we have a chance to decrease the reference count
1899-
* of the attribute */
1900-
self->timer = timer;
1854+
1855+
// hold a reference to the timer so we can invalidate/stop it later
1856+
self->timer = [NSTimer scheduledTimerWithTimeInterval: interval
1857+
repeats: !single
1858+
block: ^(NSTimer *timer) {
1859+
gil_call_method((PyObject*)self, "_on_timer");
1860+
}];
19011861
exit:
19021862
Py_XDECREF(py_interval);
19031863
Py_XDECREF(py_single);
@@ -1913,8 +1873,7 @@ static void context_cleanup(const void* info)
19131873
Timer__timer_stop(Timer* self)
19141874
{
19151875
if (self->timer) {
1916-
CFRunLoopTimerInvalidate(self->timer);
1917-
CFRelease(self->timer);
1876+
[self->timer invalidate];
19181877
self->timer = NULL;
19191878
}
19201879
Py_RETURN_NONE;
@@ -1935,7 +1894,7 @@ static void context_cleanup(const void* info)
19351894
.tp_repr = (reprfunc)Timer_repr,
19361895
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
19371896
.tp_new = (newfunc)Timer_new,
1938-
.tp_doc = "A Timer object wraps a CFRunLoopTimerRef and can add it to the event loop.",
1897+
.tp_doc = "A Timer object that contains an NSTimer that gets added to the event loop when started.",
19391898
.tp_methods = (PyMethodDef[]){ // All docstrings are inherited.
19401899
{"_timer_start",
19411900
(PyCFunction)Timer__timer_start,

0 commit comments

Comments
 (0)