Add initial pixel buffer caching usage

Leverages `QGraphicsItem.cacheMode` to speed up interactivity via
less `.paint()` calls (on mouse interaction) and redraws of the
underlying path when there are no transformations (other then a shift).
In order to keep the "flat bar on new time period" UX, a couple special
methods have to be triggered to get a redraw of the pixel buffer when
appending new data.

Use `QPainterPath.controlPointRect()` over `.boundingRect()` since
supposedly it's a lot faster. Drop all use of `QPicture` (since it seems
to conflict with the pixel buffer stuff?) and it doesn't give any
measurable speedup when drawing the "last bar" lines.

Oh, and add some profiling for now.
chart_trader
Tyler Goodlet 2020-12-26 17:40:21 -05:00
parent 6166e5900e
commit cac797a7fc
1 changed files with 83 additions and 39 deletions

View File

@ -27,7 +27,7 @@ from numba import jit, float64, int64 # , optional
from PyQt5 import QtCore, QtGui from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import QLineF, QPointF from PyQt5.QtCore import QLineF, QPointF
from .._profile import timeit # from .._profile import timeit
# from ..data._source import numba_ohlc_dtype # from ..data._source import numba_ohlc_dtype
from ._style import ( from ._style import (
_xaxis_at, _xaxis_at,
@ -355,7 +355,7 @@ class CrossHair(pg.GraphicsObject):
# update all subscribed curve dots # update all subscribed curve dots
# first = plot._ohlc[0]['index'] # first = plot._ohlc[0]['index']
for cursor in opts.get('cursors', ()): for cursor in opts.get('cursors', ()):
cursor.setIndex(ix) # - first) cursor.setIndex(ix)
# update the label on the bottom of the crosshair # update the label on the bottom of the crosshair
self.xaxis_label.update_label( self.xaxis_label.update_label(
@ -495,10 +495,16 @@ def gen_qpath(
w, w,
) -> QtGui.QPainterPath: ) -> QtGui.QPainterPath:
profiler = pg.debug.Profiler(disabled=False)
x, y, c = path_arrays_from_ohlc(data, start, bar_gap=w) x, y, c = path_arrays_from_ohlc(data, start, bar_gap=w)
profiler("generate stream with numba")
# TODO: numba the internals of this! # TODO: numba the internals of this!
return pg.functions.arrayToQPath(x, y, connect=c) path = pg.functions.arrayToQPath(x, y, connect=c)
profiler("generate path with arrayToQPath")
return path
class BarItems(pg.GraphicsObject): class BarItems(pg.GraphicsObject):
@ -520,10 +526,21 @@ class BarItems(pg.GraphicsObject):
) -> None: ) -> None:
super().__init__() super().__init__()
self.last_bar = QtGui.QPicture() # NOTE: this prevents redraws on mouse interaction which is
# a huge boon for avg interaction latency.
# TODO: one question still remaining is if this makes trasform
# interactions slower (such as zooming) and if so maybe if/when
# we implement a "history" mode for the view we disable this in
# that mode?
self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
# not sure if this is actually impoving anything but figured it
# was worth a shot:
# self.path.reserve(int(100e3 * 6))
# self.last_bar = QtGui.QPicture()
self.path = QtGui.QPainterPath() self.path = QtGui.QPainterPath()
# self._h_path = QtGui.QGraphicsPathItem(self.path)
self._pi = plotitem self._pi = plotitem
@ -570,7 +587,7 @@ class BarItems(pg.GraphicsObject):
# create pics # create pics
# self.draw_history() # self.draw_history()
self.draw_last_bar() # self.draw_last_bar()
# trigger render # trigger render
# https://doc.qt.io/qt-5/qgraphicsitem.html#update # https://doc.qt.io/qt-5/qgraphicsitem.html#update
@ -587,17 +604,16 @@ class BarItems(pg.GraphicsObject):
# ) -> None: # ) -> None:
# ... # ...
# def draw_last_bar(self) -> None:
# """Currently this draws lines to a cached ``QPicture`` which
# is supposed to speed things up on ``.paint()`` calls (which
# is a call to ``QPainter.drawPicture()`` but I'm not so sure.
def draw_last_bar(self) -> None: # """
"""Currently this draws lines to a cached ``QPicture`` which # p = QtGui.QPainter(self.last_bar)
is supposed to speed things up on ``.paint()`` calls (which # # p.setPen(self.bars_pen)
is a call to ``QPainter.drawPicture()`` but I'm not so sure. # p.drawLines(*tuple(filter(bool, self._last_bar_lines)))
# p.end()
"""
p = QtGui.QPainter(self.last_bar)
p.setPen(self.bars_pen)
p.drawLines(*tuple(filter(bool, self._last_bar_lines)))
p.end()
# @timeit # @timeit
def update_from_array( def update_from_array(
@ -642,12 +658,19 @@ class BarItems(pg.GraphicsObject):
# update path # update path
old_path = self.path old_path = self.path
self.path = prepend_path self.path = prepend_path
# self.path.reserve(int(100e3 * 6))
self.path.addPath(old_path) self.path.addPath(old_path)
# trigger redraw despite caching
self.prepareGeometryChange()
if append_length: if append_length:
# generate new lines objects for updatable "current bar" # generate new lines objects for updatable "current bar"
self._last_bar_lines = lines_from_ohlc(array[-1], self.w) self._last_bar_lines = lines_from_ohlc(array[-1], self.w)
self.draw_last_bar()
# self.draw_last_bar()
# self.update()
# generate new graphics to match provided array # generate new graphics to match provided array
# path appending logic: # path appending logic:
@ -659,11 +682,14 @@ class BarItems(pg.GraphicsObject):
self.path.moveTo(float(istop - self.w), float(new_bars[0]['open'])) self.path.moveTo(float(istop - self.w), float(new_bars[0]['open']))
self.path.addPath(append_path) self.path.addPath(append_path)
# trigger redraw despite caching
self.prepareGeometryChange()
self._xrange = first_index, last_index self._xrange = first_index, last_index
if just_history: # if just_history:
self.update() # self.update()
return # return
# last bar update # last bar update
i, o, h, l, last, v = array[-1][ i, o, h, l, last, v = array[-1][
@ -696,20 +722,21 @@ class BarItems(pg.GraphicsObject):
# else: # else:
# # XXX: h == l -> remove any HL line to avoid render bug # # XXX: h == l -> remove any HL line to avoid render bug
# if body is not None: # if body is not None:
# body = self.lines[index - 1][0] = None # self._last_bar_lines = (None, larm, rarm)
# # body = self.lines[index - 1][0] = None
self.draw_last_bar() # self.draw_last_bar()
self.resetTransform()
self.setTransform(self.transform())
self.update() self.update()
# @timeit # @timeit
def paint(self, p, opt, widget): def paint(self, p, opt, widget):
# profiler = pg.debug.Profiler(disabled=False, delayed=False) profiler = pg.debug.Profiler(disabled=False) #, delayed=False)
# TODO: use to avoid drawing artefacts?
# self.prepareGeometryChange()
# p.setCompositionMode(0) # p.setCompositionMode(0)
p.setPen(self.bars_pen)
# TODO: one thing we could try here is pictures being drawn of # TODO: one thing we could try here is pictures being drawn of
# a fixed count of bars such that based on the viewbox indices we # a fixed count of bars such that based on the viewbox indices we
@ -717,10 +744,12 @@ class BarItems(pg.GraphicsObject):
# as is necesarry for what's in "view". Not sure if this will # as is necesarry for what's in "view". Not sure if this will
# lead to any perf gains other then when zoomed in to less bars # lead to any perf gains other then when zoomed in to less bars
# in view. # in view.
p.drawPicture(0, 0, self.last_bar) # p.drawPicture(0, 0, self.last_bar)
p.drawLines(*tuple(filter(bool, self._last_bar_lines)))
profiler('draw last bar')
p.setPen(self.bars_pen)
p.drawPath(self.path) p.drawPath(self.path)
profiler('draw history path')
# @timeit # @timeit
def boundingRect(self): def boundingRect(self):
@ -738,16 +767,31 @@ class BarItems(pg.GraphicsObject):
# bounding rect for us). # bounding rect for us).
# compute aggregate bounding rectangle # compute aggregate bounding rectangle
lb = self.last_bar.boundingRect() # lb = self.last_bar.boundingRect()
hb = self.path.boundingRect()
# hb = self.path.boundingRect()
# apparently this a lot faster says the docs?
# https://doc.qt.io/qt-5/qpainterpath.html#controlPointRect
hb = self.path.controlPointRect()
hb_size = hb.size()
# print(f'hb_size: {hb_size}')
w = hb_size.width() + 1
h = hb_size.height() + 1
br = QtCore.QRectF(
return QtCore.QRectF(
# top left # top left
QtCore.QPointF(hb.topLeft()), QtCore.QPointF(hb.topLeft()),
# total size # total size
QtCore.QSizeF(QtCore.QSizeF(lb.size()) + hb.size()) # QtCore.QSizeF(QtCore.QSizeF(lb.size()) + hb.size())
QtCore.QSizeF(w, h)
# QtCore.QSizeF(lb.size() + hb.size()) # QtCore.QSizeF(lb.size() + hb.size())
) )
# print(f'bounding rect: {br}')
return br
# XXX: when we get back to enabling tina mode for xb # XXX: when we get back to enabling tina mode for xb