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
parent
6166e5900e
commit
cac797a7fc
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue