Rework display loop maxmin-ing with `Viz` pipelining

First, we rename what was `chart_maxmin()` -> `multi_maxmin()` and don't
`partial` it in to the `DisplayState`, just call it with correct `Viz`
ref inputs.

Second, as we've done with `ChartView.maybe_downsample_graphics()` use
the output from the main `Viz.update_graphics()` and feed it to the
`.maxmin()` calls for the ohlc and vlm chart but still deliver the same
output signature as prior. Also accept and use an optional profiler
input, drop `DisplayState.maxmin()` and add `.vlm_viz`.

Further perf related tweak to do with more efficient incremental
updates:
- only call `multi_maxmin()` if the main fast chart viz does a pixel
  column step.
- mask out hist viz and vlm viz and all linked fsp `._set_yrange()`
  calls for now until we figure out how to best optimize these updates
  when considering the new group-scaled-by-% style for multicharts.
- drop `.enable_auto_yrange()` calls during startup.
overlays_interaction_latency_tuning
Tyler Goodlet 2023-01-18 16:19:08 -05:00
parent 9780263cfa
commit 8b5b1c214b
1 changed files with 103 additions and 72 deletions

View File

@ -25,7 +25,10 @@ from functools import partial
import itertools import itertools
from math import floor from math import floor
import time import time
from typing import Optional, Any, Callable from typing import (
Optional,
Any,
)
import tractor import tractor
import trio import trio
@ -87,10 +90,11 @@ log = get_logger(__name__)
# approach, likely with ``numba``: # approach, likely with ``numba``:
# https://arxiv.org/abs/cs/0610046 # https://arxiv.org/abs/cs/0610046
# https://github.com/lemire/pythonmaxmin # https://github.com/lemire/pythonmaxmin
def chart_maxmin( def multi_maxmin(
i_read_range: tuple[int, int] | None,
fast_viz: Viz, fast_viz: Viz,
fqsn: str,
vlm_viz: Viz | None = None, vlm_viz: Viz | None = None,
profiler: Profiler = None,
) -> tuple[ ) -> tuple[
@ -104,26 +108,32 @@ def chart_maxmin(
Compute max and min datums "in view" for range limits. Compute max and min datums "in view" for range limits.
''' '''
out = fast_viz.maxmin() out = fast_viz.maxmin(
i_read_range=i_read_range,
)
if out is None: if out is None:
# log.warning(f'No yrange provided for {name}!?')
return (0, 0, 0) return (0, 0, 0)
( (
ixrng, ixrng,
read_slc, read_slc,
mxmn, yrange,
) = out ) = out
mn, mx = mxmn if profiler:
profiler('fast_viz.maxmin({read_slice})')
mx_vlm_in_view = 0 mn, mx = yrange
# TODO: we need to NOT call this to avoid a manual # TODO: we need to NOT call this to avoid a manual
# np.max/min trigger and especially on the vlm_chart # np.max/min trigger and especially on the vlm_chart
# vizs which aren't shown.. like vlm? # vizs which aren't shown.. like vlm?
mx_vlm_in_view = 0
if vlm_viz: if vlm_viz:
out = vlm_viz.maxmin() out = vlm_viz.maxmin(
i_read_range=i_read_range,
)
if out: if out:
( (
ixrng, ixrng,
@ -132,9 +142,16 @@ def chart_maxmin(
) = out ) = out
mx_vlm_in_view = mxmn[1] mx_vlm_in_view = mxmn[1]
if profiler:
profiler('vlm_viz.maxmin({read_slc})')
return ( return (
mx, mx,
max(mn, 0), # presuming price can't be negative?
# enforcing price can't be negative?
# TODO: do we even need this?
max(mn, 0),
mx_vlm_in_view, # vlm max mx_vlm_in_view, # vlm max
) )
@ -148,7 +165,6 @@ class DisplayState(Struct):
godwidget: GodWidget godwidget: GodWidget
quotes: dict[str, Any] quotes: dict[str, Any]
maxmin: Callable
flume: Flume flume: Flume
# high level chart handles and underlying ``Viz`` # high level chart handles and underlying ``Viz``
@ -162,6 +178,8 @@ class DisplayState(Struct):
last_price_sticky: YAxisLabel last_price_sticky: YAxisLabel
hist_last_price_sticky: YAxisLabel hist_last_price_sticky: YAxisLabel
vlm_viz: Viz
# misc state tracking # misc state tracking
vars: dict[str, Any] = field( vars: dict[str, Any] = field(
default_factory=lambda: { default_factory=lambda: {
@ -238,21 +256,22 @@ async def increment_history_view(
is_1m=True, is_1m=True,
) )
if do_px_step:
hist_viz.update_graphics()
profiler('`hist Viz.update_graphics()` call')
if liv:
hist_viz.plot.vb._set_yrange(viz=hist_viz)
profiler('hist chart yrange view')
# check if tread-in-place view x-shift is needed # check if tread-in-place view x-shift is needed
if should_tread: if should_tread:
# ensure path graphics append is shown on treads since # ensure path graphics append is shown on treads since
# the main rt loop does not call this. # the main rt loop does not call this.
hist_viz.update_graphics()
profiler('`hist Viz.update_graphics()` call')
hist_chart.increment_view(datums=append_diff) hist_chart.increment_view(datums=append_diff)
profiler('hist tread view') profiler('hist tread view')
if ( profiler.finish()
do_px_step
and liv
):
hist_viz.plot.vb._set_yrange(viz=hist_viz)
async def graphics_update_loop( async def graphics_update_loop(
@ -331,17 +350,15 @@ async def graphics_update_loop(
vlm_chart = vlm_charts[fqsn] vlm_chart = vlm_charts[fqsn]
vlm_viz = vlm_chart._vizs.get('volume') if vlm_chart else None vlm_viz = vlm_chart._vizs.get('volume') if vlm_chart else None
maxmin = partial(
chart_maxmin,
fast_viz,
fqsn,
vlm_viz,
)
( (
last_mx, last_mx,
last_mn, last_mn,
last_mx_vlm, last_mx_vlm,
) = maxmin() ) = multi_maxmin(
None,
fast_viz,
vlm_viz,
)
last, volume = ohlcv.array[-1][['close', 'volume']] last, volume = ohlcv.array[-1][['close', 'volume']]
@ -372,7 +389,7 @@ async def graphics_update_loop(
'fqsn': fqsn, 'fqsn': fqsn,
'godwidget': godwidget, 'godwidget': godwidget,
'quotes': {}, 'quotes': {},
'maxmin': maxmin, # 'maxmin': maxmin,
'flume': flume, 'flume': flume,
@ -384,6 +401,8 @@ async def graphics_update_loop(
'hist_viz': hist_viz, 'hist_viz': hist_viz,
'hist_last_price_sticky': hist_last_price_sticky, 'hist_last_price_sticky': hist_last_price_sticky,
'vlm_viz': vlm_viz,
'l1': l1, 'l1': l1,
'vars': { 'vars': {
@ -491,11 +510,10 @@ def graphics_update_cycle(
# or at least a little `mypyc` B) # or at least a little `mypyc` B)
# - pass more direct refs as input to avoid so many attr accesses? # - pass more direct refs as input to avoid so many attr accesses?
# - use a streaming minmax algo and drop the use of the # - use a streaming minmax algo and drop the use of the
# state-tracking ``chart_maxmin()`` routine from above? # state-tracking ``multi_maxmin()`` routine from above?
fqsn = ds.fqsn fqsn = ds.fqsn
chart = ds.chart chart = ds.chart
hist_chart = ds.hist_chart
vlm_chart = ds.vlm_chart vlm_chart = ds.vlm_chart
varz = ds.vars varz = ds.vars
@ -519,25 +537,19 @@ def graphics_update_cycle(
do_rt_update, do_rt_update,
should_tread, should_tread,
) = main_viz.incr_info(ds=ds) ) = main_viz.incr_info(ds=ds)
profiler('`.incr_info()`') profiler('`.incr_info()`')
# TODO: we should only run mxmn when we know # TODO: we should only run mxmn when we know
# an update is due via ``do_px_step`` above. # an update is due via ``do_px_step`` above.
(
mx_in_view,
mn_in_view,
mx_vlm_in_view,
) = ds.maxmin()
mx = mx_in_view + tick_margin
mn = mn_in_view - tick_margin
profiler('`ds.maxmin()` call')
# TODO: eventually we want to separate out the dark vlm and show # TODO: eventually we want to separate out the dark vlm and show
# them as an additional graphic. # them as an additional graphic.
clear_types = _tick_groups['clears'] clear_types = _tick_groups['clears']
mx = varz['last_mx']
mn = varz['last_mn']
mx_vlm_in_view = varz['last_mx_vlm']
# update ohlc sampled price bars # update ohlc sampled price bars
if ( if (
# do_rt_update # do_rt_update
@ -545,9 +557,24 @@ def graphics_update_cycle(
(liv and do_px_step) (liv and do_px_step)
or trigger_all or trigger_all
): ):
main_viz.update_graphics(array_key=fqsn) i_read_range, _ = main_viz.update_graphics()
profiler('`Viz.update_graphics()` call') profiler('`Viz.update_graphics()` call')
(
mx_in_view,
mn_in_view,
mx_vlm_in_view,
) = multi_maxmin(
i_read_range,
main_viz,
ds.vlm_viz,
profiler,
)
mx = mx_in_view + tick_margin
mn = mn_in_view - tick_margin
profiler('{fqsdn} `multi_maxmin()` call')
# don't real-time "shift" the curve to the # don't real-time "shift" the curve to the
# left unless we get one of the following: # left unless we get one of the following:
if ( if (
@ -717,15 +744,21 @@ def graphics_update_cycle(
is_1m=True, is_1m=True,
) )
profiler('hist `Viz.incr_info()`') profiler('hist `Viz.incr_info()`')
if (
hist_liv # TODO: track local liv maxmin without doing a recompute all the
and not hist_chart._static_yrange == 'axis' # time..plut, just generally the user is more likely to be
): # zoomed out enough on the slow chart that this is never an
hist_viz.plot.vb._set_yrange( # issue (the last datum going out of y-range).
viz=hist_viz, # hist_chart = ds.hist_chart
# yrange=yr, # this is the rt range, not hist.. XD # if (
) # hist_liv
profiler('hist vb y-autorange') # and not hist_chart._static_yrange == 'axis'
# ):
# hist_viz.plot.vb._set_yrange(
# viz=hist_viz,
# # yrange=yr, # this is the rt range, not hist.. XD
# )
# profiler('hist vb y-autorange')
# XXX: update this every draw cycle to ensure y-axis auto-ranging # XXX: update this every draw cycle to ensure y-axis auto-ranging
# only adjusts when the in-view data co-domain actually expands or # only adjusts when the in-view data co-domain actually expands or
@ -816,10 +849,10 @@ def graphics_update_cycle(
if ( if (
mx_vlm_in_view != varz['last_mx_vlm'] mx_vlm_in_view != varz['last_mx_vlm']
): ):
vlm_yr = (0, mx_vlm_in_view * 1.375)
vlm_chart.view._set_yrange(yrange=vlm_yr)
profiler('`vlm_chart.view._set_yrange()`')
varz['last_mx_vlm'] = mx_vlm_in_view varz['last_mx_vlm'] = mx_vlm_in_view
# vlm_yr = (0, mx_vlm_in_view * 1.375)
# vlm_chart.view._set_yrange(yrange=vlm_yr)
# profiler('`vlm_chart.view._set_yrange()`')
# update all downstream FSPs # update all downstream FSPs
for curve_name, viz in vlm_vizs.items(): for curve_name, viz in vlm_vizs.items():
@ -845,13 +878,12 @@ def graphics_update_cycle(
# is this even doing anything? # is this even doing anything?
# (pretty sure it's the real-time # (pretty sure it's the real-time
# resizing from last quote?) # resizing from last quote?)
fvb = viz.plot.vb
# XXX: without this we get completely # XXX: without this we get completely
# mangled/empty vlm display subchart.. # mangled/empty vlm display subchart..
fvb._set_yrange( # fvb = viz.plot.vb
viz=viz, # fvb._set_yrange(
) # viz=viz,
# )
profiler(f'vlm `Viz[{viz.name}].plot.vb._set_yrange()`') profiler(f'vlm `Viz[{viz.name}].plot.vb._set_yrange()`')
# even if we're downsampled bigly # even if we're downsampled bigly
@ -1195,9 +1227,8 @@ async def display_symbol_data(
# ensure the last datum graphic is generated # ensure the last datum graphic is generated
# for zoom-interaction purposes. # for zoom-interaction purposes.
hist_chart.get_viz(fqsn).draw_last( hist_viz = hist_chart.get_viz(fqsn)
array_key=fqsn, hist_viz.draw_last(array_key=fqsn)
)
pis.setdefault(fqsn, [None, None])[1] = hist_chart.plotItem pis.setdefault(fqsn, [None, None])[1] = hist_chart.plotItem
# don't show when not focussed # don't show when not focussed
@ -1211,6 +1242,7 @@ async def display_symbol_data(
# to avoid internal pane creation. # to avoid internal pane creation.
sidepane=pp_pane, sidepane=pp_pane,
) )
rt_viz = rt_chart.get_viz(fqsn)
pis.setdefault(fqsn, [None, None])[0] = rt_chart.plotItem pis.setdefault(fqsn, [None, None])[0] = rt_chart.plotItem
# for pause/resume on mouse interaction # for pause/resume on mouse interaction
@ -1227,7 +1259,7 @@ async def display_symbol_data(
and has_vlm(ohlcv) and has_vlm(ohlcv)
and vlm_chart is None and vlm_chart is None
): ):
vlm_charts[fqsn] = await ln.start( vlm_chart = vlm_charts[fqsn] = await ln.start(
open_vlm_displays, open_vlm_displays,
rt_linked, rt_linked,
flume, flume,
@ -1283,7 +1315,7 @@ async def display_symbol_data(
# are none? # are none?
hist_pi.hideAxis('left') hist_pi.hideAxis('left')
viz = hist_chart.draw_curve( hist_viz = hist_chart.draw_curve(
fqsn, fqsn,
hist_ohlcv, hist_ohlcv,
flume, flume,
@ -1298,7 +1330,7 @@ async def display_symbol_data(
# ensure the last datum graphic is generated # ensure the last datum graphic is generated
# for zoom-interaction purposes. # for zoom-interaction purposes.
viz.draw_last(array_key=fqsn) hist_viz.draw_last(array_key=fqsn)
hist_pi.vb.maxmin = partial( hist_pi.vb.maxmin = partial(
hist_chart.maxmin, hist_chart.maxmin,
@ -1308,8 +1340,8 @@ async def display_symbol_data(
# specially store ref to shm for lookup in display loop # specially store ref to shm for lookup in display loop
# since only a placeholder of `None` is entered in # since only a placeholder of `None` is entered in
# ``.draw_curve()``. # ``.draw_curve()``.
viz = hist_chart._vizs[fqsn] hist_viz = hist_chart._vizs[fqsn]
assert viz.plot is hist_pi assert hist_viz.plot is hist_pi
pis.setdefault(fqsn, [None, None])[1] = hist_pi pis.setdefault(fqsn, [None, None])[1] = hist_pi
rt_pi = rt_chart.overlay_plotitem( rt_pi = rt_chart.overlay_plotitem(
@ -1320,7 +1352,7 @@ async def display_symbol_data(
rt_pi.hideAxis('left') rt_pi.hideAxis('left')
rt_pi.hideAxis('bottom') rt_pi.hideAxis('bottom')
viz = rt_chart.draw_curve( rt_viz = rt_chart.draw_curve(
fqsn, fqsn,
ohlcv, ohlcv,
flume, flume,
@ -1341,8 +1373,8 @@ async def display_symbol_data(
# specially store ref to shm for lookup in display loop # specially store ref to shm for lookup in display loop
# since only a placeholder of `None` is entered in # since only a placeholder of `None` is entered in
# ``.draw_curve()``. # ``.draw_curve()``.
viz = rt_chart._vizs[fqsn] rt_viz = rt_chart._vizs[fqsn]
assert viz.plot is rt_pi assert rt_viz.plot is rt_pi
pis.setdefault(fqsn, [None, None])[0] = rt_pi pis.setdefault(fqsn, [None, None])[0] = rt_pi
rt_chart.setFocus() rt_chart.setFocus()
@ -1410,17 +1442,16 @@ async def display_symbol_data(
rt_linked.mode = mode rt_linked.mode = mode
viz = rt_chart.get_viz(order_ctl_symbol) rt_viz = rt_chart.get_viz(order_ctl_symbol)
viz.plot.setFocus() rt_viz.plot.setFocus()
# default view adjuments and sidepane alignment # default view adjuments and sidepane alignment
# as final default UX touch. # as final default UX touch.
rt_chart.default_view() rt_chart.default_view()
rt_chart.view.enable_auto_yrange()
await trio.sleep(0) await trio.sleep(0)
hist_chart.default_view() hist_chart.default_view()
hist_chart.view.enable_auto_yrange() hist_viz = hist_chart.get_viz(fqsn)
await trio.sleep(0) await trio.sleep(0)
godwidget.resize_all() godwidget.resize_all()