Skip to content

fix(viewer): force Chromium's low-memory profile on ~1 GB boards#3178

Merged
vpetersson merged 3 commits into
masterfrom
fix/webview-low-memory-chromium-profile
Jul 10, 2026
Merged

fix(viewer): force Chromium's low-memory profile on ~1 GB boards#3178
vpetersson merged 3 commits into
masterfrom
fix/webview-low-memory-chromium-profile

Conversation

@vpetersson

@vpetersson vpetersson commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

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_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

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-published MemTotal from Redis so the viewer, asset processor and UI can't drift. Not a board/DEVICE_TYPE allowlist, 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_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 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):

Steady-state Baseline + low-mem flags Δ
Total viewer-container RSS ~225 MB ~147 MB −78 MB (−35 %)
AnthiasViewer main RSS ~78 MB ~59 MB −24 %
MemAvailable head-room ~218–256 MB ~235–272 MB +~20 MB

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):

Steady-state Baseline + low-mem flags Δ
Total viewer-container RSS ~235 MB ~208 MB −27 MB (−11 %)
AnthiasViewer main RSS ~95 MB ~86 MB −10 %
MemAvailable head-room ~155–172 MB ~162–177 MB +~10 MB

The smaller win here is because the latest-arm64 build'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/meminfo is unreadable, prepends without clobbering a device value, and is idempotent across a respawn. Full tests/test_viewer.py suite green (149 passed).

🤖 Generated with Claude Code

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>
@vpetersson vpetersson requested a review from a team as a code owner July 9, 2026 17:31
@vpetersson vpetersson requested a review from Copilot July 9, 2026 17:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/anthias_viewer/__init__.py Outdated
Comment thread src/anthias_viewer/__init__.py Outdated
…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 AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread src/anthias_viewer/__init__.py
Comment thread tests/test_viewer.py
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>
@sonarqubecloud

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@vpetersson vpetersson merged commit de2f6e5 into master Jul 10, 2026
10 checks passed
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.

2 participants