ML-167: presto performance improvements
This commit is contained in:
@@ -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 -->
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user