fix(viewer): force Chromium's low-memory profile on ~1 GB boards#3178
Merged
Conversation
A ~1 GB board with no swap head-room (Raspberry Pi 3, and 1 GB Pi 4 / Pi 5 SKUs) slowly grows past its RAM under a web-page asset that stays up or cycles for hours, ending in a black screen / swap thrash (forum report: a Pi 3B+ rendered unusable, load ~9, after long uptime). QtWebEngine ships Chromium's desktop-tuned, multi-process memory model and only auto-enables its low-memory profile below ~512 MB, so a ~1 GB board never gets it. Force it on and bound the per-renderer footprint by prepending to QTWEBENGINE_CHROMIUM_FLAGS in _build_webview_env: --enable-low-end-device-mode --js-flags=--max-old-space-size=64 --renderer-process-limit=1 --process-per-site --disable-dev-shm-usage Gate on measured RAM (MemTotal from /proc/meminfo, shared with the host) rather than a board/DEVICE_TYPE allowlist, so every low-memory SKU is covered — including the 1 GB Qt5 armhf boards — and 2 GB+ boards keep the faster default process model. The switches are prepended so a device-supplied QTWEBENGINE_CHROMIUM_FLAGS and the dark-mode --blink-settings switch main.cpp appends still apply, and guarded on the flag string so an inherited value on a respawn can't stack duplicates. Hardware-validated on a real Pi 3B+ (806 MB RAM, no swap), 10-min A/B soak with two web pages cycling every 20 s: steady-state viewer-container RSS dropped from ~225 MB to ~147 MB (-78 MB / -35 %), the AnthiasViewer process from ~78 MB to ~59 MB, with ~20 MB more MemAvailable and no crashes. On a 787 MB no-swap board that margin is the difference between staying alive and OOM under the same workload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR reduces QtWebEngine/Chromium memory pressure on ~1 GB devices by conditionally prepending Chromium “low-end device” flags to QTWEBENGINE_CHROMIUM_FLAGS when _build_webview_env() detects low total RAM via /proc/meminfo, and adds unit tests to validate the RAM gate and flag injection behavior.
Changes:
- Add RAM-based low-memory detection (
_system_memory_kb(),_is_low_memory_board()) and a threshold constant. - Prepend a set of Chromium flags in
_build_webview_env()for low-memory boards, with idempotence to avoid stacking on respawn. - Add unit tests covering the gate, injection, no-op on roomy boards, and idempotence/preservation of existing flags.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/anthias_viewer/__init__.py |
Adds low-memory RAM detection and conditionally injects Chromium low-memory flags into the webview spawn environment. |
tests/test_viewer.py |
Adds unit tests validating the low-memory detection and flag injection behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…m_device Reuse anthias_common.board.is_low_ram_device / LOW_RAM_THRESHOLD_KB — the canonical ~1 GB gate that already drives the video 1080p upload cap and the system-info Low-RAM badge — instead of a second /proc/meminfo reader with its own threshold. One source of truth: the viewer, asset processor and UI now agree on which boards are constrained, and the gate reads the host_agent-published MemTotal from Redis so server and viewer can't drift. Also correct the system-info Low-RAM badge: single-QWebEngineView is unconditional now (the preloaded dual-buffer was removed for issue #2954), so the badge described a mode every board runs. It now lists the actual RAM-gated behaviours — the Chromium low-memory profile for web pages and the 1080p video upload cap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot review: the previous guard keyed only on --enable-low-end-device-mode, so a device that pre-set just that switch (without the renderer/V8 caps) would skip the rest, and a device that set a different --js-flags value would be silently overridden. Add each flag independently, matching a valued switch by its "--name=" prefix and a bare switch by the whole token, so an inherited value on a respawn isn't duplicated and a switch the operator set on purpose is preserved. Prepend order is unchanged. Also assert the prepend order in the test (injected flags precede the device-supplied one — an accidental append would otherwise still pass) and add a case for a device that pre-set some switches. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Problem
A forum user's 1 GB Raspberry Pi 3B+ was rendered unusable after updating: black screen, memory exhausted, CPU load ~9 (swap thrash), while running a simple schedule cycling web pages. See https://forums.screenly.io/t/anthias-2026-07-1-renders-pi-3b-unusable/6731
Root-caused on a real Pi 3B+ (806 MB RAM, no swap). The culprit is not the viewer's stdout buffering — direct measurement showed AnthiasViewer emits a fixed ~304 KB startup burst then ~0 B/s during web-page cycling, so that buffer never grows. The real pressure is QtWebEngine's Chromium footprint itself: the viewer container sat at ~225 MB steady-state on a 787 MB no-swap board, leaving almost no head-room for the OS + server + celery + redis, and creeping over long uptime until OOM/thrash.
QtWebEngine ships Chromium's desktop-tuned, multi-process memory model and only auto-enables its low-memory profile below ~512 MB — so a ~1 GB board never gets it.
Fix
Force Chromium's low-memory profile and cap the per-renderer footprint by prepending to
QTWEBENGINE_CHROMIUM_FLAGSin_build_webview_env:Gated on the shared
is_low_ram_device()(anthias_common.board,LOW_RAM_THRESHOLD_KB= 1.5 GiB) — the same RAM gate that already drives the video 1080p upload cap and the system-info Low-RAM badge, reading the host_agent-publishedMemTotalfrom Redis so the viewer, asset processor and UI can't drift. Not a board/DEVICE_TYPEallowlist, so every low-memory SKU is covered (a 1 GB Pi 4 / Pi 5, and the 1 GB Qt5 armhf boards) while 2 GB+ boards keep the faster default process model. The stale "single-QWebEngineView" line in that badge (unconditional since the dual-buffer removal) is corrected to list the real RAM-gated behaviours.The switches are prepended so a device-supplied
QTWEBENGINE_CHROMIUM_FLAGSand the dark-mode--blink-settingsswitchmain.cppappends still apply, and guarded on the flag string so an inherited value on a webview respawn can't stack duplicates.Hardware validation
Validated on two different low-RAM board families, each a 10-minute A/B soak with two web pages cycling every 20 s, flags confirmed present in the running Chromium's environ (and on the Rock Pi, engaged through the real
is_low_ram_device()Redis gate, not a test override):Raspberry Pi 3B+ — 787 MB, no swap, eglfs /
pi3-64(Qt6):Treatment RSS kept declining (287→147 MB) as low-end mode's purge reclaimed memory, versus baseline's flat 225 MB plateau — so it bounds the slow growth, not just the floor.
Rock Pi 4B — 966 MB, no swap, cage/Wayland / generic
arm64(latest-arm64):The smaller win here is because the
latest-arm64build's baseline renderer already ships some Chromium memory tuning (--num-raster-threads=3,--disable-databases,--disable-features=…), so low-end mode has less slack to reclaim — but the gate fires correctly on a completely different stack (cage/Wayland vs eglfs), which is the point.No crashes on either board; the webview cycled normally throughout. The reduction is board-dependent (−11 % to −35 %) but always positive. On a ~1 GB no-swap board that freed head-room is the OOM-vs-survives margin. (Neither board wedged in the 10-min window — this measures footprint + correct gating, not a reproduced-then-fixed wedge.)
--single-process(a larger saving but a stability risk on eglfs) was deliberately left out; the flags above already deliver the above without it.Tests
Added unit coverage for the RAM gate and the flag injection: activates below the threshold, no-ops at 2 GB, fails closed when
/proc/meminfois unreadable, prepends without clobbering a device value, and is idempotent across a respawn. Fulltests/test_viewer.pysuite green (149 passed).🤖 Generated with Claude Code