Use binary search (bisection) to sort table rows

This is an optimization to improve performance when the UI is fed real
time data. Instead of resorting all rows on every quote update, only
re-render when the sort key appears in the quote data, and further, only
resort rows which are changed using bisection based widget insertion to
avoid having `kivy` re-add widgets (and thus re-render graphics) more
often than absolutely necessary.
kivy_mainline_and_py3.8
Tyler Goodlet 2018-12-15 16:28:28 -05:00
parent 721e3803b2
commit 07eb8ae5e0
1 changed files with 90 additions and 62 deletions

View File

@ -8,6 +8,7 @@ Launch with ``piker monitor <watchlist name>``.
from itertools import chain
from types import ModuleType, AsyncGeneratorType
from typing import List, Callable, Dict
from bisect import bisect
import trio
import tractor
@ -19,6 +20,7 @@ from kivy.lang import Builder
from kivy import utils
from kivy.app import async_runTouchApp
from kivy.core.window import Window
from kivy.properties import BooleanProperty
from ..log import get_logger
from .pager import PagerView
@ -69,8 +71,10 @@ _kv = (f'''
size: self.texture_size
# color: {colorcode('gray')}
# font_color: {colorcode('gray')}
font_name: 'Roboto-Regular'
background_color: [0]*4 # by default transparent; use row color
# font_name: 'Hack-Regular'
# by default transparent; use row color
# if `highlight` is set use i3
background_color: {_i3_rgba} if self.click_toggle else [0]*4
# background_color: {_cell_rgba}
# spacing: 0, 0
# padding: [0]*4
@ -148,6 +152,8 @@ class Cell(Button):
``key`` is the column name index value.
"""
click_toggle = BooleanProperty(False)
def __init__(self, key=None, is_header=False, **kwargs):
super(Cell, self).__init__(**kwargs)
self.key = key
@ -177,7 +183,7 @@ class HeaderCell(Cell):
# mark this cell as the last selected
table.last_clicked_col_cell = self
# sort and render the rows immediately
self.row.table.render_rows(table.quote_cache)
self.row.table.render_rows(table.symbols2rows.values())
# TODO: make this some kind of small geometry instead
# (maybe like how trading view does it).
@ -310,6 +316,9 @@ class Row(GridLayout, HoverBehavior):
def get_cell(self, key):
return self._cell_widgets.get(key)
def get_field(self, key):
return self._last_record[key]
def _append_cell(self, text, key, header=False):
if not len(self._cell_widgets) < self.cols:
raise ValueError(f"Can not append more then {self.cols} cells")
@ -378,12 +387,10 @@ class TickerTable(GridLayout):
super(TickerTable, self).__init__(**kwargs)
self.symbols2rows = {}
self.sort_key = sort_key
self.quote_cache = {}
self.row_filter = lambda item: item
# for tracking last clicked column header cell
self.last_clicked_col_cell = None
self._last_row_toggle = 0
self._rendered = set()
self._symbols2index = {}
self._sorted = []
def append_row(self, key, row):
"""Append a `Row` of `Cell` objects to this table.
@ -391,36 +398,51 @@ class TickerTable(GridLayout):
# store ref to each row
self.symbols2rows[key] = row
self.add_widget(row)
self._sorted.append(row)
return row
def clear(self):
self.clear_widgets()
self._sorted.clear()
def render_rows(
self,
pairs: Dict[str, Row],
changed: set,
sort_key: str = None,
row_filter=None,
):
"""Sort and render all rows on the ticker grid from ``pairs``.
"""Sort and render all rows on the ticker grid from ``syms2rows``.
"""
self.clear_widgets()
self._rendered.clear()
sort_key = sort_key or self.sort_key
# TODO: intead of constantly re-rendering on every
# change do a binary search insert using ``bisect.insort()``
for row in filter(
row_filter or self.row_filter,
reversed(
sorted(
pairs.values(),
key=lambda row: row._last_record[sort_key]
)
)
):
widget = row.widget
if widget not in self._rendered:
self.add_widget(widget) # row append
self._rendered.add(widget)
key_row_pairs = list(sorted(
[(row.get_field(sort_key), row) for row in self._sorted],
key=lambda item: item[0],
))
if key_row_pairs:
sorted_keys, sorted_rows = zip(*key_row_pairs)
sorted_keys, sorted_rows = list(sorted_keys), list(sorted_rows)
else:
log.debug(f"Skipping adding widget {widget}")
sorted_keys, sorted_rows = [], []
# now remove and re-insert any rows that need to be shuffled
# due to new a new field change
for row in changed:
try:
old_index = sorted_rows.index(row)
except ValueError:
# row is not yet added so nothing to remove
pass
else:
del sorted_rows[old_index]
del sorted_keys[old_index]
self._sorted.remove(row)
self.remove_widget(row)
for row in changed:
key = row.get_field(sort_key)
index = bisect(sorted_keys, key)
sorted_keys.insert(index, key)
self._sorted.insert(index, row)
self.add_widget(row, index=index)
def ticker_search(self, patt):
"""Return sequence of matches when pattern ``patt`` is in a
@ -514,36 +536,41 @@ async def update_quotes(
# revert flash state momentarily
nursery.start_soon(revert_cells_color, unflash)
cache = {}
table.quote_cache = cache
# initial coloring
to_sort = set()
for sym, quote in first_quotes.items():
row = table.get_row(sym)
record, displayable = formatter(
quote, symbol_data=symbol_data)
row.update(record, displayable)
color_row(row, record, {})
cache[sym] = row
to_sort.add(row.widget)
table.render_rows(to_sort)
log.debug("Finished initializing update loop")
task_status.started()
# real-time cell update loop
async for quotes in agen: # new quotes data only
to_sort = set()
for symbol, quote in quotes.items():
record, displayable = formatter(
quote, symbol_data=symbol_data)
row = table.get_row(symbol)
cells = row.update(record, displayable)
color_row(row, record, cells)
cache[symbol] = row
table.render_rows(cache)
if table.sort_key in record:
to_sort.add(row.widget)
if to_sort:
table.render_rows(to_sort)
log.debug("Waiting on quotes")
log.warn("Data feed connection dropped")
# XXX: if we're cancelled this should never get called
nursery.cancel_scope.cancel()
# nursery.cancel_scope.cancel()
async def _async_main(
@ -638,6 +665,7 @@ async def _async_main(
# set up a pager view for large ticker lists
table.bind(minimum_height=table.setter('height'))
try:
async with trio.open_nursery() as nursery:
pager = PagerView(
container=box,
@ -663,11 +691,11 @@ async def _async_main(
quotes
)
try:
# Trio-kivy entry point.
await async_runTouchApp(widgets['root']) # run kivy
finally:
# cancel aysnc gen call
await quote_gen.aclose()
# cancel GUI update task
nursery.cancel_scope.cancel()
finally:
with trio.open_cancel_scope(shield=True):
# cancel aysnc gen call
await quote_gen.aclose()