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)
DEBOUNCE_MS = 300
DRAG_REDRAW_MS = 40
DRAG_REDRAW_PX = 8
# WiFi connection timeout (seconds)
WIFI_TIMEOUT = 30
@@ -164,9 +166,11 @@ records_error = False # True if API call failed
scroll_offset = 0 # Pixel scroll position for day view
_visible_count = 1 # How many records fit on screen (set during draw)
_dragging = False # True while fast scroll redraws are active
_content_height = 0 # Cached total record list height
# Touch debounce
_last_touch = 0
_last_drag_redraw = 0
# Display sleep state
_last_activity = 0
@@ -539,6 +543,21 @@ def fetch_records(year, month, day):
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):
"""Fetch a JPEG thumbnail from the given URL.
@@ -933,7 +952,7 @@ def _draw_record_list():
visible_count = 0
for rec in records:
row_h = _record_row_height(rec)
row_h = rec.get("_row_height", THUMB_SIZE + 8)
row_bottom = cy + row_h
if row_bottom <= RECORD_START_Y:
@@ -960,28 +979,56 @@ def _draw_record_list():
def _max_scroll_offset():
"""Return the maximum pixel scroll offset for the current records."""
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")
for rec in records:
content_h += _record_row_height(rec)
_content_height = 0
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):
"""Calculate a record row height without drawing it."""
display.set_font("bitmap8")
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
artists = rec.get("artists", [])
if artists:
artist_str = display_text(", ".join(artists))
artist_str = rec.get("_display_artists", "")
if artist_str:
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
content_h = max(THUMB_SIZE, ty) + 10
@@ -1016,11 +1063,7 @@ def _record_thumbnail_data(rec):
if cached is not None:
return cached
thumb_url = (
rec.get("micro_cover_url", "")
or rec.get("mini_cover_url", "")
or rec.get("thumb_url", "")
)
thumb_url = rec.get("_thumb_url", "") or _record_thumbnail_url(rec)
if not thumb_url:
return None
@@ -1033,6 +1076,15 @@ def _record_thumbnail_data(rec):
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):
"""Draw a single record row. Returns the Y position after the row
(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)
if jpeg_data is not None:
draw_jpeg(jpeg_data, THUMB_MARGIN, thumb_y, THUMB_SIZE, THUMB_SIZE)
gc.collect()
else:
_draw_placeholder(THUMB_MARGIN, thumb_y, THUMB_SIZE, THUMB_SIZE)
# 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")
ty = y + 4
ty = draw_wrapped(title, TEXT_X, ty, TEXT_W, _pen_title, scale=1)
# Artists
artists = rec.get("artists", [])
if artists:
artist_str = display_text(", ".join(artists))
artist_str = rec.get("_display_artists", "")
if artist_str:
ty += 2
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:
ty += 2
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
recs, had_error = fetch_records(view_year, view_month, day_num)
records = recs
records_error = had_error
set_day_records(recs, had_error)
# Render day view
draw_day_view()
@@ -1227,8 +1276,7 @@ def handle_day_touch(x, y):
if records_error:
draw_status("Retrying...")
recs, had_error = fetch_records(view_year, view_month, selected_day)
records = recs
records_error = had_error
set_day_records(recs, had_error)
draw_day_view()
@@ -1251,7 +1299,7 @@ def main():
global state, view_year, view_month, _last_touch
global selected_day, records, records_error, scroll_offset
global _dragging
global _last_activity
global _last_activity, _last_drag_redraw
# -- Init display --
init_display()
@@ -1285,8 +1333,7 @@ def main():
MONTH_NAMES[view_month - 1], today_day, view_year
))
recs, had_error = fetch_records(view_year, view_month, today_day)
records = recs
records_error = had_error
set_day_records(recs, had_error)
draw_day_view()
_last_activity = time.ticks_ms()
@@ -1334,6 +1381,7 @@ def main():
pending_delta = 0
dragged = False
_dragging = False
_last_drag_redraw = time.ticks_ms()
while True:
touch_point = read_touch()
if touch_point is None:
@@ -1344,18 +1392,28 @@ def main():
dragged = True
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()
new_offset = min(max(0, scroll_offset + pending_delta), max_offset)
if new_offset != scroll_offset:
scroll_offset = new_offset
_dragging = True
draw_day_view()
_last_drag_redraw = now
pending_delta = 0
last_drag_y = current_y
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:
_dragging = False
draw_day_view()