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`))
wkt/fsp_backfill_sync
Gud Boi 2026-09-15 12:49:42 -04:00
parent 20406210b3
commit 5b4622fb39
3 changed files with 83 additions and 1 deletions

View File

@ -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

View File

@ -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

View File

@ -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.