Dashboard: the idle second was re-walking the whole window
The screen repaints once a second to keep its rates, ages and clock honest. Both of its expensive stages are pure functions of data that mostly does not move between those frames: the window digest re-walked up to four thousand observations to reach the numbers it already had, and the braille activity chart re-rasterized an identical picture. The window changes in exactly two places — an arriving observation, an expiring one — so both bump a revision the digest compares against. The chart memoizes on the numbers it draws from. An idle repaint: 8.4 -> 3.8 ms. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
714bb15fe6
commit
42743bded2
2 files changed
+114
-12
No files matched your search
@@ -183,12 +183,21 @@ class DashboardScreen(Screen):
|
||||
#: dynamic rows): ``stats`` from get_stats, ``battery`` from get_battery.
|
||||
self.stats: dict = {}
|
||||
self.battery: dict = {}
|
||||
# The one-pass window aggregates (see _digest_window), refreshed per repaint.
|
||||
# The one-pass window aggregates (see _digest_window), refreshed whenever the
|
||||
# window itself moves — which is what ``_window_rev`` counts and ``_digest_rev``
|
||||
# remembers. The screen repaints once a second to keep its clocks honest, and on a
|
||||
# quiet mesh every one of those frames would otherwise re-walk the same 4000
|
||||
# observations to reach the identical numbers.
|
||||
self._window_rev = 0
|
||||
self._digest_rev = -1
|
||||
self._win_nodes: set[str] = set()
|
||||
self._win_repeaters: set[str] = set()
|
||||
self._win_counts: Counter = Counter()
|
||||
self._win_snrs: list[float] = []
|
||||
self._win_rssis: list[float] = []
|
||||
#: The last activity chart and the inputs it was drawn from — see
|
||||
#: :meth:`_activity_section`.
|
||||
self._chart_memo: Optional[tuple[tuple, list[RenderableType]]] = None
|
||||
|
||||
# --- live window -----------------------------------------------------------------
|
||||
|
||||
@@ -197,6 +206,7 @@ class DashboardScreen(Screen):
|
||||
obs = event.observation
|
||||
if obs is not None:
|
||||
self._window.append(obs)
|
||||
self._window_rev += 1
|
||||
self._prune()
|
||||
self._session.invalidate()
|
||||
|
||||
@@ -205,6 +215,7 @@ class DashboardScreen(Screen):
|
||||
cutoff = utcnow() - OBSERVATION_WINDOW
|
||||
while self._window and self._window[0].observed_at < cutoff:
|
||||
self._window.popleft()
|
||||
self._window_rev += 1
|
||||
|
||||
# --- input -----------------------------------------------------------------------
|
||||
|
||||
@@ -249,7 +260,17 @@ class DashboardScreen(Screen):
|
||||
different slice of the same window; scanning the (up to 4000-entry) deque once
|
||||
per consumer added up to five full passes on every repaint of a busy mesh.
|
||||
One pass here fills them all; the section builders read the results.
|
||||
|
||||
And that pass runs only when the window has actually moved. The screen repaints
|
||||
every second so its rates, ages and clock stay honest, but the aggregates are a
|
||||
pure function of the deque — on a mesh quiet for a beat, every one of those frames
|
||||
was re-walking four thousand observations to arrive at the numbers it already had.
|
||||
The window changes in exactly two places (an arriving observation, an expiring one),
|
||||
both of which bump the revision this compares against.
|
||||
"""
|
||||
if self._digest_rev == self._window_rev:
|
||||
return
|
||||
self._digest_rev = self._window_rev
|
||||
nodes: set[str] = set()
|
||||
repeaters: set[str] = set()
|
||||
counts: Counter = Counter()
|
||||
@@ -301,19 +322,29 @@ class DashboardScreen(Screen):
|
||||
# what this session heard itself pulses green.
|
||||
flags = (tuple(self._activity_flags()) + (True,) * minutes)[:minutes]
|
||||
styles = ["ok" if live else "muted" for live in reversed(flags)]
|
||||
chart_rows = timeline_rows(
|
||||
list(reversed(shown)), rows=_CHART_ROWS, column_styles=styles
|
||||
)
|
||||
|
||||
def caption_at(frac: float) -> str:
|
||||
if frac >= 1.0:
|
||||
return "now"
|
||||
return "−" + _span_label(round(minutes * (1 - frac)))
|
||||
# The chart is a pure function of these numbers, and on the idle second-tick they
|
||||
# are the same numbers: the histogram only moves when a packet lands or the minute
|
||||
# rolls over. Rasterizing the braille rows and their axis chrome is the priciest
|
||||
# part of this screen's paint, so it is done once per distinct picture rather than
|
||||
# once per repaint. One slot — the chart that just changed is the one to keep.
|
||||
key = (chars, label_w, peak, minutes, tuple(shown), tuple(styles))
|
||||
if self._chart_memo is not None and self._chart_memo[0] == key:
|
||||
chart = self._chart_memo[1]
|
||||
else:
|
||||
|
||||
out: list[RenderableType] = [
|
||||
heading,
|
||||
*axis_chart(chart_rows, peak, chars, caption_at, label_w=label_w),
|
||||
]
|
||||
def caption_at(frac: float) -> str:
|
||||
if frac >= 1.0:
|
||||
return "now"
|
||||
return "−" + _span_label(round(minutes * (1 - frac)))
|
||||
|
||||
chart_rows = timeline_rows(
|
||||
list(reversed(shown)), rows=_CHART_ROWS, column_styles=styles
|
||||
)
|
||||
chart = list(axis_chart(chart_rows, peak, chars, caption_at, label_w=label_w))
|
||||
self._chart_memo = (key, chart)
|
||||
|
||||
out: list[RenderableType] = [heading, *chart]
|
||||
out.append(Text())
|
||||
out.append(self._pulse_grid(shown, width))
|
||||
return out
|
||||
|
||||
@@ -328,3 +328,74 @@ def test_rhythm_activity_buckets_by_minute_of_day(tmp_path: Path) -> None:
|
||||
assert slots[17 * 60 + 55] == 2 # both 17:55 stamps share one minute slot
|
||||
assert sum(slots) == 4
|
||||
repo.close()
|
||||
|
||||
|
||||
# --- what the once-a-second repaint is allowed to redo --------------------------------
|
||||
|
||||
|
||||
def test_the_window_digest_only_runs_when_the_window_moved() -> None:
|
||||
"""The screen repaints every second; the aggregates are a pure function of the deque.
|
||||
|
||||
On a mesh quiet for a beat, every one of those frames re-walked up to four thousand
|
||||
observations to arrive at the numbers it already had. The window changes in exactly two
|
||||
places — an arriving observation and an expiring one — and only those may cost a pass.
|
||||
"""
|
||||
screen = _screen(window=[_obs(node=f"n{i:03d}", age_s=i) for i in range(200)])
|
||||
passes = [0]
|
||||
inner = type(screen)._digest_window
|
||||
|
||||
def counted(self) -> None:
|
||||
before = self._digest_rev
|
||||
inner(self)
|
||||
if before != self._digest_rev:
|
||||
passes[0] += 1
|
||||
|
||||
screen._digest_window = counted.__get__(screen)
|
||||
|
||||
screen.render_body(53)
|
||||
assert passes[0] == 1
|
||||
screen.render_body(53)
|
||||
screen.render_body(53)
|
||||
assert passes[0] == 1, "an idle repaint must not re-walk the window"
|
||||
|
||||
screen.on_event(MeshEvent.observation_event(_obs(node="new1")))
|
||||
screen.render_body(53)
|
||||
assert passes[0] == 2, "an arriving observation must refresh the aggregates"
|
||||
|
||||
|
||||
def test_a_new_observation_still_reaches_the_numbers_on_the_next_paint() -> None:
|
||||
"""The memo must not freeze the screen: a heard node has to show up in the tallies."""
|
||||
screen = _screen(window=[_obs(node="a1b2")])
|
||||
screen.render_body(53)
|
||||
assert screen._win_nodes == {"a1b2"}
|
||||
|
||||
screen.on_event(MeshEvent.observation_event(_obs(node="3d63")))
|
||||
screen.render_body(53)
|
||||
assert screen._win_nodes == {"a1b2", "3d63"}
|
||||
|
||||
|
||||
def test_the_activity_chart_is_redrawn_only_when_its_picture_changes() -> None:
|
||||
"""Rasterizing the braille rows is the priciest part of this paint — do it once."""
|
||||
histogram = [0] * ACTIVITY_BUCKETS
|
||||
histogram[0] = 5
|
||||
screen = _screen(histogram=histogram)
|
||||
|
||||
screen.render_body(53)
|
||||
first = screen._chart_memo
|
||||
assert first is not None
|
||||
screen.render_body(53)
|
||||
assert screen._chart_memo is first, "an identical histogram must reuse the chart"
|
||||
|
||||
histogram[0] = 40 # a packet lands: the newest column grows
|
||||
screen.render_body(53)
|
||||
assert screen._chart_memo is not first
|
||||
assert screen._chart_memo[0] != first[0]
|
||||
|
||||
|
||||
def test_the_chart_is_redrawn_when_the_terminal_width_changes() -> None:
|
||||
"""The chart is sized to the terminal, so the memo key has to carry the width."""
|
||||
screen = _screen(histogram=[3] * ACTIVITY_BUCKETS)
|
||||
screen.render_body(53)
|
||||
narrow = screen._chart_memo
|
||||
screen.render_body(100)
|
||||
assert screen._chart_memo is not narrow
|
||||
Reference in new issue
Block a user