From 824c81da4153be745379907eb2199826b02e49b6 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sat, 18 Sep 2021 17:09:30 -0400 Subject: [PATCH 01/14] Add todo for new view padding testing --- piker/ui/_interaction.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/piker/ui/_interaction.py b/piker/ui/_interaction.py index 97222065..9f33253d 100644 --- a/piker/ui/_interaction.py +++ b/piker/ui/_interaction.py @@ -341,7 +341,14 @@ class ChartView(ViewBox): **kwargs, ): - super().__init__(parent=parent, **kwargs) + super().__init__( + parent=parent, + # TODO: look into the default view padding + # support that might replace somem of our + # ``ChartPlotWidget._set_yrange()` + # defaultPadding=0., + **kwargs + ) # disable vertical scrolling self.setMouseEnabled(x=True, y=False) @@ -533,7 +540,6 @@ class ChartView(ViewBox): # self.updateScaleBox(ev.buttonDownPos(), ev.pos()) else: # default bevavior: click to pan view - tr = self.childGroup.transform() tr = fn.invertQTransform(tr) tr = tr.map(dif*mask) - tr.map(Point(0, 0)) From 51def5484e04b43fb4dbfa0673a206ff75ef65b6 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 28 Sep 2021 08:34:58 -0400 Subject: [PATCH 02/14] `graphics_name` is more explicit then `name` --- piker/ui/_chart.py | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index c564d3de..1678d553 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -379,16 +379,21 @@ class LinkedSplits(QWidget): style: str = 'bar', ) -> 'ChartPlotWidget': - """Start up and show main (price) chart and all linked subcharts. + '''Start up and show main (price) chart and all linked subcharts. The data input struct array must include OHLC fields. - """ + + ''' # add crosshairs self.cursor = Cursor( linkedsplits=self, digits=symbol.tick_size_digits, ) + # NOTE: atm the first (and only) OHLC price chart for the symbol + # is given a special reference but in the future there shouldn't + # be no distinction since we will have multiple symbols per + # view as part of "aggregate feeds". self.chart = self.add_plot( name=symbol.key, @@ -430,9 +435,7 @@ class LinkedSplits(QWidget): **cpw_kwargs, ) -> 'ChartPlotWidget': - '''Add (sub)plots to chart widget by name. - - If ``name`` == ``"main"`` the chart will be the the primary view. + '''Add (sub)plots to chart widget by key. ''' if self.chart is None and not _is_main: @@ -910,25 +913,26 @@ class ChartPlotWidget(pg.PlotWidget): def update_ohlc_from_array( self, - name: str, + + graphics_name: str, array: np.ndarray, **kwargs, - ) -> pg.GraphicsObject: - """Update the named internal graphics from ``array``. - """ + ) -> pg.GraphicsObject: + '''Update the named internal graphics from ``array``. + + ''' self._arrays['ohlc'] = array - graphics = self._graphics[name] + graphics = self._graphics[graphics_name] graphics.update_from_array(array, **kwargs) return graphics def update_curve_from_array( self, - name: str, + graphics_name: str, array: np.ndarray, array_key: Optional[str] = None, - **kwargs, ) -> pg.GraphicsObject: @@ -936,20 +940,24 @@ class ChartPlotWidget(pg.PlotWidget): """ - data_key = array_key or name - if name not in self._overlays: + data_key = array_key or graphics_name + if graphics_name not in self._overlays: self._arrays['ohlc'] = array else: self._arrays[data_key] = array - curve = self._graphics[name] + curve = self._graphics[graphics_name] if len(array): # TODO: we should instead implement a diff based # "only update with new items" on the pg.PlotCurveItem # one place to dig around this might be the `QBackingStore` # https://doc.qt.io/qt-5/qbackingstore.html - # curve.setData(y=array[name], x=array['index'], **kwargs) + + # NOTE: back when we weren't implementing the curve graphics + # ourselves you'd have updates using this method: + # curve.setData(y=array[graphics_name], x=array['index'], **kwargs) + curve.update_from_array( x=array['index'], y=array[data_key], @@ -1034,6 +1042,8 @@ class ChartPlotWidget(pg.PlotWidget): # self._set_xlimits(begin, end) # TODO: this should be some kind of numpy view api + + # bars = self._arrays['ohlc'][lbar:rbar] a = self._arrays['ohlc'] From 51373789fe53d9117e0706eb43978257639d66c3 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 28 Sep 2021 16:37:55 -0400 Subject: [PATCH 03/14] Autoscale the y-range for all linked charts --- piker/ui/_chart.py | 51 ++++++++++++++++------------------------------ 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index 1678d553..04f24d50 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -972,7 +972,11 @@ class ChartPlotWidget(pg.PlotWidget): yrange: Optional[tuple[float, float]] = None, range_margin: float = 0.06, - bars_range: Optional[tuple[int, int, int, int]] = None + bars_range: Optional[tuple[int, int, int, int]] = None, + + # flag to prevent triggering sibling charts from the same linked + # set from recursion errors. + autoscale_linked_plots: bool = True, ) -> None: '''Set the viewable y-range based on embedded data. @@ -999,52 +1003,33 @@ class ChartPlotWidget(pg.PlotWidget): l, lbar, rbar, r = bars_range or self.bars_range() - # TODO: we need a loop for auto-scaled subplots to all - # be triggered by one another - if self.name != 'volume': - vlm_chart = self.linked.subplots.get('volume') - if vlm_chart: - vlm_chart._set_yrange(bars_range=(l, lbar, rbar, r)) - # curve = vlm_chart._graphics['volume'] - # if rbar - lbar < 1500: - # # print('small range') - # curve._fill = True - # else: - # curve._fill = False + if autoscale_linked_plots: + # avoid recursion by sibling plots + linked = self.linked + plots = list(linked.subplots.copy().values()) + main = linked.chart + if main: + plots.append(main) - # figure out x-range in view such that user can scroll "off" - # the data set up to the point where ``_min_points_to_show`` - # are left. - # view_len = r - l + for chart in plots: + if chart and not chart._static_yrange: + chart._set_yrange( + bars_range=(l, lbar, rbar, r), + autoscale_linked_plots=False, + ) # TODO: logic to check if end of bars in view # extra = view_len - _min_points_to_show - # begin = self._arrays['ohlc'][0]['index'] - extra - # # end = len(self._arrays['ohlc']) - 1 + extra # end = self._arrays['ohlc'][-1]['index'] - 1 + extra - # XXX: test code for only rendering lines for the bars in view. - # This turns out to be very very poor perf when scaling out to - # many bars (think > 1k) on screen. - # name = self.name - # bars = self._graphics[self.name] - # bars.draw_lines( - # istart=max(lbar, l), iend=min(rbar, r), just_history=True) - # bars_len = rbar - lbar # log.debug( # f"\nl: {l}, lbar: {lbar}, rbar: {rbar}, r: {r}\n" # f"view_len: {view_len}, bars_len: {bars_len}\n" # f"begin: {begin}, end: {end}, extra: {extra}" # ) - # self._set_xlimits(begin, end) - - # TODO: this should be some kind of numpy view api - - - # bars = self._arrays['ohlc'][lbar:rbar] a = self._arrays['ohlc'] ifirst = a[0]['index'] From 9951e1d4c95b2c02eec9aa452b2612655e1bca1e Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 30 Sep 2021 07:33:43 -0400 Subject: [PATCH 04/14] Fix shm index update race There was a lingering issue where the fsp daemon would sync its shm array with the source data and we'd set the start/end indices to the same value. Under some races a reader would then read an empty `.array` which it wasn't expecting. This fixes that as well as tidies up the `ShmArray.push()` logic and adds a temporary check in `.array` for zero length if the array hasn't been written yet. We can now start removing read array length checks in consumer code and hopefully no more races will show up. Revert to old shm "last" meaning last row --- piker/ui/_chart.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index 04f24d50..dc9387ea 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -936,11 +936,12 @@ class ChartPlotWidget(pg.PlotWidget): **kwargs, ) -> pg.GraphicsObject: - """Update the named internal graphics from ``array``. - - """ + '''Update the named internal graphics from ``array``. + ''' + assert len(array) data_key = array_key or graphics_name + if graphics_name not in self._overlays: self._arrays['ohlc'] = array else: @@ -948,21 +949,19 @@ class ChartPlotWidget(pg.PlotWidget): curve = self._graphics[graphics_name] - if len(array): - # TODO: we should instead implement a diff based - # "only update with new items" on the pg.PlotCurveItem - # one place to dig around this might be the `QBackingStore` - # https://doc.qt.io/qt-5/qbackingstore.html + # NOTE: back when we weren't implementing the curve graphics + # ourselves you'd have updates using this method: + # curve.setData(y=array[graphics_name], x=array['index'], **kwargs) - # NOTE: back when we weren't implementing the curve graphics - # ourselves you'd have updates using this method: - # curve.setData(y=array[graphics_name], x=array['index'], **kwargs) - - curve.update_from_array( - x=array['index'], - y=array[data_key], - **kwargs - ) + # NOTE: graphics **must** implement a diff based update + # operation where an internal ``FastUpdateCurve._xrange`` is + # used to determine if the underlying path needs to be + # pre/ap-pended. + curve.update_from_array( + x=array['index'], + y=array[data_key], + **kwargs + ) return curve From cf2d258a276d25d3a69427c7ec7be6e57aadc355 Mon Sep 17 00:00:00 2001 From: wattygetlood <61716739+wattygetlood@users.noreply.github.com> Date: Thu, 16 Sep 2021 16:36:09 -0400 Subject: [PATCH 05/14] Only scale down for scale < 2 --- piker/ui/_style.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/piker/ui/_style.py b/piker/ui/_style.py index 34b5cb01..a7f6d985 100644 --- a/piker/ui/_style.py +++ b/piker/ui/_style.py @@ -124,14 +124,18 @@ class DpiAwareFont: if scale > 1 and self._font_size: # TODO: this denominator should probably be determined from # relative aspect ratios or something? - inches = inches * (1 / scale) * (1 + 6/16) + inches = inches * (1 + 6/16) + if scale < 2: + inches *= (1 / scale) dpi = mx_dpi + log.info(f'USING MAX DPI {dpi}') self._font_inches = inches font_size = math.floor(inches * dpi) - log.debug( - f"\nscreen:{screen.name()} with pDPI: {pdpi}, lDPI: {ldpi}" + log.info( + f"\nscreen:{screen.name()}" + f"pDPI: {pdpi}, lDPI: {ldpi}, scale: {scale}\n" f"\nOur best guess font size is {font_size}\n" ) # apply the size From 8b1232947931ae4d4cdaccbd9d9029215d1e4a47 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Wed, 6 Oct 2021 20:17:13 -0400 Subject: [PATCH 06/14] Make openGL flag actually work.. --- piker/ui/_chart.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index dc9387ea..8cbbcaa9 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -603,6 +603,9 @@ class ChartPlotWidget(pg.PlotWidget): view_color: str = 'papas_special', pen_color: str = 'bracket', + # TODO: load from config + use_open_gl: bool = False, + static_yrange: Optional[tuple[float, float]] = None, **kwargs, @@ -617,9 +620,9 @@ class ChartPlotWidget(pg.PlotWidget): # parent=None, # plotItem=None, # antialias=True, - # useOpenGL=True, **kwargs ) + self.useOpenGL(use_open_gl) self.name = name self.data_key = data_key self.linked = linkedsplits From e178c18745529baeb5616c273e4ab81ae76002a2 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 22 Oct 2021 12:13:38 -0400 Subject: [PATCH 07/14] Please please please let this dpi scaling hack work --- piker/ui/_style.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/piker/ui/_style.py b/piker/ui/_style.py index a7f6d985..0bad2895 100644 --- a/piker/ui/_style.py +++ b/piker/ui/_style.py @@ -110,7 +110,7 @@ class DpiAwareFont: mx_dpi = max(pdpi, ldpi) mn_dpi = min(pdpi, ldpi) - scale = round(ldpi/pdpi) + scale = round(ldpi/pdpi, ndigits=2) if mx_dpi <= 97: # for low dpi use larger font sizes inches = _font_sizes['lo'][self._font_size] @@ -121,20 +121,28 @@ class DpiAwareFont: dpi = mn_dpi # dpi is likely somewhat scaled down so use slightly larger font size - if scale > 1 and self._font_size: - # TODO: this denominator should probably be determined from - # relative aspect ratios or something? - inches = inches * (1 + 6/16) - if scale < 2: - inches *= (1 / scale) + if scale >= 1.1 and self._font_size: + + if 1.2 <= scale: + inches *= (1 / scale) * 1.0616 + + if scale < 1.4 or scale >= 1.5: + # TODO: this denominator should probably be determined from + # relative aspect ratios or something? + inches = inches * (1 + 6/16) + dpi = mx_dpi log.info(f'USING MAX DPI {dpi}') + # TODO: we might want to fiddle with incrementing font size by + # +1 for the edge cases above. it seems doing it via scaling is + # always going to hit that error in range mapping from inches: + # float to px size: int. self._font_inches = inches - font_size = math.floor(inches * dpi) + log.info( - f"\nscreen:{screen.name()}" + f"screen:{screen.name()}]\n" f"pDPI: {pdpi}, lDPI: {ldpi}, scale: {scale}\n" f"\nOur best guess font size is {font_size}\n" ) From 61331fee67d7942a368b35ee6f393c5601e993d3 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 2 Nov 2021 14:05:15 -0400 Subject: [PATCH 08/14] Drop order status bar down a font px size --- piker/ui/_forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/piker/ui/_forms.py b/piker/ui/_forms.py index 45d68317..72053716 100644 --- a/piker/ui/_forms.py +++ b/piker/ui/_forms.py @@ -732,7 +732,7 @@ def mk_order_pane_layout( ) -> FieldsForm: - font_size: int = _font.px_size - 1 + font_size: int = _font.px_size - 2 # TODO: maybe just allocate the whole fields form here # and expect an async ctx entry? From c1cf4c787633a36a3e8255c685219f7183411196 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 2 Dec 2021 08:08:10 -0500 Subject: [PATCH 09/14] New font scaling dpi heuristics (which i don't grok) --- piker/ui/_style.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/piker/ui/_style.py b/piker/ui/_style.py index 0bad2895..b3194427 100644 --- a/piker/ui/_style.py +++ b/piker/ui/_style.py @@ -123,16 +123,17 @@ class DpiAwareFont: # dpi is likely somewhat scaled down so use slightly larger font size if scale >= 1.1 and self._font_size: + # no idea why if 1.2 <= scale: - inches *= (1 / scale) * 1.0616 + mult = 1.0375 - if scale < 1.4 or scale >= 1.5: - # TODO: this denominator should probably be determined from - # relative aspect ratios or something? - inches = inches * (1 + 6/16) + if scale >= 2: + mult = 1.375 - dpi = mx_dpi - log.info(f'USING MAX DPI {dpi}') + + # TODO: this multiplier should probably be determined from + # relative aspect ratios or something? + inches *= mult # TODO: we might want to fiddle with incrementing font size by # +1 for the edge cases above. it seems doing it via scaling is From f21c68a67252466efa55beba6042b7aa4969197b Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 3 Dec 2021 12:43:54 -0500 Subject: [PATCH 10/14] i dunno, but display scaling is wack --- piker/ui/_style.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/piker/ui/_style.py b/piker/ui/_style.py index b3194427..1b63e08f 100644 --- a/piker/ui/_style.py +++ b/piker/ui/_style.py @@ -127,10 +127,9 @@ class DpiAwareFont: if 1.2 <= scale: mult = 1.0375 - if scale >= 2: + if scale >= 1.5: mult = 1.375 - # TODO: this multiplier should probably be determined from # relative aspect ratios or something? inches *= mult From 56b65a1cde0c41f9503e648b9e220c7b9cacd5e0 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 20 Dec 2021 13:15:16 -0500 Subject: [PATCH 11/14] 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. --- piker/ui/_chart.py | 87 +++++++++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 32 deletions(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index 8cbbcaa9..e81b3df5 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -18,6 +18,7 @@ High level chart-widget apis. ''' +from __future__ import annotations from typing import Optional from PyQt5 import QtCore, QtWidgets @@ -68,7 +69,7 @@ log = get_logger(__name__) class GodWidget(QWidget): ''' "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 organizing charts as well as other sub-widgets used to control or @@ -104,8 +105,8 @@ class GodWidget(QWidget): # self.init_strategy_ui() # self.vbox.addLayout(self.hbox) - self._chart_cache = {} - self.linkedsplits: 'LinkedSplits' = None + self._chart_cache: dict[str, LinkedSplits] = {} + self.linkedsplits: Optional[LinkedSplits] = None # assigned in the startup func `_async_main()` self._root_n: trio.Nursery = None @@ -135,7 +136,7 @@ class GodWidget(QWidget): def set_chart_symbol( self, symbol_key: str, # of form . - linkedsplits: 'LinkedSplits', # type: ignore + linkedsplits: LinkedSplits, # type: ignore ) -> None: # re-sort org cache symbol list in LIFO order @@ -146,20 +147,20 @@ class GodWidget(QWidget): def get_chart_symbol( self, symbol_key: str, - ) -> 'LinkedSplits': # type: ignore + + ) -> LinkedSplits: # type: ignore return self._chart_cache.get(symbol_key) async def load_symbol( self, - providername: str, symbol_key: str, loglevel: str, - reset: bool = False, ) -> 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. @@ -178,6 +179,7 @@ class GodWidget(QWidget): # XXX: this is CRITICAL especially with pixel buffer caching self.linkedsplits.hide() + self.linkedsplits.unfocus() # XXX: pretty sure we don't need this # remove any existing plots? @@ -202,6 +204,11 @@ class GodWidget(QWidget): ) self.set_chart_symbol(fqsn, linkedsplits) + self.vbox.addWidget(linkedsplits) + + linkedsplits.show() + linkedsplits.focus() + await trio.sleep(0) else: # 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 # self.linkedsplits.chart.qframe.hbox.removeWidget(self.pp_pane) chart = linkedsplits.chart + + # chart is already in memory so just focus it + linkedsplits.show() + linkedsplits.focus() + await trio.sleep(0) + + # resume feeds *after* rendering chart view asap await chart.resume_all_feeds() - # chart is already in memory so just focus it - if self.linkedsplits: - self.linkedsplits.unfocus() - - self.vbox.addWidget(linkedsplits) - - linkedsplits.show() - linkedsplits.focus() - self.linkedsplits = linkedsplits - symbol = linkedsplits.symbol - if symbol is not None: self.window.setWindowTitle( f'{symbol.key}@{symbol.brokers} ' @@ -239,7 +242,8 @@ class GodWidget(QWidget): return order_mode_started 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". ''' @@ -247,9 +251,19 @@ class GodWidget(QWidget): self.clearFocus() 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): - '''One-off ``QFrame`` composite which pairs a chart + ''' + One-off ``QFrame`` composite which pairs a chart + sidepane (often a ``FieldsForm`` + other widgets if provided) forming a, sort of, "chart row" with a side panel for configuration and display of off-chart data. @@ -280,8 +294,6 @@ class ChartnPane(QFrame): hbox.setContentsMargins(0, 0, 0, 0) hbox.setSpacing(3) - # self.setMaximumWidth() - class LinkedSplits(QWidget): ''' @@ -460,7 +472,10 @@ class LinkedSplits(QWidget): self.xaxis.hide() self.xaxis = xaxis - qframe = ChartnPane(sidepane=sidepane, parent=self.splitter) + qframe = ChartnPane( + sidepane=sidepane, + parent=self.splitter, + ) cpw = ChartPlotWidget( # this name will be used to register the primary @@ -554,17 +569,23 @@ class LinkedSplits(QWidget): else: assert style == 'bar', 'main chart must be OHLC' + self.resize_sidepanes() return cpw def resize_sidepanes( self, ) -> None: - '''Size all sidepanes based on the OHLC "main" plot. + ''' + Size all sidepanes based on the OHLC "main" plot and its + sidepane width. ''' - for name, cpw in self.subplots.items(): - cpw.sidepane.setMinimumWidth(self.chart.sidepane.width()) - cpw.sidepane.setMaximumWidth(self.chart.sidepane.width()) + main_chart = self.chart + if main_chart: + sp_w = main_chart.sidepane.width() + for name, cpw in self.subplots.items(): + cpw.sidepane.setMinimumWidth(sp_w) + cpw.sidepane.setMaximumWidth(sp_w) class ChartPlotWidget(pg.PlotWidget): @@ -672,12 +693,14 @@ class ChartPlotWidget(pg.PlotWidget): self._vb.sigResized.connect(self._set_yrange) async def resume_all_feeds(self): - for feed in self._feeds.values(): - await feed.resume() + async with trio.open_nursery() as n: + for feed in self._feeds.values(): + n.start_soon(feed.resume) async def pause_all_feeds(self): - for feed in self._feeds.values(): - await feed.pause() + async with trio.open_nursery() as n: + for feed in self._feeds.values(): + n.start_soon(feed.pause) @property def view(self) -> ChartView: From 644ac6661c0c1de2da556bf8651b0cdad2394704 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 20 Dec 2021 13:53:29 -0500 Subject: [PATCH 12/14] Fix sidepane alignment with FSP charts Call the resize method only after all FSP subcharts have rendered such that the main OHLC chart's final width is read. Further tweaks: - drop rsi by default - drop the stream drain stuff - fix failed-to-read shm logging --- piker/ui/_display.py | 91 +++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/piker/ui/_display.py b/piker/ui/_display.py index 43085b7d..f7d78152 100644 --- a/piker/ui/_display.py +++ b/piker/ui/_display.py @@ -61,7 +61,10 @@ log = get_logger(__name__) _quote_throttle_rate: int = 58 # Hz -def try_read(array: np.ndarray) -> Optional[np.ndarray]: +def try_read( + array: np.ndarray + +) -> Optional[np.ndarray]: ''' Try to read the last row from a shared mem array or ``None`` if the array read returns a zero-length array result. @@ -85,10 +88,9 @@ def try_read(array: np.ndarray) -> Optional[np.ndarray]: # something we need anyway, maybe there should be some kind of # signal that a prepend is taking place and this consumer can # respond (eg. redrawing graphics) accordingly. - log.warning(f'Read-race on shm array: {graphics_name}@{shm.token}') - # the array read was emtpy - return None + # the array read was emtpy + return None def update_fsp_chart( @@ -101,8 +103,10 @@ def update_fsp_chart( array = shm.array last_row = try_read(array) + # guard against unreadable case if not last_row: + log.warning(f'Read-race on shm array: {graphics_name}@{shm.token}') return # update graphics @@ -175,7 +179,6 @@ def chart_maxmin( async def update_chart_from_quotes( - linked: LinkedSplits, stream: tractor.MsgStream, ohlcv: np.ndarray, @@ -247,17 +250,16 @@ async def update_chart_from_quotes( chart.show() last_quote = time.time() - # NOTE: all code below this loop is expected to be synchronous - # and thus draw instructions are not picked up jntil the next - # wait / iteration. async for quotes in stream: now = time.time() - quote_period = now - last_quote - quote_rate = round(1/quote_period, 1) if quote_period else float('inf') + quote_period = time.time() - last_quote + quote_rate = round( + 1/quote_period, 1) if quote_period > 0 else float('inf') + if ( quote_period <= 1/_quote_throttle_rate - and quote_rate > _quote_throttle_rate + 2 + and quote_rate > _quote_throttle_rate * 1.5 ): log.warning(f'High quote rate {symbol.key}: {quote_rate}') last_quote = now @@ -454,7 +456,8 @@ def maybe_mk_fsp_shm( readonly: bool = True, ) -> (ShmArray, bool): - '''Allocate a single row shm array for an symbol-fsp pair if none + ''' + Allocate a single row shm array for an symbol-fsp pair if none exists, otherwise load the shm already existing for that token. ''' @@ -481,7 +484,6 @@ def maybe_mk_fsp_shm( @acm async def open_fsp_sidepane( - linked: LinkedSplits, conf: dict[str, dict[str, str]], @@ -570,6 +572,7 @@ async def open_fsp_cluster( async def maybe_open_fsp_cluster( workers: int = 2, **kwargs, + ) -> AsyncGenerator[int, dict[str, tractor.Portal]]: kwargs.update( @@ -589,7 +592,6 @@ async def maybe_open_fsp_cluster( async def start_fsp_displays( - cluster_map: dict[str, tractor.Portal], linkedsplits: LinkedSplits, fsps: dict[str, str], @@ -603,7 +605,8 @@ async def start_fsp_displays( display_in_own_task: bool = False, ) -> None: - '''Create sub-actors (under flat tree) + ''' + Create sub-actors (under flat tree) for each entry in config and attach to local graphics update tasks. Pass target entrypoint and historical data. @@ -668,9 +671,7 @@ async def start_fsp_displays( async def update_chart_from_fsp( - portal: tractor.Portal, - linkedsplits: LinkedSplits, brokermod: ModuleType, sym: str, @@ -687,7 +688,8 @@ async def update_chart_from_fsp( profiler: pg.debug.Profiler, ) -> None: - '''FSP stream chart update loop. + ''' + FSP stream chart update loop. This is called once for each entry in the fsp config map. @@ -792,9 +794,7 @@ async def update_chart_from_fsp( level_line(chart, 80, orient_v='top') chart._set_yrange() - - done() - chart.linked.resize_sidepanes() + done() # status updates profiler(f'fsp:{func_name} starting update loop') profiler.finish() @@ -912,7 +912,6 @@ def has_vlm(ohlcv: ShmArray) -> bool: @acm async def maybe_open_vlm_display( - linked: LinkedSplits, ohlcv: ShmArray, @@ -992,20 +991,10 @@ async def maybe_open_vlm_display( # size view to data once at outset chart._set_yrange() - # size pain to parent chart - # TODO: this appears to nearly fix a bug where the vlm sidepane - # could be sized correctly nearly immediately (since the - # order pane is already sized), right now it doesn't seem to - # fully align until the VWAP fsp-actor comes up... - await trio.sleep(0) - chart.linked.resize_sidepanes() - await trio.sleep(0) - yield chart async def display_symbol_data( - godwidget: GodWidget, provider: str, sym: str, @@ -1116,24 +1105,24 @@ async def display_symbol_data( 'chart_kwargs': {'style': 'step'} }, - 'rsi': { - 'func_name': 'rsi', # literal python func ref lookup name + # 'rsi': { + # 'func_name': 'rsi', # literal python func ref lookup name - # map of parameters to place on the fsp sidepane widget - # which should map to dynamic inputs available to the - # fsp function at runtime. - 'params': { - 'period': { - 'default_value': 14, - 'widget_kwargs': {'readonly': True}, - }, - }, + # # map of parameters to place on the fsp sidepane widget + # # which should map to dynamic inputs available to the + # # fsp function at runtime. + # 'params': { + # 'period': { + # 'default_value': 14, + # 'widget_kwargs': {'readonly': True}, + # }, + # }, - # ``ChartPlotWidget`` options passthrough - 'chart_kwargs': { - 'static_yrange': (0, 100), - }, - }, + # # ``ChartPlotWidget`` options passthrough + # 'chart_kwargs': { + # 'static_yrange': (0, 100), + # }, + # }, } if has_vlm(ohlcv): # and provider != 'binance': @@ -1201,4 +1190,10 @@ async def display_symbol_data( order_mode_started ) ): + # let Qt run to render all widgets and make sure the + # sidepanes line up vertically. + await trio.sleep(0) + linkedsplits.resize_sidepanes() + + # let the app run. await trio.sleep_forever() From 8722cf4c495299a84cb004e1960208bd6bfacb50 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 20 Dec 2021 14:21:46 -0500 Subject: [PATCH 13/14] Give a single FSP subchart more space --- piker/ui/_chart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index e81b3df5..1ee2ea93 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -361,7 +361,7 @@ class LinkedSplits(QWidget): if not prop: # proportion allocated to consumer subcharts if ln < 2: - prop = 1/(.666 * 6) + prop = 1/(.375 * 6) elif ln >= 2: prop = 3/8 From 9c57f10e7701bcae54c3048e6e024f885b8c10ff Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Wed, 22 Dec 2021 08:30:22 -0500 Subject: [PATCH 14/14] Make pause/resume feed methods sync again We can instead use the god widget's nursery to schedule all the feed pause/resume requests and be even more concurrent during a view (of symbols) switch. Use `tractor.trionics.gather_contexts()` to start up the fsp and volume chart-displays (for an additional conc speedup). Drop `dolla_vlm` again for now until we figure out how we can display it *and* vlm on the same sub-chart? It would be nice to avoid having to spawn an fsp process before showing the volume curve. --- piker/ui/_chart.py | 25 +++++++++-------- piker/ui/_display.py | 66 +++++++++++++++++++------------------------- 2 files changed, 42 insertions(+), 49 deletions(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index 1ee2ea93..6d4ebc83 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -229,7 +229,7 @@ class GodWidget(QWidget): await trio.sleep(0) # resume feeds *after* rendering chart view asap - await chart.resume_all_feeds() + chart.resume_all_feeds() self.linkedsplits = linkedsplits symbol = linkedsplits.symbol @@ -361,7 +361,7 @@ class LinkedSplits(QWidget): if not prop: # proportion allocated to consumer subcharts if ln < 2: - prop = 1/(.375 * 6) + prop = 1/3 elif ln >= 2: prop = 3/8 @@ -631,8 +631,11 @@ class ChartPlotWidget(pg.PlotWidget): **kwargs, ): - """Configure chart display settings. - """ + ''' + Configure initial display settings and connect view callback + handlers. + + ''' self.view_color = view_color self.pen_color = pen_color @@ -692,15 +695,13 @@ class ChartPlotWidget(pg.PlotWidget): # for when the splitter(s) are resized self._vb.sigResized.connect(self._set_yrange) - async def resume_all_feeds(self): - async with trio.open_nursery() as n: - for feed in self._feeds.values(): - n.start_soon(feed.resume) + def resume_all_feeds(self): + for feed in self._feeds.values(): + self.linked.godwidget._root_n.start_soon(feed.resume) - async def pause_all_feeds(self): - async with trio.open_nursery() as n: - for feed in self._feeds.values(): - n.start_soon(feed.pause) + def pause_all_feeds(self): + for feed in self._feeds.values(): + self.linked.godwidget._root_n.start_soon(feed.pause) @property def view(self) -> ChartView: diff --git a/piker/ui/_display.py b/piker/ui/_display.py index f7d78152..4c4aed1f 100644 --- a/piker/ui/_display.py +++ b/piker/ui/_display.py @@ -36,7 +36,7 @@ import trio from .. import brokers from .._cacheables import maybe_open_context -from ..trionics import async_enter_all +from tractor.trionics import gather_contexts from ..data.feed import open_feed, Feed from ._chart import ( ChartPlotWidget, @@ -266,7 +266,7 @@ async def update_chart_from_quotes( # chart isn't active/shown so skip render cycle and pause feed(s) if chart.linked.isHidden(): - await chart.pause_all_feeds() + chart.pause_all_feeds() continue for sym, quote in quotes.items(): @@ -601,9 +601,6 @@ async def start_fsp_displays( group_status_key: str, loglevel: str, - # this con - display_in_own_task: bool = False, - ) -> None: ''' Create sub-actors (under flat tree) @@ -626,7 +623,7 @@ async def start_fsp_displays( for (display_name, conf), (name, portal) in zip( fsps.items(), - # rr to cluster for now.. + # round robin to cluster for now.. cycle(cluster_map.items()), ): func_name = conf['func_name'] @@ -925,16 +922,14 @@ async def maybe_open_vlm_display( shm, opened = maybe_mk_fsp_shm( linked.symbol.key, - '$_vlm', + 'vlm', readonly=True, ) async with open_fsp_sidepane( linked, { 'vlm': { - 'params': { - 'price_func': { 'default_value': 'chl3', # tell target ``Edit`` widget to not allow @@ -962,9 +957,6 @@ async def maybe_open_vlm_display( # we do this internally ourselves since # the curve item internals are pretty convoluted. style='step', - - # original pyqtgraph flag for reference - # stepMode=True, ) # XXX: ONLY for sub-chart fsps, overlays have their @@ -999,7 +991,6 @@ async def display_symbol_data( provider: str, sym: str, loglevel: str, - order_mode_started: trio.Event, ) -> None: @@ -1026,8 +1017,7 @@ async def display_symbol_data( # group_key=loading_sym_key, # ) - async with async_enter_all( - open_feed( + async with open_feed( provider, [sym], loglevel=loglevel, @@ -1035,11 +1025,8 @@ async def display_symbol_data( # limit to at least display's FPS # avoiding needless Qt-in-guest-mode context switches tick_throttle=_quote_throttle_rate, - ), - maybe_open_fsp_cluster(), - - ) as (feed, cluster_map): + ) as feed: ohlcv: ShmArray = feed.shm bars = ohlcv.array symbol = feed.symbols[sym] @@ -1091,19 +1078,19 @@ async def display_symbol_data( # TODO: eventually we'll support some kind of n-compose syntax fsp_conf = { - 'dolla_vlm': { - 'func_name': 'dolla_vlm', - 'zero_on_step': True, - 'params': { - 'price_func': { - 'default_value': 'chl3', - # tell target ``Edit`` widget to not allow - # edits for now. - 'widget_kwargs': {'readonly': True}, - }, - }, - 'chart_kwargs': {'style': 'step'} - }, + # 'dolla_vlm': { + # 'func_name': 'dolla_vlm', + # 'zero_on_step': True, + # 'params': { + # 'price_func': { + # 'default_value': 'chl3', + # # tell target ``Edit`` widget to not allow + # # edits for now. + # 'widget_kwargs': {'readonly': True}, + # }, + # }, + # 'chart_kwargs': {'style': 'step'} + # }, # 'rsi': { # 'func_name': 'rsi', # literal python func ref lookup name @@ -1147,10 +1134,15 @@ async def display_symbol_data( await trio.sleep(0) vlm_chart = None - async with ( - trio.open_nursery() as ln, - maybe_open_vlm_display(linkedsplits, ohlcv) as vlm_chart, - ): + + async with gather_contexts( + ( + trio.open_nursery(), + maybe_open_vlm_display(linkedsplits, ohlcv), + maybe_open_fsp_cluster(), + ) + ) as (ln, vlm_chart, cluster_map): + # load initial fsp chain (otherwise known as "indicators") ln.start_soon( start_fsp_displays, @@ -1164,7 +1156,7 @@ async def display_symbol_data( loglevel, ) - # start graphics update loop(s)after receiving first live quote + # start graphics update loop after receiving first live quote ln.start_soon( update_chart_from_quotes, linkedsplits,