Skip to content

fix(signage): bound media cache storage, bandwidth and memory - #519

Merged
MrYuion merged 3 commits into
developfrom
fix/signage-media-cache
Oct 2, 2026
Merged

MrYuion merged 3 commits into
developfrom
fix/signage-media-cache

Conversation

@MrYuion

@MrYuion MrYuion commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

The offline media cache wasted bandwidth and storage, and could exhaust memory, over long uptime:

  • Budget: the cache budget was a fixed 512 MB, and eviction removed the file it had just downloaded. With 20 × 50 MB videos, every sync downloaded files 11–20, deleted them, then playback downloaded them again: 0.5–1 GB per sync.
  • Full storage: a QuotaExceededError was retried every 5 min, about 14 GB/day for one 50 MB file.
  • Read errors: a failed store read was treated as "file missing". The file was downloaded again and the old record was never deleted, so disk use grew without a limit.
  • Large files: a fixed 15 min download deadline meant files over about 450 MB never finished on a 4 Mbps link.
  • Memory: a download held about 2× the file size in memory (about 1 GB for a 500 MB video on a 2 GB device).
  • Service worker: it stored every S3 media file a second time, with no eviction.

Changes

All in apps/signage/src/app/media-cache.service.ts and apps/signage/ngsw-config.json:

  • Budget: 80% of (quota − usage outside the cache), from navigator.storage.estimate(), with a 512 MB fallback. navigator.storage.persist() is requested once.
    • Files in the current list are never evicted.
    • A file that cannot fit is skipped, or the download stops after its headers or when the body grows too big. The file is listed in media_cache.too_large and streams during playback, with no 30 s wait.
  • Full storage: QuotaExceededError, or a blob write DataError, evicts unneeded files and retries once. If the retry fails, the file streams.
  • Records: a failed store read does not start a download. Replaced, duplicate and empty records are deleted, at boot too.
  • Deadline: while data arrives, only the 60 s stall timeout applies.
  • Streaming: downloads stream into a Blob through a TransformStream. On small storage, Chrome can fail to build a Blob over about 20 MB. Files up to 50 MB then retry once in memory, and larger files stream.
  • Stagger race: after the stagger delay, the sync checks again whether playback cached the file or found it too large.
  • Service worker: the https://*.amazonaws.com/**/*.* asset group entry is removed.

Napkin math for 20 × 50 MB at 4 Mbps:

Before After
Download per sync after the first 0.5–1 GB 0
Peak memory per download ~2× file size about flat (chunk sized)
500 MB video never finishes ~17 min, once

USER_STORIES.md (US-SIG-025, 026) and DEBUGGING.md (new "Media cache storage" section) are updated.

Testing

  • Unit tests for each case. The test that asserted download-then-delete is replaced.
  • nx test signage passes (386 tests). nx build signage passes. nx lint signage has 12 errors that are also on develop.
  • Tested against a local PlaceOS stack with Playwright, with mp4s on a local server:
    • The budget matches the formula to the byte.
    • 10 × 40 MB give one request and one record each; a second sync downloads only the new file.
    • Over budget: one request per refused file, which then streams.
    • A quota error leads to evict, retry once, then streaming.
    • 100 MB at 800 kbps finished in 39.5 min (past the old 15 min cap).
    • 300 MB download: the JS heap stayed at 56–100 MB. The old method reached 353 MB.
    • Orphan cleanup works, there are no amazonaws entries in ngsw.json, and an offline reload plays from the cache.
    • Small storage was tested on real 400 MB, 2 GB and 8 GB volumes: the in-memory fallback, a real disk-full DataError, and 300 MB on 8 GB.

Review follow-up

  • Playback and syncs share one budget check (_cacheFit). Playback never evicts, and on full storage it marks the file too large.
  • Evict before reading a body whose Content-Length needs the room.
  • Room is checked again just before storing and counts entries being stored, so parallel downloads cannot spend the same room.
  • Template backgrounds play from the server when the cache has no copy.
  • CI: the test job runs Node 22, where Response.blob() returns Node's Blob. The spec now gives the service jsdom's Blob. Production is not affected.

Known gaps

  • After an eviction, signage.service.ts logs "Unable to release cached media … not found". This is only log noise.

Merge order

Rebased on develop after #517 and #518 merged. No remaining dependencies.


Changes made by Claude Opus 5.5 (1M context) in Claude Code, running in T3 Code.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
frontend-templates Ignored Ignored Preview Oct 2, 2026 12:44am UTC

@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

[Medium risk] Bounds media cache storage, bandwidth, and memory usage.

The PR is not ready to merge while slow plugins can be skipped and abandoned cache clears can continue after recovery.

What we checked:

  • Can normal playback still preload?: No. When a transition ends, the player sets the active output to the pending output and clears the animation flag.

Summary

The signage media cache now sizes itself to available browser storage, avoids holding whole downloads in memory, and streams files that cannot be cached during playback. It also stops the service worker from keeping a second copy of Amazon-hosted media.

  • Current playlist files stay protected as the cache makes room for new ones.
  • Large or unstorable files can play from the network without repeated cache downloads.
  • Cache records are cleaned up, and slow downloads can continue while data is arriving.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Signage starts] --> B[Watchdog checks playback]
  B --> C{Plugin reports finished?}
  C -->|Yes| D[Next item]
  C -->|No| E{Run limit passed?}
  E -->|Yes| D
  B --> F{Recovery needed?}
  F -->|Yes| G[Clear app cache or reload]
  G --> H{Still on page after two minutes?}
  H -->|Yes| I[Fallback reload]
Loading

Reviews (5) · Last reviewed commit: "fix(signage): stop parallel downloads sp..."

Comment thread apps/signage/src/app/media-cache.service.ts Outdated
Comment thread apps/signage/src/app/media-cache.service.ts Outdated
Comment thread apps/signage/src/app/media-cache.service.ts Outdated
@MrYuion

MrYuion commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

CI test (signage) failure fixed in 1978372. The cause was the test environment. The test job has no setup-node, so it runs the runner image's Node 22, while the build job and local runs use Node 24. Before Node 24, Response.blob() returns Node's own Blob, and jsdom's File stores it as the 13-byte text "[object Blob]"; that is the "expected 13" in both failures. A browser has only one Blob type, so production is not affected. The spec now gives the service jsdom's Blob. Separately, the test job could pin Node 24 to match the build job.

Comment thread apps/signage/src/app/media-cache.service.ts
Comment thread apps/signage/src/app/media-cache.service.ts
@MrYuion
MrYuion force-pushed the fix/signage-media-cache branch from 78e0b24 to c27e200 Compare October 2, 2026 00:24
- The cache budget comes from navigator.storage.estimate() and
  persistent storage is requested. Files in the current playlist are
  never evicted, and a file that cannot fit streams instead. The fixed
  512 MB budget evicted files it had just downloaded, so every sync
  downloaded them again.
- A full store (QuotaExceededError or a blob write DataError) evicts,
  retries once, then streams the file. It retried every 5 minutes.
- A failed store read no longer downloads the file again. Replaced,
  duplicate and empty records are deleted.
- Downloads have no overall deadline while data arrives, so large
  files on slow links can finish.
- Downloads stream into a Blob instead of an array of chunks. When the
  browser cannot build the Blob on small storage, files up to 50 MB
  retry once in memory and larger files stream.
- The sync checks the cache again after its stagger delay.
- The service worker no longer stores S3 media a second time.
- Playback and cache syncs share one budget check. Playback never
  evicts and marks a file too large when storage is full. It allowed a
  download as large as the whole budget and retried full storage.
- Evict before reading a body whose Content-Length needs the room. On a
  nearly full disk the browser can fail to build a Blob that would fit
  once older files are gone.
- Template backgrounds play from the server when the cache has no copy.
  They went blank.
- Tests: hand the service jsdom's Blob. Before Node 24, Response.blob()
  returns Node's Blob, which jsdom's File stores as "[object Blob]".
A sync and playback could each work out the free room, download
different files, and both store them over the budget. The room is now
checked again just before storing, counting entries being stored, and a
file claims its size before the store yields. The sync also works out
the room again after its stagger delay.
@MrYuion
MrYuion force-pushed the fix/signage-media-cache branch from c27e200 to 64745c9 Compare October 2, 2026 00:33
@MrYuion
MrYuion merged commit 1d9a310 into develop Oct 2, 2026
5 of 6 checks passed
@MrYuion
MrYuion deleted the fix/signage-media-cache branch October 2, 2026 00:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant