8000 MNT/FIX: macosx change Timer to NSTimer instance · matplotlib/matplotlib@75b1dc4 · GitHub
[go: up one dir, main page]

Skip to content

Commit 75b1dc4

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 75b1dc4

File tree

3 files changed

+53
-57
lines changed

3 files changed

+53
-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_backends_interactive.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,3 +626,39 @@ def test_figure_leak_20490(env, time_mem):
626626

627627
growth = int(result.stdout)
628628
assert growth <= acceptable_memory_leakage
629+
630+
631+
def _impl_test_interactive_timers():
632+
# A timer with <1 millisecond gets converted to int and therefore 0
633+
# milliseconds, which the mac framework interprets as singleshot.
634+
# We only want singleshot if we specify that ourselves, otherwise we want
635+
# a repeating timer
636+
from unittest.mock import Mock
637+
import matplotlib.pyplot as plt
638+
fig = plt.figure()
639+
timer = fig.canvas.new_timer(0.1)
640+
mock = Mock()
641+
timer.add_callback(mock)
642+
timer.start()
643+
plt.pause(1)
644+
timer.stop()
645+
assert mock.call_count > 1
646+
647+
# Now turn it into a single shot timer and verify only one gets triggered
648+
mock.call_count = 0
649+
timer.single_shot = True
650+
timer.start()
651+
plt.pause(1)
652+
assert mock.call_count == 1
653+
654+
# Make sure we can start the timer a second time
655+
timer.start()
656+
plt.pause(1)
657+
assert mock.call_count == 2
658+
plt.close("all")
659+
660+
661+
@pytest.mark.parametrize("env", _get_testable_interactive_backends())
662+
def test_interactive_timers(env):
663+
_run_helper(_impl_test_interactive_timers,
664+
timeout=_test_timeout, extra_env=env)

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 NSTimer %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)
0