From 01b8968a0aa6c4f140f8f7e078d840813ac51134 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 9 Jan 2013 22:21:32 -0500 Subject: [PATCH 1/3] Performance improvements: - AxisItem shows 2 tick levels instead of 3 - Lots of boundingRect and dataBounds caching (improves ViewBox auto-range performance, especially with multiple plots) - GraphicsScene avoids testing for hover intersections with non-hoverable items (much less slowdown when moving mouse over plots) These are deep changes; need good testing before we release them. --- pyqtgraph/GraphicsScene/GraphicsScene.py | 20 ++++- pyqtgraph/debug.py | 71 ++++++++++++---- pyqtgraph/graphicsItems/AxisItem.py | 2 +- pyqtgraph/graphicsItems/GraphicsItem.py | 98 ++++++++++++++++------ pyqtgraph/graphicsItems/PlotCurveItem.py | 80 +++++++++++------- pyqtgraph/graphicsItems/PlotDataItem.py | 7 +- pyqtgraph/graphicsItems/VTickGroup.py | 69 +-------------- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 81 +++++++----------- pyqtgraph/widgets/GraphicsView.py | 6 +- 9 files changed, 234 insertions(+), 200 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index a5f60059..d0a75d16 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -75,6 +75,8 @@ class GraphicsScene(QtGui.QGraphicsScene): sigMouseMoved = QtCore.Signal(object) ## emits position of mouse on every move sigMouseClicked = QtCore.Signal(object) ## emitted when mouse is clicked. Check for event.isAccepted() to see whether the event has already been acted on. + sigPrepareForPaint = QtCore.Signal() ## emitted immediately before the scene is about to be rendered + _addressCache = weakref.WeakValueDictionary() ExportDirectory = None @@ -98,6 +100,7 @@ class GraphicsScene(QtGui.QGraphicsScene): self.clickEvents = [] self.dragButtons = [] + self.prepItems = weakref.WeakKeyDictionary() ## set of items with prepareForPaintMethods self.mouseGrabber = None self.dragItem = None self.lastDrag = None @@ -112,6 +115,17 @@ class GraphicsScene(QtGui.QGraphicsScene): self.exportDialog = None + def render(self, *args): + self.prepareForPaint() + return QGraphicsScene.render(self, *args) + + def prepareForPaint(self): + """Called before every render. This method will inform items that the scene is about to + be rendered by emitting sigPrepareForPaint. + + This allows items to delay expensive processing until they know a paint will be required.""" + self.sigPrepareForPaint.emit() + def setClickRadius(self, r): """ @@ -224,7 +238,7 @@ class GraphicsScene(QtGui.QGraphicsScene): else: acceptable = int(ev.buttons()) == 0 ## if we are in mid-drag, do not allow items to accept the hover event. event = HoverEvent(ev, acceptable) - items = self.itemsNearEvent(event) + items = self.itemsNearEvent(event, hoverable=True) self.sigMouseHover.emit(items) prevItems = list(self.hoverItems.keys()) @@ -402,7 +416,7 @@ class GraphicsScene(QtGui.QGraphicsScene): #return item return self.translateGraphicsItem(item) - def itemsNearEvent(self, event, selMode=QtCore.Qt.IntersectsItemShape, sortOrder=QtCore.Qt.DescendingOrder): + def itemsNearEvent(self, event, selMode=QtCore.Qt.IntersectsItemShape, sortOrder=QtCore.Qt.DescendingOrder, hoverable=False): """ Return an iterator that iterates first through the items that directly intersect point (in Z order) followed by any other items that are within the scene's click radius. @@ -429,6 +443,8 @@ class GraphicsScene(QtGui.QGraphicsScene): ## remove items whose shape does not contain point (scene.items() apparently sucks at this) items2 = [] for item in items: + if hoverable and not hasattr(item, 'hoverEvent'): + continue shape = item.shape() if shape is None: continue diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index ea9157aa..d5f86139 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -356,8 +356,14 @@ class GarbageWatcher(object): return self.objs[item] -class Profiler(object): +class Profiler: """Simple profiler allowing measurement of multiple time intervals. + Arguments: + msg: message to print at start and finish of profiling + disabled: If true, profiler does nothing (so you can leave it in place) + delayed: If true, all messages are printed after call to finish() + (this can result in more accurate time step measurements) + globalDelay: if True, all nested profilers delay printing until the top level finishes Example: prof = Profiler('Function') @@ -368,34 +374,65 @@ class Profiler(object): prof.finish() """ depth = 0 + msgs = [] - def __init__(self, msg="Profiler", disabled=False): - self.depth = Profiler.depth - Profiler.depth += 1 - + def __init__(self, msg="Profiler", disabled=False, delayed=True, globalDelay=True): self.disabled = disabled if disabled: return + + self.markCount = 0 + self.finished = False + self.depth = Profiler.depth + Profiler.depth += 1 + if not globalDelay: + self.msgs = [] + self.delayed = delayed + self.msg = " "*self.depth + msg + msg2 = self.msg + " >>> Started" + if self.delayed: + self.msgs.append(msg2) + else: + print msg2 self.t0 = ptime.time() self.t1 = self.t0 - self.msg = " "*self.depth + msg - print(self.msg, ">>> Started") - def mark(self, msg=''): + def mark(self, msg=None): if self.disabled: return - t1 = ptime.time() - print(" "+self.msg, msg, "%gms" % ((t1-self.t1)*1000)) - self.t1 = t1 - def finish(self): - if self.disabled: + if msg is None: + msg = str(self.markCount) + self.markCount += 1 + + t1 = ptime.time() + msg2 = " "+self.msg+" "+msg+" "+"%gms" % ((t1-self.t1)*1000) + if self.delayed: + self.msgs.append(msg2) + else: + print msg2 + self.t1 = ptime.time() ## don't measure time it took to print + + def finish(self, msg=None): + if self.disabled or self.finished: return - t1 = ptime.time() - print(self.msg, '<<< Finished, total time:', "%gms" % ((t1-self.t0)*1000)) - def __del__(self): - Profiler.depth -= 1 + if msg is not None: + self.mark(msg) + t1 = ptime.time() + msg = self.msg + ' <<< Finished, total time: %gms' % ((t1-self.t0)*1000) + if self.delayed: + self.msgs.append(msg) + if self.depth == 0: + for line in self.msgs: + print line + Profiler.msgs = [] + else: + print msg + Profiler.depth = self.depth + self.finished = True + + def profile(code, name='profile_run', sort='cumulative', num=30): diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 82cbcfae..7be3e30f 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -369,7 +369,7 @@ class AxisItem(GraphicsWidget): return [ (intervals[minorIndex+2], 0), (intervals[minorIndex+1], 0), - (intervals[minorIndex], 0) + #(intervals[minorIndex], 0) ## Pretty, but eats up CPU ] ##### This does not work -- switching between 2/5 confuses the automatic text-level-selection diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 2018fb4c..2314709d 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -3,8 +3,30 @@ from pyqtgraph.GraphicsScene import GraphicsScene from pyqtgraph.Point import Point import pyqtgraph.functions as fn import weakref +from pyqtgraph.pgcollections import OrderedDict import operator +class FiniteCache(OrderedDict): + """Caches a finite number of objects, removing + least-frequently used items.""" + def __init__(self, length): + self._length = length + OrderedDict.__init__(self) + + def __setitem__(self, item, val): + self.pop(item, None) # make sure item is added to end + OrderedDict.__setitem__(self, item, val) + while len(self) > self._length: + del self[self.keys()[0]] + + def __getitem__(self, item): + val = dict.__getitem__(self, item) + del self[item] + self[item] = val ## promote this key + return val + + + class GraphicsItem(object): """ **Bases:** :class:`object` @@ -16,6 +38,8 @@ class GraphicsItem(object): The GraphicsView system places a lot of emphasis on the notion that the graphics within the scene should be device independent--you should be able to take the same graphics and display them on screens of different resolutions, printers, export to SVG, etc. This is nice in principle, but causes me a lot of headache in practice. It means that I have to circumvent all the device-independent expectations any time I want to operate in pixel coordinates rather than arbitrary scene coordinates. A lot of the code in GraphicsItem is devoted to this task--keeping track of view widgets and device transforms, computing the size and shape of a pixel in local item coordinates, etc. Note that in item coordinates, a pixel does not have to be square or even rectangular, so just asking how to increase a bounding rect by 2px can be a rather complex task. """ + _pixelVectorGlobalCache = FiniteCache(100) + def __init__(self, register=True): if not hasattr(self, '_qtBaseClass'): for b in self.__class__.__bases__: @@ -25,6 +49,7 @@ class GraphicsItem(object): if not hasattr(self, '_qtBaseClass'): raise Exception('Could not determine Qt base class for GraphicsItem: %s' % str(self)) + self._pixelVectorCache = [None, None] self._viewWidget = None self._viewBox = None self._connectedView = None @@ -155,7 +180,6 @@ class GraphicsItem(object): - def pixelVectors(self, direction=None): """Return vectors in local coordinates representing the width and height of a view pixel. If direction is specified, then return vectors parallel and orthogonal to it. @@ -163,13 +187,28 @@ class GraphicsItem(object): Return (None, None) if pixel size is not yet defined (usually because the item has not yet been displayed) or if pixel size is below floating-point precision limit. """ - + + ## This is an expensive function that gets called very frequently. + ## We have two levels of cache to try speeding things up. + dt = self.deviceTransform() if dt is None: return None, None + ## check local cache + if direction is None and dt == self._pixelVectorCache[0]: + return self._pixelVectorCache[1] + + ## check global cache + key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32()) + pv = self._pixelVectorGlobalCache.get(key, None) + if pv is not None: + self._pixelVectorCache = [dt, pv] + return pv + + if direction is None: - direction = Point(1, 0) + direction = QtCore.QPointF(1, 0) if direction.manhattanLength() == 0: raise Exception("Cannot compute pixel length for 0-length vector.") @@ -184,28 +223,33 @@ class GraphicsItem(object): r = ((abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))) * (abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))))**0.5 directionr = direction * r - viewDir = Point(dt.map(directionr) - dt.map(Point(0,0))) - if viewDir.manhattanLength() == 0: + ## map direction vector onto device + #viewDir = Point(dt.map(directionr) - dt.map(Point(0,0))) + #mdirection = dt.map(directionr) + dirLine = QtCore.QLineF(QtCore.QPointF(0,0), directionr) + viewDir = dt.map(dirLine) + if viewDir.length() == 0: return None, None ## pixel size cannot be represented on this scale - - orthoDir = Point(viewDir[1], -viewDir[0]) ## orthogonal to line in pixel-space - + + ## get unit vector and orthogonal vector (length of pixel) + #orthoDir = Point(viewDir[1], -viewDir[0]) ## orthogonal to line in pixel-space try: - normView = viewDir.norm() ## direction of one pixel orthogonal to line - normOrtho = orthoDir.norm() + normView = viewDir.unitVector() + #normView = viewDir.norm() ## direction of one pixel orthogonal to line + normOrtho = normView.normalVector() + #normOrtho = orthoDir.norm() except: raise Exception("Invalid direction %s" %directionr) - + ## map back to item dti = fn.invertQTransform(dt) - return Point(dti.map(normView)-dti.map(Point(0,0))), Point(dti.map(normOrtho)-dti.map(Point(0,0))) + #pv = Point(dti.map(normView)-dti.map(Point(0,0))), Point(dti.map(normOrtho)-dti.map(Point(0,0))) + pv = Point(dti.map(normView).p2()), Point(dti.map(normOrtho).p2()) + self._pixelVectorCache[1] = pv + self._pixelVectorCache[0] = dt + self._pixelVectorGlobalCache[key] = pv + return self._pixelVectorCache[1] - #vt = self.deviceTransform() - #if vt is None: - #return None - #vt = vt.inverted()[0] - #orig = vt.map(QtCore.QPointF(0, 0)) - #return vt.map(QtCore.QPointF(1, 0))-orig, vt.map(QtCore.QPointF(0, 1))-orig def pixelLength(self, direction, ortho=False): """Return the length of one pixel in the direction indicated (in local coordinates) @@ -220,7 +264,6 @@ class GraphicsItem(object): return orthoV.length() return normV.length() - def pixelSize(self): ## deprecated @@ -235,7 +278,7 @@ class GraphicsItem(object): if vt is None: return 0 vt = fn.invertQTransform(vt) - return Point(vt.map(QtCore.QPointF(1, 0))-vt.map(QtCore.QPointF(0, 0))).length() + return vt.map(QtCore.QLineF(0, 0, 1, 0)).length() def pixelHeight(self): ## deprecated @@ -243,7 +286,8 @@ class GraphicsItem(object): if vt is None: return 0 vt = fn.invertQTransform(vt) - return Point(vt.map(QtCore.QPointF(0, 1))-vt.map(QtCore.QPointF(0, 0))).length() + return vt.map(QtCore.QLineF(0, 0, 0, 1)).length() + #return Point(vt.map(QtCore.QPointF(0, 1))-vt.map(QtCore.QPointF(0, 0))).length() def mapToDevice(self, obj): @@ -357,10 +401,11 @@ class GraphicsItem(object): tr = self.itemTransform(relativeItem) if isinstance(tr, tuple): ## difference between pyside and pyqt - tr = tr[0] - vec = tr.map(Point(1,0)) - tr.map(Point(0,0)) - return Point(vec).angle(Point(1,0)) - + tr = tr[0] + #vec = tr.map(Point(1,0)) - tr.map(Point(0,0)) + vec = tr.map(QtCore.QLineF(0,0,1,0)) + #return Point(vec).angle(Point(1,0)) + return vec.angleTo(QtCore.QLineF(vec.p1(), vec.p1()+QtCore.QPointF(1,0))) #def itemChange(self, change, value): #ret = self._qtBaseClass.itemChange(self, change, value) @@ -500,3 +545,6 @@ class GraphicsItem(object): else: self._exportOpts = False + #def update(self): + #self._qtBaseClass.update(self) + #print "Update:", self \ No newline at end of file diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 8af13e19..d035a0e4 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -52,7 +52,7 @@ class PlotCurveItem(GraphicsObject): self.clear() self.path = None self.fillPath = None - + self._boundsCache = [None, None] ## this is disastrous for performance. #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) @@ -85,6 +85,12 @@ class PlotCurveItem(GraphicsObject): return self.xData, self.yData def dataBounds(self, ax, frac=1.0, orthoRange=None): + ## Need this to run as fast as possible. + ## check cache first: + cache = self._boundsCache[ax] + if cache is not None and cache[0] == (frac, orthoRange): + return cache[1] + (x, y) = self.getData() if x is None or len(x) == 0: return (0, 0) @@ -103,15 +109,22 @@ class PlotCurveItem(GraphicsObject): if frac >= 1.0: - return (d.min(), d.max()) + b = (d.min(), d.max()) elif frac <= 0.0: raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) else: - return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) + b = (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) + self._boundsCache[ax] = [(frac, orthoRange), b] + return b + + def invalidateBounds(self): + self._boundingRect = None + self._boundsCache = [None, None] def setPen(self, *args, **kargs): """Set the pen used to draw the curve.""" self.opts['pen'] = fn.mkPen(*args, **kargs) + self.invalidateBounds() self.update() def setShadowPen(self, *args, **kargs): @@ -120,17 +133,20 @@ class PlotCurveItem(GraphicsObject): pen to be visible. """ self.opts['shadowPen'] = fn.mkPen(*args, **kargs) + self.invalidateBounds() self.update() def setBrush(self, *args, **kargs): """Set the brush used when filling the area under the curve""" self.opts['brush'] = fn.mkBrush(*args, **kargs) + self.invalidateBounds() self.update() def setFillLevel(self, level): """Set the level filled to when filling under the curve""" self.opts['fillLevel'] = level self.fillPath = None + self.invalidateBounds() self.update() #def setColor(self, color): @@ -221,7 +237,9 @@ class PlotCurveItem(GraphicsObject): #self.setCacheMode(QtGui.QGraphicsItem.NoCache) ## Disabling and re-enabling the cache works around a bug in Qt 4.6 causing the cached results to display incorrectly ## Test this bug with test_PlotWidget and zoom in on the animated plot + self.invalidateBounds() self.prepareGeometryChange() + self.informViewBoundsChanged() self.yData = kargs['y'].view(np.ndarray) self.xData = kargs['x'].view(np.ndarray) @@ -349,36 +367,38 @@ class PlotCurveItem(GraphicsObject): return self.path def boundingRect(self): - (x, y) = self.getData() - if x is None or y is None or len(x) == 0 or len(y) == 0: - return QtCore.QRectF() + if self._boundingRect is None: + (x, y) = self.getData() + if x is None or y is None or len(x) == 0 or len(y) == 0: + return QtCore.QRectF() + + + if self.opts['shadowPen'] is not None: + lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1) + else: + lineWidth = (self.opts['pen'].width()+1) + + pixels = self.pixelVectors() + if pixels == (None, None): + pixels = [Point(0,0), Point(0,0)] + + xmin = x.min() + xmax = x.max() + ymin = y.min() + ymax = y.max() - if self.opts['shadowPen'] is not None: - lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1) - else: - lineWidth = (self.opts['pen'].width()+1) + if self.opts['fillLevel'] is not None: + ymin = min(ymin, self.opts['fillLevel']) + ymax = max(ymax, self.opts['fillLevel']) + + xmin -= pixels[0].x() * lineWidth + xmax += pixels[0].x() * lineWidth + ymin -= abs(pixels[1].y()) * lineWidth + ymax += abs(pixels[1].y()) * lineWidth - - pixels = self.pixelVectors() - if pixels == (None, None): - pixels = [Point(0,0), Point(0,0)] - - xmin = x.min() - xmax = x.max() - ymin = y.min() - ymax = y.max() - - if self.opts['fillLevel'] is not None: - ymin = min(ymin, self.opts['fillLevel']) - ymax = max(ymax, self.opts['fillLevel']) - - xmin -= pixels[0].x() * lineWidth - xmax += pixels[0].x() * lineWidth - ymin -= abs(pixels[1].y()) * lineWidth - ymax += abs(pixels[1].y()) * lineWidth - - return QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin) + self._boundingRect = QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin) + return self._boundingRect def paint(self, p, opt, widget): prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 22aa3ad9..e46279d7 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -369,9 +369,10 @@ class PlotDataItem(GraphicsObject): self.updateItems() prof.mark('update items') - view = self.getViewBox() - if view is not None: - view.itemBoundsChanged(self) ## inform view so it can update its range if it wants + self.informViewBoundsChanged() + #view = self.getViewBox() + #if view is not None: + #view.itemBoundsChanged(self) ## inform view so it can update its range if it wants self.sigPlotChanged.emit(self) prof.mark('emit') diff --git a/pyqtgraph/graphicsItems/VTickGroup.py b/pyqtgraph/graphicsItems/VTickGroup.py index 85ed596e..c6880f91 100644 --- a/pyqtgraph/graphicsItems/VTickGroup.py +++ b/pyqtgraph/graphicsItems/VTickGroup.py @@ -34,8 +34,7 @@ class VTickGroup(UIGraphicsItem): if xvals is None: xvals = [] - #bounds = QtCore.QRectF(0, yrange[0], 1, yrange[1]-yrange[0]) - UIGraphicsItem.__init__(self)#, bounds=bounds) + UIGraphicsItem.__init__(self) if pen is None: pen = (200, 200, 200) @@ -44,15 +43,10 @@ class VTickGroup(UIGraphicsItem): self.ticks = [] self.xvals = [] - #if view is None: - #self.view = None - #else: - #self.view = weakref.ref(view) self.yrange = [0,1] self.setPen(pen) self.setYRange(yrange) self.setXVals(xvals) - #self.valid = False def setPen(self, *args, **kwargs): """Set the pen to use for drawing ticks. Can be specified as any arguments valid @@ -75,80 +69,20 @@ class VTickGroup(UIGraphicsItem): """Set the y range [low, high] that the ticks are drawn on. 0 is the bottom of the view, 1 is the top.""" self.yrange = vals - #self.relative = relative - #if self.view is not None: - #if relative: - #self.view().sigRangeChanged.connect(self.rescale) - #else: - #try: - #self.view().sigRangeChanged.disconnect(self.rescale) - #except: - #pass self.rebuildTicks() - #self.valid = False def dataBounds(self, *args, **kargs): return None ## item should never affect view autoscaling - #def viewRangeChanged(self): - ### called when the view is scaled - - #UIGraphicsItem.viewRangeChanged(self) - - #self.resetTransform() - ##vb = self.view().viewRect() - ##p1 = vb.bottom() - vb.height() * self.yrange[0] - ##p2 = vb.bottom() - vb.height() * self.yrange[1] - - ##br = self.boundingRect() - ##yr = [p1, p2] - - - - ##self.rebuildTicks() - - ##br = self.boundingRect() - ##print br - ##self.translate(0.0, br.y()) - ##self.scale(1.0, br.height()) - ##self.boundingRect() - #self.update() - - #def boundingRect(self): - #print "--request bounds:" - #b = self.path.boundingRect() - #b2 = UIGraphicsItem.boundingRect(self) - #b2.setY(b.y()) - #b2.setWidth(b.width()) - #print " ", b - #print " ", b2 - #print " ", self.mapRectToScene(b) - #return b2 - def yRange(self): - #if self.relative: - #height = self.view.size().height() - #p1 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[0])))) - #p2 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[1])))) - #return [p1.y(), p2.y()] - #else: - #return self.yrange - return self.yrange def rebuildTicks(self): self.path = QtGui.QPainterPath() yrange = self.yRange() - #print "rebuild ticks:", yrange for x in self.xvals: - #path.moveTo(x, yrange[0]) - #path.lineTo(x, yrange[1]) self.path.moveTo(x, 0.) self.path.lineTo(x, 1.) - #self.setPath(self.path) - #self.valid = True - #self.rescale() - #print " done..", self.boundingRect() def paint(self, p, *args): UIGraphicsItem.paint(self, p, *args) @@ -161,7 +95,6 @@ class VTickGroup(UIGraphicsItem): p.scale(1.0, br.height()) p.setPen(self.pen) p.drawPath(self.path) - #QtGui.QGraphicsPathItem.paint(self, *args) if __name__ == '__main__': diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 8da539fb..ae7298ba 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -9,6 +9,7 @@ from pyqtgraph.GraphicsScene import GraphicsScene import pyqtgraph import weakref from copy import deepcopy +import pyqtgraph.debug as debug __all__ = ['ViewBox'] @@ -110,6 +111,7 @@ class ViewBox(GraphicsWidget): 'background': None, } self._updatingRange = False ## Used to break recursive loops. See updateAutoRange. + self._itemBoundsCache = weakref.WeakKeyDictionary() self.locateGroup = None ## items displayed when using ViewBox.locate(item) @@ -548,7 +550,7 @@ class ViewBox(GraphicsWidget): fractionVisible[i] = 1.0 childRange = None - + order = [0,1] if self.state['autoVisibleOnly'][0] is True: order = [1,0] @@ -571,40 +573,18 @@ class ViewBox(GraphicsWidget): if xr is not None: if self.state['autoPan'][ax]: x = sum(xr) * 0.5 - #x = childRect.center().x() w2 = (targetRect[ax][1]-targetRect[ax][0]) / 2. - #childRect.setLeft(x-w2) - #childRect.setRight(x+w2) childRange[ax] = [x-w2, x+w2] else: - #wp = childRect.width() * 0.02 wp = (xr[1] - xr[0]) * 0.02 - #childRect = childRect.adjusted(-wp, 0, wp, 0) childRange[ax][0] -= wp childRange[ax][1] += wp - #targetRect[ax][0] = childRect.left() - #targetRect[ax][1] = childRect.right() targetRect[ax] = childRange[ax] args['xRange' if ax == 0 else 'yRange'] = targetRect[ax] - #else: - ### Make corrections to Y range - #if self.state['autoPan'][1]: - #y = childRect.center().y() - #h2 = (targetRect[1][1]-targetRect[1][0]) / 2. - #childRect.setTop(y-h2) - #childRect.setBottom(y+h2) - #else: - #hp = childRect.height() * 0.02 - #childRect = childRect.adjusted(0, -hp, 0, hp) - - #targetRect[1][0] = childRect.top() - #targetRect[1][1] = childRect.bottom() - #args['yRange'] = targetRect[1] if len(args) == 0: return args['padding'] = 0 args['disableAutoRange'] = False - #self.setRange(xRange=targetRect[0], yRange=targetRect[1], padding=0, disableAutoRange=False) self.setRange(**args) finally: self._updatingRange = False @@ -744,6 +724,7 @@ class ViewBox(GraphicsWidget): self.updateAutoRange() def itemBoundsChanged(self, item): + self._itemBoundsCache.pop(item, None) self.updateAutoRange() def invertY(self, b=True): @@ -1015,6 +996,8 @@ class ViewBox(GraphicsWidget): [[xmin, xmax], [ymin, ymax]] Values may be None if there are no specific bounds for an axis. """ + prof = debug.Profiler('updateAutoRange', disabled=True) + #items = self.allChildren() items = self.addedItems @@ -1029,38 +1012,36 @@ class ViewBox(GraphicsWidget): if not item.isVisible(): continue - #print "=========", item useX = True useY = True if hasattr(item, 'dataBounds'): - if frac is None: - frac = (1.0, 1.0) - xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0]) - yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1]) - #print " xr:", xr, " yr:", yr - if xr is None or xr == (None, None): - useX = False - xr = (0,0) - if yr is None or yr == (None, None): - useY = False - yr = (0,0) + bounds = self._itemBoundsCache.get(item, None) + if bounds is None: + if frac is None: + frac = (1.0, 1.0) + xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0]) + yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1]) + if xr is None or xr == (None, None): + useX = False + xr = (0,0) + if yr is None or yr == (None, None): + useY = False + yr = (0,0) - bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0]) - #print " xr:", xr, " yr:", yr - #print " item real:", bounds + bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0]) + bounds = self.mapFromItemToView(item, bounds).boundingRect() + self._itemBoundsCache[item] = (bounds, useX, useY) + else: + bounds, useX, useY = bounds else: if int(item.flags() & item.ItemHasNoContents) > 0: continue - #print " empty" else: bounds = item.boundingRect() - #bounds = [[item.left(), item.top()], [item.right(), item.bottom()]] - #print " item:", bounds - #bounds = QtCore.QRectF(bounds[0][0], bounds[1][0], bounds[0][1]-bounds[0][0], bounds[1][1]-bounds[1][0]) - bounds = self.mapFromItemToView(item, bounds).boundingRect() - #print " ", bounds + bounds = self.mapFromItemToView(item, bounds).boundingRect() + + prof.mark('1') - #print " useX:", useX, " useY:", useY if not any([useX, useY]): continue @@ -1073,11 +1054,6 @@ class ViewBox(GraphicsWidget): else: continue ## need to check for item rotations and decide how best to apply this boundary. - #print " useX:", useX, " useY:", useY - - #print " range:", range - #print " bounds (r,l,t,b):", bounds.right(), bounds.left(), bounds.top(), bounds.bottom() - if useY: if range[1] is not None: range[1] = [min(bounds.top(), range[1][0]), max(bounds.bottom(), range[1][1])] @@ -1088,9 +1064,9 @@ class ViewBox(GraphicsWidget): range[0] = [min(bounds.left(), range[0][0]), max(bounds.right(), range[0][1])] else: range[0] = [bounds.left(), bounds.right()] + prof.mark('2') - #print " range:", range - + prof.finish() return range def childrenBoundingRect(self, *args, **kwds): @@ -1287,5 +1263,4 @@ class ViewBox(GraphicsWidget): self.scene().removeItem(self.locateGroup) self.locateGroup = None - from .ViewBoxMenu import ViewBoxMenu diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index 403aed9d..dd49ab7d 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -143,7 +143,11 @@ class GraphicsView(QtGui.QGraphicsView): else: brush = fn.mkBrush(background) self.setBackgroundBrush(brush) - + + def paintEvent(self, ev): + self.scene().prepareForPaint() + #print "GV: paint", ev.rect() + return QtGui.QGraphicsView.paintEvent(self, ev) def close(self): self.centralWidget = None From 513e904a591004482d94d9675ab49182eefe11bc Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 10 Jan 2013 16:10:27 -0500 Subject: [PATCH 2/3] Improved performance for remote plotting: - reduced cost of transferring arrays between processes (pickle is too slow) - avoid unnecessary synchronous calls Added RemoteSpeedTest example --- examples/MultiPlotSpeedTest.py | 63 ++++++++++++++ examples/RemoteSpeedTest.py | 78 +++++++++++++++++ examples/__main__.py | 1 + examples/multiprocess.py | 3 + pyqtgraph/multiprocess/parallelizer.py | 4 +- pyqtgraph/multiprocess/processes.py | 14 ++-- pyqtgraph/multiprocess/remoteproxy.py | 107 +++++++++++++++++++----- pyqtgraph/widgets/RemoteGraphicsView.py | 37 +++++++- 8 files changed, 277 insertions(+), 30 deletions(-) create mode 100644 examples/MultiPlotSpeedTest.py create mode 100644 examples/RemoteSpeedTest.py diff --git a/examples/MultiPlotSpeedTest.py b/examples/MultiPlotSpeedTest.py new file mode 100644 index 00000000..e25de42e --- /dev/null +++ b/examples/MultiPlotSpeedTest.py @@ -0,0 +1,63 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import initExample + + +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np +import pyqtgraph as pg +from pyqtgraph.ptime import time +#QtGui.QApplication.setGraphicsSystem('raster') +app = QtGui.QApplication([]) +#mw = QtGui.QMainWindow() +#mw.resize(800,800) + +p = pg.plot() +#p.setRange(QtCore.QRectF(0, -10, 5000, 20)) +p.setLabel('bottom', 'Index', units='B') + +nPlots = 10 +#curves = [p.plot(pen=(i,nPlots*1.3)) for i in range(nPlots)] +curves = [pg.PlotCurveItem(pen=(i,nPlots*1.3)) for i in range(nPlots)] +for c in curves: + p.addItem(c) + +rgn = pg.LinearRegionItem([1,100]) +p.addItem(rgn) + + +data = np.random.normal(size=(53,5000/nPlots)) +ptr = 0 +lastTime = time() +fps = None +count = 0 +def update(): + global curve, data, ptr, p, lastTime, fps, nPlots, count + count += 1 + #print "---------", count + for i in range(nPlots): + curves[i].setData(i+data[(ptr+i)%data.shape[0]]) + #print " setData done." + ptr += nPlots + now = time() + dt = now - lastTime + lastTime = now + if fps is None: + fps = 1.0/dt + else: + s = np.clip(dt*3., 0, 1) + fps = fps * (1-s) + (1.0/dt) * s + p.setTitle('%0.2f fps' % fps) + #app.processEvents() ## force complete redraw for every plot +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(0) + + + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/RemoteSpeedTest.py b/examples/RemoteSpeedTest.py new file mode 100644 index 00000000..b3415a9d --- /dev/null +++ b/examples/RemoteSpeedTest.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +""" +This example demonstrates the use of RemoteGraphicsView to improve performance in +applications with heavy load. It works by starting a second process to handle +all graphics rendering, thus freeing up the main process to do its work. + +In this example, the update() function is very expensive and is called frequently. +After update() generates a new set of data, it can either plot directly to a local +plot (bottom) or remotely via a RemoteGraphicsView (top), allowing speed comparison +between the two cases. IF you have a multi-core CPU, it should be obvious that the +remote case is much faster. +""" + +import initExample ## Add path to library (just for examples; you do not need this) +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg +import pyqtgraph.widgets.RemoteGraphicsView +import numpy as np + +app = pg.mkQApp() + +view = pg.widgets.RemoteGraphicsView.RemoteGraphicsView() +pg.setConfigOptions(antialias=True) ## this will be expensive for the local plot +view.pg.setConfigOptions(antialias=True) ## prettier plots at no cost to the main process! + +label = QtGui.QLabel() +rcheck = QtGui.QCheckBox('plot remote') +rcheck.setChecked(True) +lcheck = QtGui.QCheckBox('plot local') +lplt = pg.PlotWidget() +layout = pg.LayoutWidget() +layout.addWidget(rcheck) +layout.addWidget(lcheck) +layout.addWidget(label) +layout.addWidget(view, row=1, col=0, colspan=3) +layout.addWidget(lplt, row=2, col=0, colspan=3) +layout.resize(800,800) +layout.show() + +## Create a PlotItem in the remote process that will be displayed locally +rplt = view.pg.PlotItem() +rplt._setProxyOptions(deferGetattr=True) ## speeds up access to rplt.plot +view.setCentralItem(rplt) + +lastUpdate = pg.ptime.time() +avgFps = 0.0 + +def update(): + global check, label, plt, lastUpdate, avgFps, rpltfunc + data = np.random.normal(size=(10000,50)).sum(axis=1) + data += 5 * np.sin(np.linspace(0, 10, data.shape[0])) + + if rcheck.isChecked(): + rplt.plot(data, clear=True, _callSync='off') ## We do not expect a return value. + ## By turning off callSync, we tell + ## the proxy that it does not need to + ## wait for a reply from the remote + ## process. + if lcheck.isChecked(): + lplt.plot(data, clear=True) + + now = pg.ptime.time() + fps = 1.0 / (now - lastUpdate) + lastUpdate = now + avgFps = avgFps * 0.8 + fps * 0.2 + label.setText("Generating %0.2f fps" % avgFps) + +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(0) + + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/__main__.py b/examples/__main__.py index e234a9da..40fbb5e9 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -22,6 +22,7 @@ examples = OrderedDict([ ('Dock widgets', 'dockarea.py'), ('Console', 'ConsoleWidget.py'), ('Histograms', 'histogram.py'), + ('Remote Plotting', 'RemoteSpeedTest.py'), ('GraphicsItems', OrderedDict([ ('Scatter Plot', 'ScatterPlot.py'), #('PlotItem', 'PlotItem.py'), diff --git a/examples/multiprocess.py b/examples/multiprocess.py index 0b2d7ed8..f6756345 100644 --- a/examples/multiprocess.py +++ b/examples/multiprocess.py @@ -43,6 +43,9 @@ from pyqtgraph.Qt import QtCore, QtGui app = pg.QtGui.QApplication([]) print "\n=================\nStart QtProcess" +import sys +if (sys.flags.interactive != 1): + print " (not interactive; remote process will exit immediately.)" proc = mp.QtProcess() d1 = proc.transfer(np.random.normal(size=1000)) d2 = proc.transfer(np.random.normal(size=1000)) diff --git a/pyqtgraph/multiprocess/parallelizer.py b/pyqtgraph/multiprocess/parallelizer.py index 0d5d6f5c..2d03c000 100644 --- a/pyqtgraph/multiprocess/parallelizer.py +++ b/pyqtgraph/multiprocess/parallelizer.py @@ -1,6 +1,6 @@ import os, sys, time, multiprocessing, re from processes import ForkedProcess -from remoteproxy import ExitError +from remoteproxy import ClosedError class CanceledError(Exception): """Raised when the progress dialog is canceled during a processing operation.""" @@ -152,7 +152,7 @@ class Parallelize(object): n = ch.processRequests() if n > 0: waitingChildren += 1 - except ExitError: + except ClosedError: #print ch.childPid, 'process finished' rem.append(ch) if self.showProgress: diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index a4769679..f95a3ec4 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -1,9 +1,9 @@ -from remoteproxy import RemoteEventHandler, ExitError, NoResultError, LocalObjectProxy, ObjectProxy +from remoteproxy import RemoteEventHandler, ClosedError, NoResultError, LocalObjectProxy, ObjectProxy import subprocess, atexit, os, sys, time, random, socket, signal import cPickle as pickle import multiprocessing.connection -__all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ExitError', 'NoResultError'] +__all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ClosedError', 'NoResultError'] class Process(RemoteEventHandler): """ @@ -100,7 +100,7 @@ def startEventLoop(name, port, authkey): try: HANDLER.processRequests() # exception raised when the loop should exit time.sleep(0.01) - except ExitError: + except ClosedError: break @@ -225,7 +225,7 @@ class ForkedProcess(RemoteEventHandler): try: self.processRequests() # exception raised when the loop should exit time.sleep(0.01) - except ExitError: + except ClosedError: break except: print "Error occurred in forked event loop:" @@ -267,11 +267,11 @@ class RemoteQtEventHandler(RemoteEventHandler): def processRequests(self): try: RemoteEventHandler.processRequests(self) - except ExitError: + except ClosedError: from pyqtgraph.Qt import QtGui, QtCore QtGui.QApplication.instance().quit() self.timer.stop() - #raise + #raise SystemExit class QtProcess(Process): """ @@ -315,7 +315,7 @@ class QtProcess(Process): def processRequests(self): try: Process.processRequests(self) - except ExitError: + except ClosedError: self.timer.stop() def startQtEventLoop(name, port, authkey): diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index f8da1bd7..ee5a0d6c 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -1,10 +1,15 @@ import os, __builtin__, time, sys, traceback, weakref import cPickle as pickle +import numpy as np -class ExitError(Exception): +class ClosedError(Exception): + """Raised when an event handler receives a request to close the connection + or discovers that the connection has been closed.""" pass class NoResultError(Exception): + """Raised when a request for the return value of a remote call fails + because the call has not yet returned.""" pass @@ -82,14 +87,14 @@ class RemoteEventHandler(object): Returns the number of events processed. """ if self.exited: - raise ExitError() + raise ClosedError() numProcessed = 0 while self.conn.poll(): try: self.handleRequest() numProcessed += 1 - except ExitError: + except ClosedError: self.exited = True raise except IOError as err: @@ -108,14 +113,20 @@ class RemoteEventHandler(object): Blocks until a request is available.""" result = None try: - cmd, reqId, optStr = self.conn.recv() ## args, kwds are double-pickled to ensure this recv() call never fails - except EOFError: + cmd, reqId, nByteMsgs, optStr = self.conn.recv() ## args, kwds are double-pickled to ensure this recv() call never fails + except EOFError, IOError: ## remote process has shut down; end event loop - raise ExitError() - except IOError: - raise ExitError() + raise ClosedError() #print os.getpid(), "received request:", cmd, reqId + ## read byte messages following the main request + byteData = [] + for i in range(nByteMsgs): + try: + byteData.append(self.conn.recv_bytes()) + except EOFError, IOError: + raise ClosedError() + try: if cmd == 'result' or cmd == 'error': @@ -137,17 +148,36 @@ class RemoteEventHandler(object): obj = opts['obj'] fnargs = opts['args'] fnkwds = opts['kwds'] + + ## If arrays were sent as byte messages, they must be re-inserted into the + ## arguments + if len(byteData) > 0: + for i,arg in enumerate(fnargs): + if isinstance(arg, tuple) and len(arg) > 0 and arg[0] == '__byte_message__': + ind = arg[1] + dtype, shape = arg[2] + fnargs[i] = np.fromstring(byteData[ind], dtype=dtype).reshape(shape) + for k,arg in fnkwds.items(): + if isinstance(arg, tuple) and len(arg) > 0 and arg[0] == '__byte_message__': + ind = arg[1] + dtype, shape = arg[2] + fnkwds[k] = np.fromstring(byteData[ind], dtype=dtype).reshape(shape) + if len(fnkwds) == 0: ## need to do this because some functions do not allow keyword arguments. - #print obj, fnargs result = obj(*fnargs) else: result = obj(*fnargs, **fnkwds) + elif cmd == 'getObjValue': result = opts['obj'] ## has already been unpickled into its local value returnType = 'value' elif cmd == 'transfer': result = opts['obj'] returnType = 'proxy' + elif cmd == 'transferArray': + ## read array data from next message: + result = np.fromstring(byteData[0], dtype=opts['dtype']).reshape(opts['shape']) + returnType = 'proxy' elif cmd == 'import': name = opts['module'] fromlist = opts.get('fromlist', []) @@ -201,7 +231,7 @@ class RemoteEventHandler(object): ## (more importantly, do not call any code that would ## normally be invoked at exit) else: - raise ExitError() + raise ClosedError() @@ -216,7 +246,7 @@ class RemoteEventHandler(object): except: self.send(request='error', reqId=reqId, callSync='off', opts=dict(exception=None, excString=excStr)) - def send(self, request, opts=None, reqId=None, callSync='sync', timeout=10, returnType=None, **kwds): + def send(self, request, opts=None, reqId=None, callSync='sync', timeout=10, returnType=None, byteData=None, **kwds): """Send a request or return packet to the remote process. Generally it is not necessary to call this method directly; it is for internal use. (The docstring has information that is nevertheless useful to the programmer @@ -235,6 +265,9 @@ class RemoteEventHandler(object): opts Extra arguments sent to the remote process that determine the way the request will be handled (see below) returnType 'proxy', 'value', or 'auto' + byteData If specified, this is a list of objects to be sent as byte messages + to the remote process. + This is used to send large arrays without the cost of pickling. ========== ==================================================================== Description of request strings and options allowed for each: @@ -312,7 +345,9 @@ class RemoteEventHandler(object): if returnType is not None: opts['returnType'] = returnType - #print "send", opts + + #print os.getpid(), "send request:", request, reqId, opts + ## double-pickle args to ensure that at least status and request ID get through try: optStr = pickle.dumps(opts) @@ -322,9 +357,19 @@ class RemoteEventHandler(object): print "=======================================" raise - request = (request, reqId, optStr) + nByteMsgs = 0 + if byteData is not None: + nByteMsgs = len(byteData) + + ## Send primary request + request = (request, reqId, nByteMsgs, optStr) self.conn.send(request) + ## follow up by sending byte messages + if byteData is not None: + for obj in byteData: ## Remote process _must_ be prepared to read the same number of byte messages! + self.conn.send_bytes(obj) + if callSync == 'off': return @@ -345,10 +390,10 @@ class RemoteEventHandler(object): ## raises NoResultError if the result is not available yet #print self.results.keys(), os.getpid() if reqId not in self.results: - #self.readPipe() try: self.processRequests() - except ExitError: + except ClosedError: ## even if remote connection has closed, we may have + ## received new data during this call to processRequests() pass if reqId not in self.results: raise NoResultError() @@ -393,17 +438,33 @@ class RemoteEventHandler(object): def callObj(self, obj, args, kwds, **opts): opts = opts.copy() + args = list(args) + + ## Decide whether to send arguments by value or by proxy noProxyTypes = opts.pop('noProxyTypes', None) if noProxyTypes is None: noProxyTypes = self.proxyOptions['noProxyTypes'] - autoProxy = opts.pop('autoProxy', self.proxyOptions['autoProxy']) + autoProxy = opts.pop('autoProxy', self.proxyOptions['autoProxy']) if autoProxy is True: - args = tuple([self.autoProxy(v, noProxyTypes) for v in args]) + args = [self.autoProxy(v, noProxyTypes) for v in args] for k, v in kwds.iteritems(): opts[k] = self.autoProxy(v, noProxyTypes) - return self.send(request='callObj', opts=dict(obj=obj, args=args, kwds=kwds), **opts) + byteMsgs = [] + + ## If there are arrays in the arguments, send those as byte messages. + ## We do this because pickling arrays is too expensive. + for i,arg in enumerate(args): + if arg.__class__ == np.ndarray: + args[i] = ("__byte_message__", len(byteMsgs), (arg.dtype, arg.shape)) + byteMsgs.append(arg) + for k,v in kwds.items(): + if v.__class__ == np.ndarray: + kwds[k] = ("__byte_message__", len(byteMsgs), (v.dtype, v.shape)) + byteMsgs.append(v) + + return self.send(request='callObj', opts=dict(obj=obj, args=args, kwds=kwds), byteData=byteMsgs, **opts) def registerProxy(self, proxy): ref = weakref.ref(proxy, self.deleteProxy) @@ -421,7 +482,11 @@ class RemoteEventHandler(object): Transfer an object by value to the remote host (the object must be picklable) and return a proxy for the new remote object. """ - return self.send(request='transfer', opts=dict(obj=obj), **kwds) + if obj.__class__ is np.ndarray: + opts = {'dtype': obj.dtype, 'shape': obj.shape} + return self.send(request='transferArray', opts=opts, byteData=[obj], **kwds) + else: + return self.send(request='transfer', opts=dict(obj=obj), **kwds) def autoProxy(self, obj, noProxyTypes): ## Return object wrapped in LocalObjectProxy _unless_ its type is in noProxyTypes. @@ -453,6 +518,8 @@ class Request(object): If block is True, wait until the result has arrived or *timeout* seconds passes. If the timeout is reached, raise NoResultError. (use timeout=None to disable) If block is False, raise NoResultError immediately if the result has not arrived yet. + + If the process's connection has closed before the result arrives, raise ClosedError. """ if self.gotResult: @@ -464,6 +531,8 @@ class Request(object): if block: start = time.time() while not self.hasResult(): + if self.proc.exited: + raise ClosedError() time.sleep(0.005) if timeout >= 0 and time.time() - start > timeout: print "Request timed out:", self.description diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index 3752a6bb..d8e720b5 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -18,6 +18,8 @@ class RemoteGraphicsView(QtGui.QWidget): def __init__(self, parent=None, *args, **kwds): self._img = None self._imgReq = None + self._sizeHint = (640,480) ## no clue why this is needed, but it seems to be the default sizeHint for GraphicsView. + ## without it, the widget will not compete for space against another GraphicsView. QtGui.QWidget.__init__(self) self._proc = mp.QtProcess() self.pg = self._proc._import('pyqtgraph') @@ -26,12 +28,18 @@ class RemoteGraphicsView(QtGui.QWidget): self._view = rpgRemote.Renderer(*args, **kwds) self._view._setProxyOptions(deferGetattr=True) self.setFocusPolicy(self._view.focusPolicy()) + self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + self.setMouseTracking(True) shmFileName = self._view.shmFileName() self.shmFile = open(shmFileName, 'r') self.shm = mmap.mmap(self.shmFile.fileno(), mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_READ) - self._view.sceneRendered.connect(mp.proxy(self.remoteSceneChanged)) + self._view.sceneRendered.connect(mp.proxy(self.remoteSceneChanged)) #, callSync='off')) + ## Note: we need synchronous signals + ## even though there is no return value-- + ## this informs the renderer that it is + ## safe to begin rendering again. for method in ['scene', 'setCentralItem']: setattr(self, method, getattr(self._view, method)) @@ -41,8 +49,12 @@ class RemoteGraphicsView(QtGui.QWidget): self._view.resize(self.size(), _callSync='off') return ret + def sizeHint(self): + return QtCore.QSize(*self._sizeHint) + def remoteSceneChanged(self, data): w, h, size = data + #self._sizeHint = (whint, hhint) if self.shm.size != size: self.shm.close() self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ) @@ -82,7 +94,17 @@ class RemoteGraphicsView(QtGui.QWidget): ev.accept() return QtGui.QWidget.keyEvent(self, ev) - + def enterEvent(self, ev): + self._view.enterEvent(ev.type(), _callSync='off') + return QtGui.QWidget.enterEvent(self, ev) + + def leaveEvent(self, ev): + self._view.leaveEvent(ev.type(), _callSync='off') + return QtGui.QWidget.leaveEvent(self, ev) + + def remoteProcess(self): + """Return the remote process handle. (see multiprocess.remoteproxy.RemoteEventHandler)""" + return self._proc class Renderer(GraphicsView): @@ -126,6 +148,8 @@ class Renderer(GraphicsView): def renderView(self): if self.img is None: ## make sure shm is large enough and get its address + if self.width() == 0 or self.height() == 0: + return size = self.width() * self.height() * 4 if size > self.shm.size(): self.shm.resize(size) @@ -168,5 +192,14 @@ class Renderer(GraphicsView): GraphicsView.keyEvent(self, QtGui.QKeyEvent(typ, mods, text, autorep, count)) return ev.accepted() + def enterEvent(self, typ): + ev = QtCore.QEvent(QtCore.QEvent.Type(typ)) + return GraphicsView.enterEvent(self, ev) + + def leaveEvent(self, typ): + ev = QtCore.QEvent(QtCore.QEvent.Type(typ)) + return GraphicsView.leaveEvent(self, ev) + + \ No newline at end of file From 6903886b3a64bfbf90d160d60fca478c6c420e68 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 11 Jan 2013 20:21:11 -0500 Subject: [PATCH 3/3] Fixes against previous commits: - fixed example testing script - Added finer axis ticks back in some instances - fixed improper bounds caching in log/fft mode - fixed exception handling in remoteproxy --- examples/Plotting.py | 3 ++ examples/ScatterPlot.py | 3 +- examples/__main__.py | 1 + examples/initExample.py | 1 - pyqtgraph/graphicsItems/AxisItem.py | 17 +++++--- pyqtgraph/graphicsItems/PlotCurveItem.py | 52 ------------------------ pyqtgraph/graphicsItems/PlotDataItem.py | 2 + pyqtgraph/multiprocess/remoteproxy.py | 4 +- 8 files changed, 21 insertions(+), 62 deletions(-) diff --git a/examples/Plotting.py b/examples/Plotting.py index a41fcd1e..7842ad3d 100644 --- a/examples/Plotting.py +++ b/examples/Plotting.py @@ -45,6 +45,9 @@ p5 = win.addPlot(title="Scatter plot, axis labels, log scale") x = np.random.normal(size=1000) * 1e-5 y = x*1000 + 0.005 * np.random.normal(size=1000) y -= y.min()-1.0 +mask = x > 1e-15 +x = x[mask] +y = y[mask] p5.plot(x, y, pen=None, symbol='t', symbolPen=None, symbolSize=10, symbolBrush=(100, 100, 255, 50)) p5.setLabel('left', "Y Axis", units='A') p5.setLabel('bottom', "Y Axis", units='s') diff --git a/examples/ScatterPlot.py b/examples/ScatterPlot.py index 9baf5cd3..03e849ad 100644 --- a/examples/ScatterPlot.py +++ b/examples/ScatterPlot.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -import sys, os ## Add path to library (just for examples; you do not need this) -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) +import initExample from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg diff --git a/examples/__main__.py b/examples/__main__.py index 40fbb5e9..87673208 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -182,6 +182,7 @@ def testFile(name, f, exe, lib, graphicsSystem=None): code = """ try: %s + import initExample import pyqtgraph as pg %s import %s diff --git a/examples/initExample.py b/examples/initExample.py index 6ee9db27..204a1ead 100644 --- a/examples/initExample.py +++ b/examples/initExample.py @@ -16,7 +16,6 @@ if not hasattr(sys, 'frozen'): sys.path.remove(p) sys.path.insert(0, p) - ## should force example to use PySide instead of PyQt if 'pyside' in sys.argv: from PySide import QtGui diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 7be3e30f..aba5fa8c 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -350,9 +350,7 @@ class AxisItem(GraphicsWidget): ## decide optimal minor tick spacing in pixels (this is just aesthetics) pixelSpacing = np.log(size+10) * 5 - optimalTickCount = size / pixelSpacing - if optimalTickCount < 1: - optimalTickCount = 1 + optimalTickCount = max(2., size / pixelSpacing) ## optimal minor tick spacing optimalSpacing = dif / optimalTickCount @@ -366,12 +364,21 @@ class AxisItem(GraphicsWidget): while intervals[minorIndex+1] <= optimalSpacing: minorIndex += 1 - return [ + levels = [ (intervals[minorIndex+2], 0), (intervals[minorIndex+1], 0), #(intervals[minorIndex], 0) ## Pretty, but eats up CPU ] + ## decide whether to include the last level of ticks + minSpacing = min(size / 20., 30.) + maxTickCount = size / minSpacing + if dif / intervals[minorIndex] <= maxTickCount: + levels.append((intervals[minorIndex], 0)) + return levels + + + ##### This does not work -- switching between 2/5 confuses the automatic text-level-selection ### Determine major/minor tick spacings which flank the optimal spacing. #intervals = np.array([1., 2., 5., 10., 20., 50., 100.]) * p10unit @@ -587,7 +594,7 @@ class AxisItem(GraphicsWidget): ticks = tickLevels[i][1] ## length of tick - tickLength = self.tickLength / ((i*1.0)+1.0) + tickLength = self.tickLength / ((i*0.5)+1.0) lineAlpha = 255 / (i+1) if self.grid is not False: diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index d035a0e4..c54671bb 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -149,39 +149,6 @@ class PlotCurveItem(GraphicsObject): self.invalidateBounds() self.update() - #def setColor(self, color): - #self.pen.setColor(color) - #self.update() - - #def setAlpha(self, alpha, auto): - #self.opts['alphaHint'] = alpha - #self.opts['alphaMode'] = auto - #self.update() - - #def setSpectrumMode(self, mode): - #self.opts['spectrumMode'] = mode - #self.xDisp = self.yDisp = None - #self.path = None - #self.update() - - #def setLogMode(self, mode): - #self.opts['logMode'] = mode - #self.xDisp = self.yDisp = None - #self.path = None - #self.update() - - #def setPointMode(self, mode): - #self.opts['pointMode'] = mode - #self.update() - - - #def setDownsampling(self, ds): - #if self.opts['downsample'] != ds: - #self.opts['downsample'] = ds - #self.xDisp = self.yDisp = None - #self.path = None - #self.update() - def setData(self, *args, **kargs): """ ============== ======================================================== @@ -483,25 +450,6 @@ class PlotCurveItem(GraphicsObject): self.path = None #del self.xData, self.yData, self.xDisp, self.yDisp, self.path - #def mousePressEvent(self, ev): - ##GraphicsObject.mousePressEvent(self, ev) - #if not self.clickable: - #ev.ignore() - #if ev.button() != QtCore.Qt.LeftButton: - #ev.ignore() - #self.mousePressPos = ev.pos() - #self.mouseMoved = False - - #def mouseMoveEvent(self, ev): - ##GraphicsObject.mouseMoveEvent(self, ev) - #self.mouseMoved = True - ##print "move" - - #def mouseReleaseEvent(self, ev): - ##GraphicsObject.mouseReleaseEvent(self, ev) - #if not self.mouseMoved: - #self.sigClicked.emit(self) - def mouseClickEvent(self, ev): if not self.clickable or ev.button() != QtCore.Qt.LeftButton: return diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index e46279d7..8e6162f2 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -164,6 +164,7 @@ class PlotDataItem(GraphicsObject): self.opts['fftMode'] = mode self.xDisp = self.yDisp = None self.updateItems() + self.informViewBoundsChanged() def setLogMode(self, xMode, yMode): if self.opts['logMode'] == [xMode, yMode]: @@ -171,6 +172,7 @@ class PlotDataItem(GraphicsObject): self.opts['logMode'] = [xMode, yMode] self.xDisp = self.yDisp = None self.updateItems() + self.informViewBoundsChanged() def setPointMode(self, mode): if self.opts['pointMode'] == mode: diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index ee5a0d6c..94cc6048 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -114,7 +114,7 @@ class RemoteEventHandler(object): result = None try: cmd, reqId, nByteMsgs, optStr = self.conn.recv() ## args, kwds are double-pickled to ensure this recv() call never fails - except EOFError, IOError: + except (EOFError, IOError): ## remote process has shut down; end event loop raise ClosedError() #print os.getpid(), "received request:", cmd, reqId @@ -124,7 +124,7 @@ class RemoteEventHandler(object): for i in range(nByteMsgs): try: byteData.append(self.conn.recv_bytes()) - except EOFError, IOError: + except (EOFError, IOError): raise ClosedError()