Make chart switching super fast again

This fixes a weird re-render bug/slowdown/artifact that was introduced
with the order mode sidepane work. Prior to the sidepane addition, chart
switching was immediate with zero noticeable widget rendering steps.

The slow down was caused by 2 things:
- not yielding back to the Qt loop asap after re-showing/focussing
  a linked split chart that was already in memory.
- pausing/resuming feeds only after a Qt loop render cycle has
  completed.

This now restores the near zero latency UX.
py3.10_support
Tyler Goodlet 2021-12-20 13:15:16 -05:00
parent f21c68a672
commit 56b65a1cde
1 changed files with 55 additions and 32 deletions

View File

@ -18,6 +18,7 @@
High level chart-widget apis. High level chart-widget apis.
''' '''
from __future__ import annotations
from typing import Optional from typing import Optional
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
@ -68,7 +69,7 @@ log = get_logger(__name__)
class GodWidget(QWidget): class GodWidget(QWidget):
''' '''
"Our lord and savior, the holy child of window-shua, there is no "Our lord and savior, the holy child of window-shua, there is no
widget above thee." - 6||6 widget above thee." - 6|6
The highest level composed widget which contains layouts for The highest level composed widget which contains layouts for
organizing charts as well as other sub-widgets used to control or organizing charts as well as other sub-widgets used to control or
@ -104,8 +105,8 @@ class GodWidget(QWidget):
# self.init_strategy_ui() # self.init_strategy_ui()
# self.vbox.addLayout(self.hbox) # self.vbox.addLayout(self.hbox)
self._chart_cache = {} self._chart_cache: dict[str, LinkedSplits] = {}
self.linkedsplits: 'LinkedSplits' = None self.linkedsplits: Optional[LinkedSplits] = None
# assigned in the startup func `_async_main()` # assigned in the startup func `_async_main()`
self._root_n: trio.Nursery = None self._root_n: trio.Nursery = None
@ -135,7 +136,7 @@ class GodWidget(QWidget):
def set_chart_symbol( def set_chart_symbol(
self, self,
symbol_key: str, # of form <fqsn>.<providername> symbol_key: str, # of form <fqsn>.<providername>
linkedsplits: 'LinkedSplits', # type: ignore linkedsplits: LinkedSplits, # type: ignore
) -> None: ) -> None:
# re-sort org cache symbol list in LIFO order # re-sort org cache symbol list in LIFO order
@ -146,20 +147,20 @@ class GodWidget(QWidget):
def get_chart_symbol( def get_chart_symbol(
self, self,
symbol_key: str, symbol_key: str,
) -> 'LinkedSplits': # type: ignore
) -> LinkedSplits: # type: ignore
return self._chart_cache.get(symbol_key) return self._chart_cache.get(symbol_key)
async def load_symbol( async def load_symbol(
self, self,
providername: str, providername: str,
symbol_key: str, symbol_key: str,
loglevel: str, loglevel: str,
reset: bool = False, reset: bool = False,
) -> trio.Event: ) -> trio.Event:
'''Load a new contract into the charting app. '''
Load a new contract into the charting app.
Expects a ``numpy`` structured array containing all the ohlcv fields. Expects a ``numpy`` structured array containing all the ohlcv fields.
@ -178,6 +179,7 @@ class GodWidget(QWidget):
# XXX: this is CRITICAL especially with pixel buffer caching # XXX: this is CRITICAL especially with pixel buffer caching
self.linkedsplits.hide() self.linkedsplits.hide()
self.linkedsplits.unfocus()
# XXX: pretty sure we don't need this # XXX: pretty sure we don't need this
# remove any existing plots? # remove any existing plots?
@ -202,6 +204,11 @@ class GodWidget(QWidget):
) )
self.set_chart_symbol(fqsn, linkedsplits) self.set_chart_symbol(fqsn, linkedsplits)
self.vbox.addWidget(linkedsplits)
linkedsplits.show()
linkedsplits.focus()
await trio.sleep(0)
else: else:
# symbol is already loaded and ems ready # symbol is already loaded and ems ready
@ -215,21 +222,17 @@ class GodWidget(QWidget):
# also switch it over to the new chart's interal-layout # also switch it over to the new chart's interal-layout
# self.linkedsplits.chart.qframe.hbox.removeWidget(self.pp_pane) # self.linkedsplits.chart.qframe.hbox.removeWidget(self.pp_pane)
chart = linkedsplits.chart chart = linkedsplits.chart
await chart.resume_all_feeds()
# chart is already in memory so just focus it # chart is already in memory so just focus it
if self.linkedsplits:
self.linkedsplits.unfocus()
self.vbox.addWidget(linkedsplits)
linkedsplits.show() linkedsplits.show()
linkedsplits.focus() linkedsplits.focus()
await trio.sleep(0)
# resume feeds *after* rendering chart view asap
await chart.resume_all_feeds()
self.linkedsplits = linkedsplits self.linkedsplits = linkedsplits
symbol = linkedsplits.symbol symbol = linkedsplits.symbol
if symbol is not None: if symbol is not None:
self.window.setWindowTitle( self.window.setWindowTitle(
f'{symbol.key}@{symbol.brokers} ' f'{symbol.key}@{symbol.brokers} '
@ -239,7 +242,8 @@ class GodWidget(QWidget):
return order_mode_started return order_mode_started
def focus(self) -> None: def focus(self) -> None:
'''Focus the top level widget which in turn focusses the chart '''
Focus the top level widget which in turn focusses the chart
ala "view mode". ala "view mode".
''' '''
@ -247,9 +251,19 @@ class GodWidget(QWidget):
self.clearFocus() self.clearFocus()
self.linkedsplits.chart.setFocus() self.linkedsplits.chart.setFocus()
def resizeEvent(self, event: QtCore.QEvent) -> None:
'''
Top level god widget resize handler.
Where we do UX magic to make things not suck B)
'''
log.debug('god widget resize')
class ChartnPane(QFrame): class ChartnPane(QFrame):
'''One-off ``QFrame`` composite which pairs a chart '''
One-off ``QFrame`` composite which pairs a chart
+ sidepane (often a ``FieldsForm`` + other widgets if + sidepane (often a ``FieldsForm`` + other widgets if
provided) forming a, sort of, "chart row" with a side panel provided) forming a, sort of, "chart row" with a side panel
for configuration and display of off-chart data. for configuration and display of off-chart data.
@ -280,8 +294,6 @@ class ChartnPane(QFrame):
hbox.setContentsMargins(0, 0, 0, 0) hbox.setContentsMargins(0, 0, 0, 0)
hbox.setSpacing(3) hbox.setSpacing(3)
# self.setMaximumWidth()
class LinkedSplits(QWidget): class LinkedSplits(QWidget):
''' '''
@ -460,7 +472,10 @@ class LinkedSplits(QWidget):
self.xaxis.hide() self.xaxis.hide()
self.xaxis = xaxis self.xaxis = xaxis
qframe = ChartnPane(sidepane=sidepane, parent=self.splitter) qframe = ChartnPane(
sidepane=sidepane,
parent=self.splitter,
)
cpw = ChartPlotWidget( cpw = ChartPlotWidget(
# this name will be used to register the primary # this name will be used to register the primary
@ -554,17 +569,23 @@ class LinkedSplits(QWidget):
else: else:
assert style == 'bar', 'main chart must be OHLC' assert style == 'bar', 'main chart must be OHLC'
self.resize_sidepanes()
return cpw return cpw
def resize_sidepanes( def resize_sidepanes(
self, self,
) -> None: ) -> None:
'''Size all sidepanes based on the OHLC "main" plot. '''
Size all sidepanes based on the OHLC "main" plot and its
sidepane width.
''' '''
main_chart = self.chart
if main_chart:
sp_w = main_chart.sidepane.width()
for name, cpw in self.subplots.items(): for name, cpw in self.subplots.items():
cpw.sidepane.setMinimumWidth(self.chart.sidepane.width()) cpw.sidepane.setMinimumWidth(sp_w)
cpw.sidepane.setMaximumWidth(self.chart.sidepane.width()) cpw.sidepane.setMaximumWidth(sp_w)
class ChartPlotWidget(pg.PlotWidget): class ChartPlotWidget(pg.PlotWidget):
@ -672,12 +693,14 @@ class ChartPlotWidget(pg.PlotWidget):
self._vb.sigResized.connect(self._set_yrange) self._vb.sigResized.connect(self._set_yrange)
async def resume_all_feeds(self): async def resume_all_feeds(self):
async with trio.open_nursery() as n:
for feed in self._feeds.values(): for feed in self._feeds.values():
await feed.resume() n.start_soon(feed.resume)
async def pause_all_feeds(self): async def pause_all_feeds(self):
async with trio.open_nursery() as n:
for feed in self._feeds.values(): for feed in self._feeds.values():
await feed.pause() n.start_soon(feed.pause)
@property @property
def view(self) -> ChartView: def view(self) -> ChartView: