3ac7f836c4
Replaces the Windows port with a Raspberry Pi / Linux implementation on Raspberry Pi OS "Trixie" (Debian 13, aarch64, Wayland/labwc). The Windows code is removed here but preserved on the Windows-Player branch. Entry point ----------- linux/run_linux.py replaces windows/run_win.py. src/main.py stays platform-neutral; all Pi-specific behaviour is injected from linux/. Five bugs that prevented the port (all measured on real hardware) ---------------------------------------------------------------- 1. Kivy's PyPI wheel bundles an SDL2 built WITHOUT the wayland driver, so no window could be created (Trixie has no X server). linux/fix_kivy_sdl2.sh symlinks the system SDL2 over the bundled filename. 2. SDL2 requires WAYLAND_DISPLAY to be *set* - the socket alone is not enough, unlike wlopm. This broke every systemd/cron/autostart launch. linux_display.ensure_session_environment() detects and exports it. 3. Kivy's Clock resolves callbacks via func.__name__; a patch assigned under a different name crashed the player ~20s after a successful start. 4. The inherited signal_screen_activity() shelled out to tvservice, xdotool and ydotool - none exist on Trixie - and mis-escaped 'wlopm --on \*', so the display blanked after 10 minutes. 5. The launchers ran src/main.py directly, bypassing every platform patch and resolving the data directory one level too high. Web links --------- - --ozone-platform-hint=auto does NOT fall back to Wayland on Chromium 152; it aborts. The platform is now chosen explicitly. - The keyring password prompt is suppressed via the ENVIRONMENT, not the flags: launch_env() strips DBUS_SESSION_BUS_ADDRESS for the child so Chromium cannot reach gnome-keyring-daemon. - Teardown kills the whole process group (needs start_new_session=True); previously it silently fell back to terminate() and orphaned children. Video normalisation ------------------- A 4K video cannot play on a Pi 4: ffpyplayer decodes in software, measured at 0.90x realtime (1080p is 3.03x). Oversized media is downscaled to 1920x1080 at sync time using the hardware h264_v4l2m2m encoder (~31s for an 18s clip), triggered by resolution only so already-playable files are untouched. src/media_state.py owns the shared on-disk contract: a .kiwy-converting marker makes the player skip the item while it is being rebuilt, then the converted file is played instead. If nothing is playable at all (a single-item playlist whose only video is converting), the player loops the intro video rather than leaving a blank screen. Also fixed ---------- - network_monitor: replaced netsh/ifconfig/dhclient with nmcli (Trixie uses NetworkManager; ifconfig and dhclient are not even installed). - Removed the Windows-only focus keeper/guardian from main.py. - main.py: duplicate SDL_AUDIODRIVER setdefault (a silent no-op); Settings "Test connection" now uses tempfile.gettempdir(). - config/app_config.json: credentials blanked so a fresh clone runs the first-run setup flow. Verification ------------ linux/test_media_state.py 18/18, test_linux_patches.py 21/21, test_linux_browser_flags.py 27/27. Verified live against a real DigiServer: image -> weblink -> image -> video with correct durations, zero leaked Chromium processes, and no throttling over a 10 minute monitored run.
127 lines
3.8 KiB
Python
127 lines
3.8 KiB
Python
"""_probe_chromium_footprint.py — measure Chromium's footprint for one page.
|
|
|
|
Compares the flag profiles so the memory trade-off is measured rather than
|
|
guessed. Run:
|
|
|
|
.venv/bin/python linux/_probe_chromium_footprint.py [url]
|
|
|
|
Reports process count and total resident memory for the browser tree, using the
|
|
same flags the player uses (so the numbers match production).
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from linux_display import ensure_session_environment # noqa: E402
|
|
|
|
ensure_session_environment()
|
|
|
|
import linux_browser # noqa: E402
|
|
|
|
URL = sys.argv[1] if len(sys.argv) > 1 else 'about:blank'
|
|
|
|
|
|
def tree_memory(root_pid):
|
|
"""(PSS kB, process count) for root_pid and its descendants.
|
|
|
|
PSS (proportional set size) from ``/proc/<pid>/smaps_rollup`` is the right
|
|
measure here, **not** RSS. Chromium forks many processes that share the same
|
|
libraries and file-backed pages; summing RSS counts every shared page once
|
|
per process, which inflated earlier measurements by roughly 2x and even made
|
|
a smaller configuration look larger.
|
|
"""
|
|
try:
|
|
out = subprocess.run(
|
|
['ps', '-eo', 'pid,ppid'], capture_output=True, text=True,
|
|
timeout=10, check=False,
|
|
).stdout
|
|
except Exception:
|
|
return 0, 0
|
|
|
|
parents = {}
|
|
for line in out.splitlines()[1:]:
|
|
parts = line.split()
|
|
if len(parts) >= 2 and parts[0].isdigit():
|
|
parents[int(parts[0])] = int(parts[1])
|
|
|
|
family = [root_pid]
|
|
changed = True
|
|
while changed:
|
|
changed = False
|
|
for pid, parent in parents.items():
|
|
if parent in family and pid not in family:
|
|
family.append(pid)
|
|
changed = True
|
|
|
|
total_kb = 0
|
|
for pid in family:
|
|
try:
|
|
with open(f'/proc/{pid}/smaps_rollup') as fh:
|
|
for line in fh:
|
|
if line.startswith('Pss:'):
|
|
total_kb += int(line.split()[1])
|
|
break
|
|
except OSError:
|
|
# Process exited between listing and reading.
|
|
continue
|
|
return total_kb, len(family)
|
|
|
|
|
|
def measure(mode):
|
|
os.environ['KIWY_CHROMIUM_MODE'] = mode
|
|
profile = f'/tmp/kiwy-footprint-{mode}'
|
|
subprocess.run(['rm', '-rf', profile], check=False)
|
|
|
|
adapter = linux_browser.LinuxChromiumAdapter(
|
|
browser_path=linux_browser.find_linux_browser(), kiosk=True,
|
|
)
|
|
adapter._profile_dir = profile
|
|
|
|
ok = adapter.launch(URL, 1280, 720)
|
|
if not ok or adapter._proc is None:
|
|
return None
|
|
|
|
try:
|
|
# Let Chromium finish spawning helpers before sampling.
|
|
time.sleep(8)
|
|
if adapter._proc.poll() is not None:
|
|
return {'crashed': True, 'rc': adapter._proc.returncode}
|
|
pss_kb, procs = tree_memory(adapter._proc.pid)
|
|
return {"pss_mb": pss_kb / 1024.0, "procs": procs}
|
|
finally:
|
|
adapter.teardown()
|
|
time.sleep(1.5)
|
|
|
|
|
|
print(f'url = {URL}\n')
|
|
print(f'{"mode":<10} {"procs":>6} {"PSS (MB)":>10}')
|
|
print('-' * 30)
|
|
|
|
results = {}
|
|
for mode in ('safe', 'light', 'minimal'):
|
|
result = measure(mode)
|
|
results[mode] = result
|
|
if result is None:
|
|
note = 'launch failed'
|
|
elif result.get('crashed'):
|
|
note = f'CRASHED rc={result["rc"]}'
|
|
else:
|
|
note = ''
|
|
if note:
|
|
print(f'{mode:<10} {"-":>6} {note:>10}')
|
|
else:
|
|
print(f'{mode:<10} {result["procs"]:>6} {result["pss_mb"]:>10.0f}')
|
|
|
|
print()
|
|
base = results.get('safe', {}) or {}
|
|
light = results.get('light', {}) or {}
|
|
if base.get('pss_mb') and light.get('pss_mb'):
|
|
saved = base['pss_mb'] - light['pss_mb']
|
|
pct = 100.0 * saved / base['pss_mb']
|
|
print(f'light mode saves {saved:.0f} MB ({pct:.0f}%) vs the default flag set')
|
|
print(f' processes: {base["procs"]} -> {light["procs"]}')
|