From 5b4622fb39829ed95f1f5d82d587190f3d341548 Mon Sep 17 00:00:00 2001 From: goodboy Date: Tue, 15 Sep 2026 12:49:42 -0400 Subject: [PATCH] Skip unchanged realtime FSP redraws Cache destination bounds and timestamp/value bytes on `Viz`. Skip identical quote-driven refreshes, including stable NaNs, while changed samples and forced history repairs still redraw. Add a regression covering each invalidation path. (this commit msg was generated in some part by `codex` using `gpt-6` (`openai`)) --- piker/ui/_dataviz.py | 1 + piker/ui/_fsp.py | 26 +++++++++++++++++++ tests/test_fsp_sync.py | 57 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/piker/ui/_dataviz.py b/piker/ui/_dataviz.py index 823a8d25..39ee2df9 100644 --- a/piker/ui/_dataviz.py +++ b/piker/ui/_dataviz.py @@ -319,6 +319,7 @@ class Viz(Struct): _in_ds: bool = False _index_step: float | None = None _time_step: float | None = None + _last_fsp_update_sig: tuple[int, int, bytes]|None = None # map from uppx -> (downsampled data, incremental graphics) _src_r: Renderer | None = None diff --git a/piker/ui/_fsp.py b/piker/ui/_fsp.py index 939e0da0..4c9e6831 100644 --- a/piker/ui/_fsp.py +++ b/piker/ui/_fsp.py @@ -101,10 +101,36 @@ def update_fsp_chart( ) return + # `update_fsp_chart()` runs for every source quote, while an FSP can + # publish more slowly. `Viz._last_fsp_update_sig` combines the + # destination's absolute half-open `ShmArray` bounds with bitwise + # `last_row['time']` and `last_row[array_key]` scalars. Comparing + # bytes keeps repeated NaNs stable; scalar equality treats every NaN + # as changed. A changed `ShmArray._first.value` catches history + # prepends, a changed `ShmArray._last.value` catches sample appends, + # and changed row bytes catch mutation of the current sample. + # `force_redraw` bypasses this gate because an equal-bound gap repair + # can rewrite earlier rows without changing the current row. This is + # only a repaint key, not synchronization proof; a mixed SHM read + # merely causes one extra later redraw. + update_sig: tuple[int, int, bytes] = ( + shm._first.value, + shm._last.value, + np.asarray(last_row['time']).tobytes() + + np.asarray(last_row[array_key]).tobytes(), + ) + if ( + not force_redraw + and + viz._last_fsp_update_sig == update_sig + ): + return + # update graphics # NOTE: this does a length check internally which allows it # staying above the last row check below.. viz.update_graphics(force_redraw=force_redraw) + viz._last_fsp_update_sig = update_sig # XXX: re: ``array_key``: fsp func names must be unique meaning we # can't have duplicates of the underlying data even if multiple diff --git a/tests/test_fsp_sync.py b/tests/test_fsp_sync.py index 078f468b..894b1380 100644 --- a/tests/test_fsp_sync.py +++ b/tests/test_fsp_sync.py @@ -33,7 +33,10 @@ from piker.data._sharedmem import NDTokenMsg from piker.data.ticktools import FeedQuote from piker.fsp._api import Fsp from piker.ui._chart import LinkedSplits -from piker.ui._fsp import update_fsp_vizs +from piker.ui._fsp import ( + update_fsp_chart, + update_fsp_vizs, +) from tractor.ipc._shm import ( NDToken, ShmArray, @@ -502,6 +505,58 @@ class Viz: self.force_redraws.append(force_redraw) +def test_fsp_chart_skips_unchanged_realtime_state() -> None: + ''' + Avoid repainting an unchanged current FSP datum on every quote. + + The display loop calls `update_fsp_chart()` for every realtime + quote, even when a given FSP destination has not advanced or + changed its current value. That rebuilt and painted the same last + step rectangle repeatedly. Exercise identical state, an in-place + value update, a new sample with the same value, and a forced + history repair. The update counts prove unchanged requests are + skipped while each legitimate invalidation still redraws. A final + stable-NaN pair proves scalar NaN inequality can not defeat the + guard. + + ''' + shm = FspShm() + viz = Viz('flow', shm) + + update_fsp_chart(viz, 'flow', 'flow') + update_fsp_chart(viz, 'flow', 'flow') + assert viz.updates == 1 + + shm._array['flow'][shm._last.value - 1] = 42 + update_fsp_chart(viz, 'flow', 'flow') + assert viz.updates == 2 + + shm._array['flow'][shm._last.value] = 42 + shm._last.value += 1 + update_fsp_chart(viz, 'flow', 'flow') + assert viz.updates == 3 + + update_fsp_chart( + viz, + 'flow', + 'flow', + force_redraw=True, + ) + assert viz.updates == 4 + + shm._array['flow'][shm._last.value - 1] = np.nan + update_fsp_chart(viz, 'flow', 'flow') + update_fsp_chart(viz, 'flow', 'flow') + assert viz.updates == 5 + assert viz.force_redraws == [ + False, + False, + False, + True, + False, + ] + + def test_fsp_history_update_redraws_only_derived_vizs() -> None: ''' Keep a source-history prepend from refreshing the whole chart.