ML-167: presto performance improvements

This commit is contained in:
Claudio Ortolina
2026-05-06 07:24:27 +01:00
parent 154c3d9078
commit c550cb8c35
2 changed files with 151 additions and 30 deletions
@@ -0,0 +1,63 @@
---
id: ML-167
title: Improve Presto day-view scroll performance
status: Done
assignee: []
created_date: "2026-05-06 06:19"
updated_date: "2026-05-06 06:21"
labels:
- presto
- performance
dependencies: []
modified_files:
- presto/records_on_the_day.py
priority: medium
ordinal: 12000
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Optimize the Presto MicroPython records-on-this-day client so day-view scrolling feels snappier without changing the visible behavior. The current hot path repeatedly measures row text and computes scroll extents while dragging, and thumbnail fetching can still occur during drawing. Keep the existing placeholder-while-dragging behavior because decoding covers during drag was too slow on device.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Record display text, metadata, row height, and total scroll extent are prepared once when records are loaded or retried
- [x] #2 Micro cover images are prefetched and cached before rendering the day view when records load
- [x] #3 Drag scrolling redraws are throttled so display updates do not saturate the device
- [x] #4 Cover placeholders remain in use during active drag scrolling and real covers repaint after release
- [x] #5 Thumbnail rendering does not call garbage collection once per row draw
- [x] #6 README remains accurate if user-visible behavior changes
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add a single record assignment/preparation path after each successful or failed day-record fetch.
2. Precompute display-safe title/artist strings, metadata text, row heights, thumbnail URL, and total content height before rendering the day view.
3. Preload only micro_cover_url thumbnails before the first draw to avoid eager large fallback downloads.
4. Keep placeholder rendering while dragging, remove per-row gc.collect from thumbnail drawing, and throttle drag redraws by time and pixel delta.
5. Verify with Python syntax compilation and leave README unchanged because behavior remains the same.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented set_day_records/2, prepare_records_for_display/0, cached per-record \_display_title/\_display_artists/\_display_meta/\_thumb_url/\_row_height, cached \_content_height for max scroll offset, micro-thumbnail preloading, drag redraw throttling via DRAG_REDRAW_MS/DRAG_REDRAW_PX, and removed gc.collect from per-row thumbnail rendering. Cover placeholders remain during active drag and covers repaint after release.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Optimized the Presto day-view scroll path by moving text measurement, row height computation, content height calculation, and micro thumbnail fetches out of active scrolling. Drag redraws are now throttled and still use lightweight placeholders, with covers repainted after release. Verified syntax with py_compile.
<!-- SECTION:FINAL_SUMMARY:END -->
+88 -30
View File
@@ -127,6 +127,8 @@ MONTH_NAMES = [
# Touch debounce (milliseconds) # Touch debounce (milliseconds)
DEBOUNCE_MS = 300 DEBOUNCE_MS = 300
DRAG_REDRAW_MS = 40
DRAG_REDRAW_PX = 8
# WiFi connection timeout (seconds) # WiFi connection timeout (seconds)
WIFI_TIMEOUT = 30 WIFI_TIMEOUT = 30
@@ -164,9 +166,11 @@ records_error = False # True if API call failed
scroll_offset = 0 # Pixel scroll position for day view scroll_offset = 0 # Pixel scroll position for day view
_visible_count = 1 # How many records fit on screen (set during draw) _visible_count = 1 # How many records fit on screen (set during draw)
_dragging = False # True while fast scroll redraws are active _dragging = False # True while fast scroll redraws are active
_content_height = 0 # Cached total record list height
# Touch debounce # Touch debounce
_last_touch = 0 _last_touch = 0
_last_drag_redraw = 0
# Display sleep state # Display sleep state
_last_activity = 0 _last_activity = 0
@@ -539,6 +543,21 @@ def fetch_records(year, month, day):
return [], True return [], True
def set_day_records(recs, had_error):
"""Set day-view records and prepare display caches."""
global records, records_error, scroll_offset, _content_height
records = recs
records_error = had_error
scroll_offset = 0
if had_error:
_content_height = 0
return
prepare_records_for_display()
def fetch_thumbnail(url): def fetch_thumbnail(url):
"""Fetch a JPEG thumbnail from the given URL. """Fetch a JPEG thumbnail from the given URL.
@@ -933,7 +952,7 @@ def _draw_record_list():
visible_count = 0 visible_count = 0
for rec in records: for rec in records:
row_h = _record_row_height(rec) row_h = rec.get("_row_height", THUMB_SIZE + 8)
row_bottom = cy + row_h row_bottom = cy + row_h
if row_bottom <= RECORD_START_Y: if row_bottom <= RECORD_START_Y:
@@ -960,28 +979,56 @@ def _draw_record_list():
def _max_scroll_offset(): def _max_scroll_offset():
"""Return the maximum pixel scroll offset for the current records.""" """Return the maximum pixel scroll offset for the current records."""
viewport_h = HEIGHT - RECORD_START_Y - 28 viewport_h = HEIGHT - RECORD_START_Y - 28
content_h = 0 return max(0, _content_height - viewport_h)
def prepare_records_for_display():
"""Cache display fields, row heights, and thumbnails for current records."""
global _content_height
display.set_font("bitmap8") display.set_font("bitmap8")
for rec in records: _content_height = 0
content_h += _record_row_height(rec)
return max(0, content_h - viewport_h) for rec in records:
_prepare_record_for_display(rec)
_content_height += rec.get("_row_height", THUMB_SIZE + 8)
preload_record_thumbnails()
gc.collect()
def _prepare_record_for_display(rec):
"""Prepare one record's display text and row height."""
title = display_text(rec.get("title", "Unknown Title"))
artists = rec.get("artists", [])
artist_str = display_text(", ".join(artists)) if artists else ""
meta_text = _record_meta_text(rec)
rec["_display_title"] = title
rec["_display_artists"] = artist_str
rec["_display_meta"] = meta_text
rec["_thumb_url"] = _record_thumbnail_url(rec)
rec["_row_height"] = _record_row_height(rec)
def preload_record_thumbnails():
"""Fetch thumbnail bytes before the first day-view draw."""
for rec in records:
if rec.get("micro_cover_url", ""):
_record_thumbnail_data(rec)
def _record_row_height(rec): def _record_row_height(rec):
"""Calculate a record row height without drawing it.""" """Calculate a record row height without drawing it."""
display.set_font("bitmap8")
min_h = THUMB_SIZE + 8 min_h = THUMB_SIZE + 8
title = display_text(rec.get("title", "Unknown Title")) title = rec.get("_display_title", display_text(rec.get("title", "Unknown Title")))
ty = _wrapped_line_count(title, TEXT_W, scale=1) * 12 ty = _wrapped_line_count(title, TEXT_W, scale=1) * 12
artists = rec.get("artists", []) artist_str = rec.get("_display_artists", "")
if artists: if artist_str:
artist_str = display_text(", ".join(artists))
ty += 2 + _wrapped_line_count(artist_str, TEXT_W, scale=1) * 12 ty += 2 + _wrapped_line_count(artist_str, TEXT_W, scale=1) * 12
if _record_meta_text(rec): if rec.get("_display_meta", _record_meta_text(rec)):
ty += 2 + 12 ty += 2 + 12
content_h = max(THUMB_SIZE, ty) + 10 content_h = max(THUMB_SIZE, ty) + 10
@@ -1016,11 +1063,7 @@ def _record_thumbnail_data(rec):
if cached is not None: if cached is not None:
return cached return cached
thumb_url = ( thumb_url = rec.get("_thumb_url", "") or _record_thumbnail_url(rec)
rec.get("micro_cover_url", "")
or rec.get("mini_cover_url", "")
or rec.get("thumb_url", "")
)
if not thumb_url: if not thumb_url:
return None return None
@@ -1033,6 +1076,15 @@ def _record_thumbnail_data(rec):
return data return data
def _record_thumbnail_url(rec):
"""Return the preferred cover URL for a record row."""
return (
rec.get("micro_cover_url", "")
or rec.get("mini_cover_url", "")
or rec.get("thumb_url", "")
)
def _draw_record_row(rec, y): def _draw_record_row(rec, y):
"""Draw a single record row. Returns the Y position after the row """Draw a single record row. Returns the Y position after the row
(including separator), so the next row can be positioned correctly (including separator), so the next row can be positioned correctly
@@ -1047,24 +1099,22 @@ def _draw_record_row(rec, y):
jpeg_data = _record_thumbnail_data(rec) jpeg_data = _record_thumbnail_data(rec)
if jpeg_data is not None: if jpeg_data is not None:
draw_jpeg(jpeg_data, THUMB_MARGIN, thumb_y, THUMB_SIZE, THUMB_SIZE) draw_jpeg(jpeg_data, THUMB_MARGIN, thumb_y, THUMB_SIZE, THUMB_SIZE)
gc.collect()
else: else:
_draw_placeholder(THUMB_MARGIN, thumb_y, THUMB_SIZE, THUMB_SIZE) _draw_placeholder(THUMB_MARGIN, thumb_y, THUMB_SIZE, THUMB_SIZE)
# Title (starts at same height as thumbnail) # Title (starts at same height as thumbnail)
title = display_text(rec.get("title", "Unknown Title")) title = rec.get("_display_title", display_text(rec.get("title", "Unknown Title")))
display.set_font("bitmap8") display.set_font("bitmap8")
ty = y + 4 ty = y + 4
ty = draw_wrapped(title, TEXT_X, ty, TEXT_W, _pen_title, scale=1) ty = draw_wrapped(title, TEXT_X, ty, TEXT_W, _pen_title, scale=1)
# Artists # Artists
artists = rec.get("artists", []) artist_str = rec.get("_display_artists", "")
if artists: if artist_str:
artist_str = display_text(", ".join(artists))
ty += 2 ty += 2
ty = draw_wrapped(artist_str, TEXT_X, ty, TEXT_W, _pen_artist, scale=1) ty = draw_wrapped(artist_str, TEXT_X, ty, TEXT_W, _pen_artist, scale=1)
meta_text = _record_meta_text(rec) meta_text = rec.get("_display_meta", _record_meta_text(rec))
if meta_text: if meta_text:
ty += 2 ty += 2
ty = draw_wrapped(meta_text, TEXT_X, ty, TEXT_W, _pen_dim_text, scale=1) ty = draw_wrapped(meta_text, TEXT_X, ty, TEXT_W, _pen_dim_text, scale=1)
@@ -1211,8 +1261,7 @@ def handle_month_touch(x, y):
# Fetch from API # Fetch from API
recs, had_error = fetch_records(view_year, view_month, day_num) recs, had_error = fetch_records(view_year, view_month, day_num)
records = recs set_day_records(recs, had_error)
records_error = had_error
# Render day view # Render day view
draw_day_view() draw_day_view()
@@ -1227,8 +1276,7 @@ def handle_day_touch(x, y):
if records_error: if records_error:
draw_status("Retrying...") draw_status("Retrying...")
recs, had_error = fetch_records(view_year, view_month, selected_day) recs, had_error = fetch_records(view_year, view_month, selected_day)
records = recs set_day_records(recs, had_error)
records_error = had_error
draw_day_view() draw_day_view()
@@ -1251,7 +1299,7 @@ def main():
global state, view_year, view_month, _last_touch global state, view_year, view_month, _last_touch
global selected_day, records, records_error, scroll_offset global selected_day, records, records_error, scroll_offset
global _dragging global _dragging
global _last_activity global _last_activity, _last_drag_redraw
# -- Init display -- # -- Init display --
init_display() init_display()
@@ -1285,8 +1333,7 @@ def main():
MONTH_NAMES[view_month - 1], today_day, view_year MONTH_NAMES[view_month - 1], today_day, view_year
)) ))
recs, had_error = fetch_records(view_year, view_month, today_day) recs, had_error = fetch_records(view_year, view_month, today_day)
records = recs set_day_records(recs, had_error)
records_error = had_error
draw_day_view() draw_day_view()
_last_activity = time.ticks_ms() _last_activity = time.ticks_ms()
@@ -1334,6 +1381,7 @@ def main():
pending_delta = 0 pending_delta = 0
dragged = False dragged = False
_dragging = False _dragging = False
_last_drag_redraw = time.ticks_ms()
while True: while True:
touch_point = read_touch() touch_point = read_touch()
if touch_point is None: if touch_point is None:
@@ -1344,18 +1392,28 @@ def main():
dragged = True dragged = True
pending_delta += last_drag_y - current_y pending_delta += last_drag_y - current_y
if abs(pending_delta) >= 3: now = time.ticks_ms()
redraw_due = time.ticks_diff(now, _last_drag_redraw) >= DRAG_REDRAW_MS
if abs(pending_delta) >= DRAG_REDRAW_PX and redraw_due:
max_offset = _max_scroll_offset() max_offset = _max_scroll_offset()
new_offset = min(max(0, scroll_offset + pending_delta), max_offset) new_offset = min(max(0, scroll_offset + pending_delta), max_offset)
if new_offset != scroll_offset: if new_offset != scroll_offset:
scroll_offset = new_offset scroll_offset = new_offset
_dragging = True _dragging = True
draw_day_view() draw_day_view()
_last_drag_redraw = now
pending_delta = 0 pending_delta = 0
last_drag_y = current_y last_drag_y = current_y
time.sleep(0.01) time.sleep(0.01)
if pending_delta:
max_offset = _max_scroll_offset()
new_offset = min(max(0, scroll_offset + pending_delta), max_offset)
if new_offset != scroll_offset:
scroll_offset = new_offset
_dragging = True
if _dragging: if _dragging:
_dragging = False _dragging = False
draw_day_view() draw_day_view()