Skip to content

Commit 9280d50

Browse files
committed
fix: break plot<->item/canvas reference cycles to free GDI by refcount
QwtPlotItem and QwtPlotCanvas held a strong back-reference to their parent QwtPlot, forming Python reference cycles that only the cyclic garbage collector could reclaim. This delayed the release of native GDI handles and could exhaust the ~10000 GDI-object limit on Windows under heavy plot create/destroy churn. Make these back-references weak so a plot is freed by reference counting alone, like the C++ Qwt parent/child model.
1 parent 64a9401 commit 9280d50

2 files changed

Lines changed: 39 additions & 18 deletions

File tree

qwt/plot.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"""
2121

2222
import math
23+
import weakref
2324

2425
import numpy as np
2526
from qtpy.QtCore import QEvent, QObject, QRectF, QSize, Qt, Signal
@@ -1815,16 +1816,23 @@ def attach(self, plot):
18151816
18161817
:py:meth:`detach()`
18171818
"""
1818-
if plot is self.__data.plot:
1819+
if plot is self.plot():
18191820
return
18201821

1821-
if self.__data.plot:
1822-
self.__data.plot.attachItem(self, False)
1822+
current = self.plot()
1823+
if current is not None:
1824+
current.attachItem(self, False)
18231825

1824-
self.__data.plot = plot
1826+
# Store the parent plot as a WEAK reference: an item must not keep its
1827+
# plot (and the plot's GDI-backed child widgets) alive. A strong
1828+
# back-reference here forms a ``plot <-> item`` reference cycle that can
1829+
# only be reclaimed by the cyclic garbage collector, which delays the
1830+
# release of native GDI handles and can lead to GDI-handle exhaustion
1831+
# on Windows when many plots are created and destroyed.
1832+
self.__data.plot = None if plot is None else weakref.ref(plot)
18251833

1826-
if self.__data.plot:
1827-
self.__data.plot.attachItem(self, True)
1834+
if plot is not None:
1835+
plot.attachItem(self, True)
18281836

18291837
def detach(self):
18301838
"""
@@ -1854,7 +1862,8 @@ def plot(self):
18541862
"""
18551863
:return: attached plot
18561864
"""
1857-
return self.__data.plot
1865+
ref = self.__data.plot
1866+
return None if ref is None else ref()
18581867

18591868
def z(self):
18601869
"""
@@ -1881,11 +1890,12 @@ def setZ(self, z):
18811890
:py:meth:`z()`, :py:meth:`QwtPlotDict.itemList()`
18821891
"""
18831892
if self.__data.z != z:
1884-
if self.__data.plot:
1885-
self.__data.plot.attachItem(self, False)
1893+
plot = self.plot()
1894+
if plot is not None:
1895+
plot.attachItem(self, False)
18861896
self.__data.z = z
1887-
if self.__data.plot:
1888-
self.__data.plot.attachItem(self, True)
1897+
if plot is not None:
1898+
plot.attachItem(self, True)
18891899
self.itemChanged()
18901900

18911901
def setTitle(self, title):
@@ -2091,8 +2101,9 @@ def itemChanged(self):
20912101
20922102
:py:meth:`QwtPlot.legendChanged()`, :py:meth:`QwtPlot.autoRefresh()`
20932103
"""
2094-
if self.__data.plot:
2095-
self.__data.plot.autoRefresh()
2104+
plot = self.plot()
2105+
if plot is not None:
2106+
plot.autoRefresh()
20962107

20972108
def legendChanged(self):
20982109
"""
@@ -2102,8 +2113,9 @@ def legendChanged(self):
21022113
21032114
:py:meth:`QwtPlot.updateLegend()`, :py:meth:`itemChanged()`
21042115
"""
2105-
if self.testItemAttribute(QwtPlotItem.Legend) and self.__data.plot:
2106-
self.__data.plot.updateLegend(self)
2116+
plot = self.plot()
2117+
if self.testItemAttribute(QwtPlotItem.Legend) and plot is not None:
2118+
plot.updateLegend(self)
21072119

21082120
def setAxes(self, xAxis, yAxis):
21092121
"""

qwt/plot_canvas.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
:members:
1414
"""
1515

16+
import weakref
1617
from collections.abc import Sequence
1718

1819
from qtpy.QtCore import QEvent, QObject, QPoint, QPointF, QRect, QRectF, QSize, Qt
@@ -442,7 +443,12 @@ class QwtPlotCanvas(QFrame):
442443

443444
def __init__(self, plot=None):
444445
super(QwtPlotCanvas, self).__init__(plot)
445-
self.__plot = plot
446+
# Store the parent plot as a WEAK reference to avoid a
447+
# ``plot <-> canvas`` reference cycle (the plot already owns the canvas
448+
# through Qt's parent/child relationship). A strong back-reference here
449+
# would keep the plot alive until the cyclic garbage collector runs,
450+
# delaying the release of native GDI handles on Windows.
451+
self.__plot = None if plot is None else weakref.ref(plot)
446452
self.setFrameStyle(QFrame.Panel | QFrame.Sunken)
447453
self.setLineWidth(2)
448454
self.__data = QwtPlotCanvas_PrivateData()
@@ -456,7 +462,8 @@ def plot(self):
456462
"""
457463
:return: Parent plot widget
458464
"""
459-
return self.__plot
465+
ref = self.__plot
466+
return None if ref is None else ref()
460467

461468
def setPaintAttribute(self, attribute, on=True):
462469
"""
@@ -695,7 +702,9 @@ def drawCanvas(self, painter, withBackground):
695702
else:
696703
# print('**DEBUG: QwtPlotCanvas.drawCanvas')
697704
painter.setClipRect(self.contentsRect(), Qt.IntersectClip)
698-
self.plot().drawCanvas(painter)
705+
plot = self.plot()
706+
if plot is not None:
707+
plot.drawCanvas(painter)
699708
painter.restore()
700709
if withBackground and hackStyledBackground:
701710
# Now paint the border on top

0 commit comments

Comments
 (0)