From 8dd7f07158a2bb9135c3f056fb614659b293b3a5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 3 Apr 2014 13:37:07 -0400 Subject: [PATCH 1/8] Start stability tests: - randomly add / remove widgets and graphicsitems to provoke a crash - create and delete various objects and ensure that nothing is left in memory --- pyqtgraph/tests/test_ref_cycles.py | 45 ++++++++++++++++++++++++++++++ pyqtgraph/tests/test_stability.py | 39 ++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 pyqtgraph/tests/test_ref_cycles.py create mode 100644 pyqtgraph/tests/test_stability.py diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py new file mode 100644 index 00000000..ac65eaf9 --- /dev/null +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -0,0 +1,45 @@ +""" +Test for unwanted reference cycles + +""" +import pyqtgraph as pg +import numpy as np +import gc +app = pg.mkQApp() + +def processEvents(): + for i in range(3): + gc.collect() + app.processEvents() + # processEvents ignored DeferredDelete events; we must process these + # manually. + app.sendPostedEvents(None, pg.QtCore.QEvent.DeferredDelete) + +def test_PlotItem(): + for i in range(10): + plt = pg.PlotItem() + plt.plot(np.random.normal(size=10000)) + processEvents() + + ot = pg.debug.ObjTracker() + + plots = [] + for i in range(10): + plt = pg.PlotItem() + plt.plot(np.random.normal(size=10000)) + plots.append(plt) + processEvents() + + ot.diff() + + del plots + processEvents() + + ot.diff() + + + return ot + + +if __name__ == '__main__': + ot = test_PlotItem() diff --git a/pyqtgraph/tests/test_stability.py b/pyqtgraph/tests/test_stability.py new file mode 100644 index 00000000..3838af7d --- /dev/null +++ b/pyqtgraph/tests/test_stability.py @@ -0,0 +1,39 @@ +""" +PyQt/PySide stress test: + +Create lots of random widgets and graphics items, connect them together randomly, +the tear them down repeatedly. + +The purpose of this is to attempt to generate segmentation faults. +""" +import pyqtgraph as pg +import random + +random.seed(12345) + +widgetTypes = [pg.PlotWidget, pg.ImageView, pg.GraphicsView, pg.QtGui.QWidget, + pg.QtGui.QTreeWidget, pg.QtGui.QPushButton] + +itemTypes = [pg.PlotCurveItem, pg.ImageItem, pg.PlotDataItem, pg.ViewBox, + pg.QtGui.QGraphicsRectItem] + +while True: + action = random.randint(0,5) + if action == 0: + # create a widget + pass + elif action == 1: + # set parent (widget or None), possibly create a reference in either direction + pass + elif action == 2: + # + pass + elif action == 3: + pass + + + + + + + From 82bd5ef584e782ca08888a0c29a93a6901aa6a55 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 13 Apr 2014 22:51:54 -0400 Subject: [PATCH 2/8] stability test is successfully crashing, ref_cycle test exposes cycles! --- pyqtgraph/tests/test_ref_cycles.py | 51 +++++---- pyqtgraph/tests/test_stability.py | 162 ++++++++++++++++++++++++----- 2 files changed, 170 insertions(+), 43 deletions(-) diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index ac65eaf9..87e0d71d 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -4,7 +4,7 @@ Test for unwanted reference cycles """ import pyqtgraph as pg import numpy as np -import gc +import gc, weakref app = pg.mkQApp() def processEvents(): @@ -15,31 +15,46 @@ def processEvents(): # manually. app.sendPostedEvents(None, pg.QtCore.QEvent.DeferredDelete) -def test_PlotItem(): - for i in range(10): - plt = pg.PlotItem() - plt.plot(np.random.normal(size=10000)) - processEvents() +#def test_PlotItem(): + #for i in range(10): + #plt = pg.PlotItem() + #plt.plot(np.random.normal(size=10000)) + #processEvents() - ot = pg.debug.ObjTracker() + #ot = pg.debug.ObjTracker() - plots = [] - for i in range(10): - plt = pg.PlotItem() - plt.plot(np.random.normal(size=10000)) - plots.append(plt) - processEvents() + #plots = [] + #for i in range(10): + #plt = pg.PlotItem() + #plt.plot(np.random.normal(size=10000)) + #plots.append(plt) + #processEvents() - ot.diff() + #ot.diff() - del plots - processEvents() + #del plots + #processEvents() - ot.diff() + #ot.diff() - return ot + #return ot +def test_PlotWidget(): + def mkref(*args, **kwds): + iv = pg.PlotWidget(*args, **kwds) + return weakref.ref(iv) + + for i in range(100): + assert mkref()() is None + +def test_ImageView(): + def mkref(*args, **kwds): + iv = pg.ImageView(*args, **kwds) + return weakref.ref(iv) + + for i in range(100): + assert mkref()() is None if __name__ == '__main__': ot = test_PlotItem() diff --git a/pyqtgraph/tests/test_stability.py b/pyqtgraph/tests/test_stability.py index 3838af7d..709080ac 100644 --- a/pyqtgraph/tests/test_stability.py +++ b/pyqtgraph/tests/test_stability.py @@ -6,34 +6,146 @@ the tear them down repeatedly. The purpose of this is to attempt to generate segmentation faults. """ +from PyQt4.QtTest import QTest import pyqtgraph as pg -import random +from random import seed, randint +import sys, gc, weakref -random.seed(12345) +app = pg.mkQApp() -widgetTypes = [pg.PlotWidget, pg.ImageView, pg.GraphicsView, pg.QtGui.QWidget, - pg.QtGui.QTreeWidget, pg.QtGui.QPushButton] +seed(12345) -itemTypes = [pg.PlotCurveItem, pg.ImageItem, pg.PlotDataItem, pg.ViewBox, - pg.QtGui.QGraphicsRectItem] +widgetTypes = [ + pg.PlotWidget, + pg.ImageView, + pg.GraphicsView, + pg.QtGui.QWidget, + pg.QtGui.QTreeWidget, + pg.QtGui.QPushButton, + ] + +itemTypes = [ + pg.PlotCurveItem, + pg.ImageItem, + pg.PlotDataItem, + pg.ViewBox, + pg.QtGui.QGraphicsRectItem + ] + +widgets = [] +items = [] +allWidgets = weakref.WeakSet() + +def test_stability(): + global allWidgets + try: + gc.disable() + actions = [ + createWidget, + #setParent, + forgetWidget, + showWidget, + processEvents, + #raiseException, + #addReference, + ] + + thread = WorkThread() + #thread.start() + + while True: + try: + action = randItem(actions) + action() + print('[%d widgets alive]' % len(allWidgets)) + except KeyboardInterrupt: + thread.kill() + break + except: + sys.excepthook(*sys.exc_info()) + finally: + gc.enable() + + + +class WorkThread(pg.QtCore.QThread): + '''Intended to give the gc an opportunity to run from a non-gui thread.''' + def run(self): + i = 0 + while True: + i += 1 + if (i % 1000000) == 0: + print('--worker--') + + +def randItem(items): + return items[randint(0, len(items)-1)] + +def p(msg): + print(msg) + sys.stdout.flush() + +def createWidget(): + p('create widget') + global widgets, allWidgets + widget = randItem(widgetTypes)() + widgets.append(widget) + allWidgets.add(widget) + p(" %s" % widget) + return widget + +def setParent(): + p('set parent') + global widgets + if len(widgets) < 2: + return + child = parent = None + while child is parent: + child = randItem(widgets) + parent = randItem(widgets) + p(" %s parent of %s" % (parent, child)) + child.setParent(parent) + +def forgetWidget(): + p('forget widget') + global widgets + if len(widgets) < 1: + return + widget = randItem(widgets) + p(' %s' % widget) + widgets.remove(widget) + +def showWidget(): + p('show widget') + global widgets + if len(widgets) < 1: + return + widget = randItem(widgets) + p(' %s' % widget) + widget.show() + +def processEvents(): + p('process events') + QTest.qWait(25) + +class TestException(Exception): + pass + +def raiseException(): + p('raise exception') + raise TestException("A test exception") + +def addReference(): + p('add reference') + global widgets + if len(widgets) < 1: + return + obj1 = randItem(widgets) + obj2 = randItem(widgets) + p(' %s -> %s' % (obj1, obj2)) + obj1._testref = obj2 + -while True: - action = random.randint(0,5) - if action == 0: - # create a widget - pass - elif action == 1: - # set parent (widget or None), possibly create a reference in either direction - pass - elif action == 2: - # - pass - elif action == 3: - pass - - - - - - +if __name__ == '__main__': + test_stability() \ No newline at end of file From ef7949a35eca727f3f4d4bc05d684871db640b85 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 13 Apr 2014 22:59:57 -0400 Subject: [PATCH 3/8] Fixed ref cycle in SignalProxy --- pyqtgraph/SignalProxy.py | 5 +++-- pyqtgraph/tests/test_ref_cycles.py | 4 ++-- pyqtgraph/tests/test_stability.py | 10 +++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/SignalProxy.py b/pyqtgraph/SignalProxy.py index 6f9b9112..d36282fa 100644 --- a/pyqtgraph/SignalProxy.py +++ b/pyqtgraph/SignalProxy.py @@ -2,6 +2,7 @@ from .Qt import QtCore from .ptime import time from . import ThreadsafeTimer +import weakref __all__ = ['SignalProxy'] @@ -34,7 +35,7 @@ class SignalProxy(QtCore.QObject): self.timer = ThreadsafeTimer.ThreadsafeTimer() self.timer.timeout.connect(self.flush) self.block = False - self.slot = slot + self.slot = weakref.ref(slot) self.lastFlushTime = None if slot is not None: self.sigDelayed.connect(slot) @@ -80,7 +81,7 @@ class SignalProxy(QtCore.QObject): except: pass try: - self.sigDelayed.disconnect(self.slot) + self.sigDelayed.disconnect(self.slot()) except: pass diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index 87e0d71d..2154632e 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -45,7 +45,7 @@ def test_PlotWidget(): iv = pg.PlotWidget(*args, **kwds) return weakref.ref(iv) - for i in range(100): + for i in range(5): assert mkref()() is None def test_ImageView(): @@ -53,7 +53,7 @@ def test_ImageView(): iv = pg.ImageView(*args, **kwds) return weakref.ref(iv) - for i in range(100): + for i in range(5): assert mkref()() is None if __name__ == '__main__': diff --git a/pyqtgraph/tests/test_stability.py b/pyqtgraph/tests/test_stability.py index 709080ac..dfcfb15e 100644 --- a/pyqtgraph/tests/test_stability.py +++ b/pyqtgraph/tests/test_stability.py @@ -41,11 +41,11 @@ def test_stability(): try: gc.disable() actions = [ - createWidget, - #setParent, - forgetWidget, - showWidget, - processEvents, + createWidget, + #setParent, + forgetWidget, + showWidget, + #processEvents, #raiseException, #addReference, ] From d45eadc9a5faaea03fd2d7f911fbb1f9302fae70 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 15 Apr 2014 15:10:04 -0400 Subject: [PATCH 4/8] update stability test --- pyqtgraph/tests/test_stability.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/tests/test_stability.py b/pyqtgraph/tests/test_stability.py index dfcfb15e..6d9acd0d 100644 --- a/pyqtgraph/tests/test_stability.py +++ b/pyqtgraph/tests/test_stability.py @@ -45,22 +45,27 @@ def test_stability(): #setParent, forgetWidget, showWidget, - #processEvents, + processEvents, #raiseException, #addReference, ] thread = WorkThread() - #thread.start() + thread.start() while True: try: action = randItem(actions) action() - print('[%d widgets alive]' % len(allWidgets)) + print('[%d widgets alive, %d zombie]' % (len(allWidgets), len(allWidgets) - len(widgets))) except KeyboardInterrupt: - thread.kill() - break + print("Caught interrupt; send another to exit.") + try: + for i in range(100): + QTest.qWait(100) + except KeyboardInterrupt: + thread.terminate() + break except: sys.excepthook(*sys.exc_info()) finally: @@ -88,7 +93,10 @@ def p(msg): def createWidget(): p('create widget') global widgets, allWidgets + if len(widgets) > 50: + return widget = randItem(widgetTypes)() + widget.setWindowTitle(widget.__class__.__name__) widgets.append(widget) allWidgets.add(widget) p(" %s" % widget) From 0479507dbbba308e9b416e917cf5aca9310a3164 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 27 Apr 2014 13:07:31 -0400 Subject: [PATCH 5/8] Expanded ref checks Fixed ref cycle in ImageItem -> HistogramLutItem --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 17 +++--- pyqtgraph/tests/test_ref_cycles.py | 66 +++++++++------------ 2 files changed, 36 insertions(+), 47 deletions(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 71577422..6a915902 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -17,6 +17,7 @@ from .. import functions as fn import numpy as np from .. import debug as debug +import weakref __all__ = ['HistogramLUTItem'] @@ -42,7 +43,7 @@ class HistogramLUTItem(GraphicsWidget): """ GraphicsWidget.__init__(self) self.lut = None - self.imageItem = None + self.imageItem = lambda: None # fake a dead weakref self.layout = QtGui.QGraphicsGridLayout() self.setLayout(self.layout) @@ -138,7 +139,7 @@ class HistogramLUTItem(GraphicsWidget): #self.region.setBounds([vr.top(), vr.bottom()]) def setImageItem(self, img): - self.imageItem = img + self.imageItem = weakref.ref(img) img.sigImageChanged.connect(self.imageChanged) img.setLookupTable(self.getLookupTable) ## send function pointer, not the result #self.gradientChanged() @@ -150,11 +151,11 @@ class HistogramLUTItem(GraphicsWidget): self.update() def gradientChanged(self): - if self.imageItem is not None: + if self.imageItem() is not None: if self.gradient.isLookupTrivial(): - self.imageItem.setLookupTable(None) #lambda x: x.astype(np.uint8)) + self.imageItem().setLookupTable(None) #lambda x: x.astype(np.uint8)) else: - self.imageItem.setLookupTable(self.getLookupTable) ## send function pointer, not the result + self.imageItem().setLookupTable(self.getLookupTable) ## send function pointer, not the result self.lut = None #if self.imageItem is not None: @@ -178,14 +179,14 @@ class HistogramLUTItem(GraphicsWidget): #self.update() def regionChanging(self): - if self.imageItem is not None: - self.imageItem.setLevels(self.region.getRegion()) + if self.imageItem() is not None: + self.imageItem().setLevels(self.region.getRegion()) self.sigLevelsChanged.emit(self) self.update() def imageChanged(self, autoLevel=False, autoRange=False): profiler = debug.Profiler() - h = self.imageItem.getHistogram() + h = self.imageItem().getHistogram() profiler('get histogram') if h[0] is None: return diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index 2154632e..3e78b382 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -7,54 +7,42 @@ import numpy as np import gc, weakref app = pg.mkQApp() -def processEvents(): - for i in range(3): - gc.collect() - app.processEvents() - # processEvents ignored DeferredDelete events; we must process these - # manually. - app.sendPostedEvents(None, pg.QtCore.QEvent.DeferredDelete) +def assert_alldead(refs): + for ref in refs: + assert ref() is None -#def test_PlotItem(): - #for i in range(10): - #plt = pg.PlotItem() - #plt.plot(np.random.normal(size=10000)) - #processEvents() - - #ot = pg.debug.ObjTracker() - - #plots = [] - #for i in range(10): - #plt = pg.PlotItem() - #plt.plot(np.random.normal(size=10000)) - #plots.append(plt) - #processEvents() - - #ot.diff() - - #del plots - #processEvents() - - #ot.diff() - - - #return ot +def mkrefs(*objs): + return map(weakref.ref, objs) def test_PlotWidget(): - def mkref(*args, **kwds): - iv = pg.PlotWidget(*args, **kwds) - return weakref.ref(iv) + def mkobjs(*args, **kwds): + w = pg.PlotWidget(*args, **kwds) + data = pg.np.array([1,5,2,4,3]) + c = w.plot(data, name='stuff') + w.addLegend() + + # test that connections do not keep objects alive + w.plotItem.vb.sigRangeChanged.connect(mkrefs) + app.focusChanged.connect(w.plotItem.vb.invertY) + + # return weakrefs to a bunch of objects that should die when the scope exits. + return mkrefs(w, c, data, w.plotItem, w.plotItem.vb, w.plotItem.getMenu(), w.plotItem.getAxis('left')) for i in range(5): - assert mkref()() is None + assert_alldead(mkobjs()) def test_ImageView(): - def mkref(*args, **kwds): - iv = pg.ImageView(*args, **kwds) - return weakref.ref(iv) + def mkobjs(): + iv = pg.ImageView() + data = np.zeros((10,10,5)) + iv.setImage(data) + + return mkrefs(iv, iv.imageItem, iv.view, iv.ui.histogram, data) for i in range(5): - assert mkref()() is None + assert_alldead(mkobjs()) + + if __name__ == '__main__': ot = test_PlotItem() From 27f24d1a6a6aa512a87e7b74517c22962ca958fd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 27 Apr 2014 13:27:25 -0400 Subject: [PATCH 6/8] Expand ref cycle check to include all child QObjects --- pyqtgraph/tests/test_ref_cycles.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index 3e78b382..9e3fee19 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -11,8 +11,27 @@ def assert_alldead(refs): for ref in refs: assert ref() is None +def qObjectTree(root): + """Return root and its entire tree of qobject children""" + childs = [root] + for ch in pg.QtCore.QObject.children(root): + childs += qObjectTree(ch) + return childs + def mkrefs(*objs): - return map(weakref.ref, objs) + """Return a list of weakrefs to each object in *objs. + QObject instances are expanded to include all child objects. + """ + allObjs = {} + for obj in objs: + if isinstance(obj, pg.QtCore.QObject): + obj = qObjectTree(obj) + else: + obj = [obj] + for o in obj: + allObjs[id(o)] = o + + return map(weakref.ref, allObjs.values()) def test_PlotWidget(): def mkobjs(*args, **kwds): From 13cca1d9cab3156845efe99b5f81ab8858cdf076 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 27 Apr 2014 13:44:58 -0400 Subject: [PATCH 7/8] Add GraphicsWindow ref check --- pyqtgraph/tests/test_ref_cycles.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index 9e3fee19..0284852c 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -61,7 +61,17 @@ def test_ImageView(): for i in range(5): assert_alldead(mkobjs()) +def test_GraphicsWindow(): + def mkobjs(): + w = pg.GraphicsWindow() + p1 = w.addPlot() + v1 = w.addViewBox() + return mkrefs(w, p1, v1) + + for i in range(5): + assert_alldead(mkobjs()) + if __name__ == '__main__': ot = test_PlotItem() From 24c25b604ac29453c9c999b1d347376f9bbdd088 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 27 Apr 2014 14:02:55 -0400 Subject: [PATCH 8/8] disabled stability test (because it is expected to crash) --- pyqtgraph/tests/test_stability.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/tests/test_stability.py b/pyqtgraph/tests/test_stability.py index 6d9acd0d..a64e30e4 100644 --- a/pyqtgraph/tests/test_stability.py +++ b/pyqtgraph/tests/test_stability.py @@ -36,7 +36,8 @@ widgets = [] items = [] allWidgets = weakref.WeakSet() -def test_stability(): + +def crashtest(): global allWidgets try: gc.disable() @@ -136,12 +137,12 @@ def processEvents(): p('process events') QTest.qWait(25) -class TestException(Exception): +class TstException(Exception): pass def raiseException(): p('raise exception') - raise TestException("A test exception") + raise TstException("A test exception") def addReference(): p('add reference')