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.
multichartz
Tyler Goodlet 2023-01-18 16:19:08 -05:00
parent e44b485bcb
commit 47ffe60047
1 changed files with 103 additions and 72 deletions

View File

@ -25,7 +25,10 @@ from functools import partial
import itertools
from math import floor
import time
from typing import Optional, Any, Callable
from typing import (
Optional,
Any,
)
import tractor
import trio
@ -85,10 +88,11 @@ log = get_logger(__name__)
# approach, likely with ``numba``:
# https://arxiv.org/abs/cs/0610046
# https://github.com/lemire/pythonmaxmin
def chart_maxmin(
def multi_maxmin(
i_read_range: tuple[int, int] | None,
fast_viz: Viz,
fqsn: str,
vlm_viz: Viz | None = None,
profiler: Profiler = None,
) -> tuple[
@ -102,26 +106,32 @@ def chart_maxmin(
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:
# log.warning(f'No yrange provided for {name}!?')
return (0, 0, 0)
(
ixrng,
read_slc,
mxmn,
yrange,
) = 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
# np.max/min trigger and especially on the vlm_chart
# vizs which aren't shown.. like vlm?
mx_vlm_in_view = 0
if vlm_viz:
out = vlm_viz.maxmin()
out = vlm_viz.maxmin(
i_read_range=i_read_range,
)
if out:
(
ixrng,
@ -130,9 +140,16 @@ def chart_maxmin(
) = out
mx_vlm_in_view = mxmn[1]
if profiler:
profiler('vlm_viz.maxmin({read_slc})')
return (
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
)
@ -146,7 +163,6 @@ class DisplayState(Struct):
godwidget: GodWidget
quotes: dict[str, Any]
maxmin: Callable
flume: Flume
# high level chart handles and underlying ``Viz``
@ -160,6 +176,8 @@ class DisplayState(Struct):
last_price_sticky: YAxisLabel
hist_last_price_sticky: YAxisLabel
vlm_viz: Viz
# misc state tracking
vars: dict[str, Any] = {
'tick_margin': 0,
@ -232,21 +250,22 @@ async def increment_history_view(
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
if should_tread:
# ensure path graphics append is shown on treads since
# 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)
profiler('hist tread view')
if (
do_px_step
and liv
):
hist_viz.plot.vb._set_yrange(viz=hist_viz)
profiler.finish()
async def graphics_update_loop(
@ -325,17 +344,15 @@ async def graphics_update_loop(
vlm_chart = vlm_charts[fqsn]
vlm_viz = vlm_chart._vizs.get('volume') if vlm_chart else None
maxmin = partial(
chart_maxmin,
fast_viz,
fqsn,
vlm_viz,
)
(
last_mx,
last_mn,
last_mx_vlm,
) = maxmin()
) = multi_maxmin(
None,
fast_viz,
vlm_viz,
)
last, volume = ohlcv.array[-1][['close', 'volume']]
@ -366,7 +383,7 @@ async def graphics_update_loop(
'fqsn': fqsn,
'godwidget': godwidget,
'quotes': {},
'maxmin': maxmin,
# 'maxmin': maxmin,
'flume': flume,
@ -378,6 +395,8 @@ async def graphics_update_loop(
'hist_viz': hist_viz,
'hist_last_price_sticky': hist_last_price_sticky,
'vlm_viz': vlm_viz,
'l1': l1,
'vars': {
@ -485,11 +504,10 @@ def graphics_update_cycle(
# or at least a little `mypyc` B)
# - pass more direct refs as input to avoid so many attr accesses?
# - 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
chart = ds.chart
hist_chart = ds.hist_chart
vlm_chart = ds.vlm_chart
varz = ds.vars
@ -513,25 +531,19 @@ def graphics_update_cycle(
do_rt_update,
should_tread,
) = main_viz.incr_info(ds=ds)
profiler('`.incr_info()`')
# TODO: we should only run mxmn when we know
# 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
# them as an additional graphic.
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
if (
# do_rt_update
@ -539,9 +551,24 @@ def graphics_update_cycle(
(liv and do_px_step)
or trigger_all
):
main_viz.update_graphics(array_key=fqsn)
i_read_range, _ = main_viz.update_graphics()
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
# left unless we get one of the following:
if (
@ -711,15 +738,21 @@ def graphics_update_cycle(
is_1m=True,
)
profiler('hist `Viz.incr_info()`')
if (
hist_liv
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')
# TODO: track local liv maxmin without doing a recompute all the
# time..plut, just generally the user is more likely to be
# zoomed out enough on the slow chart that this is never an
# issue (the last datum going out of y-range).
# hist_chart = ds.hist_chart
# if (
# hist_liv
# 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
# only adjusts when the in-view data co-domain actually expands or
@ -810,10 +843,10 @@ def graphics_update_cycle(
if (
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
# 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
for curve_name, viz in vlm_vizs.items():
@ -839,13 +872,12 @@ def graphics_update_cycle(
# is this even doing anything?
# (pretty sure it's the real-time
# resizing from last quote?)
fvb = viz.plot.vb
# XXX: without this we get completely
# mangled/empty vlm display subchart..
fvb._set_yrange(
viz=viz,
)
# fvb = viz.plot.vb
# fvb._set_yrange(
# viz=viz,
# )
profiler(f'vlm `Viz[{viz.name}].plot.vb._set_yrange()`')
# even if we're downsampled bigly
@ -1189,9 +1221,8 @@ async def display_symbol_data(
# ensure the last datum graphic is generated
# for zoom-interaction purposes.
hist_chart.get_viz(fqsn).draw_last(
array_key=fqsn,
)
hist_viz = hist_chart.get_viz(fqsn)
hist_viz.draw_last(array_key=fqsn)
pis.setdefault(fqsn, [None, None])[1] = hist_chart.plotItem
# don't show when not focussed
@ -1205,6 +1236,7 @@ async def display_symbol_data(
# to avoid internal pane creation.
sidepane=pp_pane,
)
rt_viz = rt_chart.get_viz(fqsn)
pis.setdefault(fqsn, [None, None])[0] = rt_chart.plotItem
# for pause/resume on mouse interaction
@ -1221,7 +1253,7 @@ async def display_symbol_data(
and has_vlm(ohlcv)
and vlm_chart is None
):
vlm_charts[fqsn] = await ln.start(
vlm_chart = vlm_charts[fqsn] = await ln.start(
open_vlm_displays,
rt_linked,
flume,
@ -1277,7 +1309,7 @@ async def display_symbol_data(
# are none?
hist_pi.hideAxis('left')
viz = hist_chart.draw_curve(
hist_viz = hist_chart.draw_curve(
fqsn,
hist_ohlcv,
flume,
@ -1292,7 +1324,7 @@ async def display_symbol_data(
# ensure the last datum graphic is generated
# for zoom-interaction purposes.
viz.draw_last(array_key=fqsn)
hist_viz.draw_last(array_key=fqsn)
hist_pi.vb.maxmin = partial(
hist_chart.maxmin,
@ -1302,8 +1334,8 @@ async def display_symbol_data(
# specially store ref to shm for lookup in display loop
# since only a placeholder of `None` is entered in
# ``.draw_curve()``.
viz = hist_chart._vizs[fqsn]
assert viz.plot is hist_pi
hist_viz = hist_chart._vizs[fqsn]
assert hist_viz.plot is hist_pi
pis.setdefault(fqsn, [None, None])[1] = hist_pi
rt_pi = rt_chart.overlay_plotitem(
@ -1314,7 +1346,7 @@ async def display_symbol_data(
rt_pi.hideAxis('left')
rt_pi.hideAxis('bottom')
viz = rt_chart.draw_curve(
rt_viz = rt_chart.draw_curve(
fqsn,
ohlcv,
flume,
@ -1335,8 +1367,8 @@ async def display_symbol_data(
# specially store ref to shm for lookup in display loop
# since only a placeholder of `None` is entered in
# ``.draw_curve()``.
viz = rt_chart._vizs[fqsn]
assert viz.plot is rt_pi
rt_viz = rt_chart._vizs[fqsn]
assert rt_viz.plot is rt_pi
pis.setdefault(fqsn, [None, None])[0] = rt_pi
rt_chart.setFocus()
@ -1404,17 +1436,16 @@ async def display_symbol_data(
rt_linked.mode = mode
viz = rt_chart.get_viz(order_ctl_symbol)
viz.plot.setFocus()
rt_viz = rt_chart.get_viz(order_ctl_symbol)
rt_viz.plot.setFocus()
# default view adjuments and sidepane alignment
# as final default UX touch.
rt_chart.default_view()
rt_chart.view.enable_auto_yrange()
await trio.sleep(0)
hist_chart.default_view()
hist_chart.view.enable_auto_yrange()
hist_viz = hist_chart.get_viz(fqsn)
await trio.sleep(0)
godwidget.resize_all()