mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-19 09:29:06 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8927a1b6b1 |
@@ -1688,6 +1688,43 @@ else
|
||||
echo "✗ $CMDLINE_FILE not found; skipping isolcpus optimization"
|
||||
fi
|
||||
|
||||
# Enable the memory cgroup controller (idempotent).
|
||||
# The Pi firmware boots with cgroup_disable=memory, so systemd's MemoryMax= is
|
||||
# accepted and silently ignored — the display service then has no ceiling, and
|
||||
# a runaway takes the whole board down (sshd can no longer fork, the panel goes
|
||||
# dark) rather than just restarting the one service.
|
||||
if [ "$SKIP_PERF" != "1" ] && [ -f "$CMDLINE_FILE" ]; then
|
||||
if grep -q 'cgroup_enable=memory' "$CMDLINE_FILE"; then
|
||||
echo "cgroup_enable=memory already present in $CMDLINE_FILE"
|
||||
else
|
||||
echo "Adding cgroup_enable=memory to $CMDLINE_FILE..."
|
||||
cp "$CMDLINE_FILE" "$CMDLINE_FILE.bak" 2>/dev/null || true
|
||||
sed -i '1 s/$/ cgroup_enable=memory cgroup_memory=1/' "$CMDLINE_FILE"
|
||||
echo " Takes effect after reboot. Verify with:"
|
||||
echo " grep memory /sys/fs/cgroup/cgroup.controllers"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Persist the journal (idempotent).
|
||||
# These images default to volatile storage: journald keeps everything in /run
|
||||
# (tmpfs), so every reboot destroys the logs — including the ones that would
|
||||
# explain why the board rebooted. Capped so an SD card is not worn out by logs.
|
||||
if [ -d /var/log/journal ] && [ -n "$(ls -A /var/log/journal 2>/dev/null)" ]; then
|
||||
echo "Persistent journald storage already enabled"
|
||||
else
|
||||
echo "Enabling persistent journald storage..."
|
||||
mkdir -p /etc/systemd/journald.conf.d
|
||||
cat > /etc/systemd/journald.conf.d/ledmatrix-persistent.conf <<'JOURNALD'
|
||||
# Installed by LEDMatrix first_time_install.sh
|
||||
[Journal]
|
||||
Storage=persistent
|
||||
SystemMaxUse=64M
|
||||
JOURNALD
|
||||
mkdir -p /var/log/journal
|
||||
systemd-tmpfiles --create --prefix /var/log/journal >/dev/null 2>&1 || true
|
||||
systemctl restart systemd-journald >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Ensure dtparam=audio=off in config.txt (idempotent)
|
||||
if [ "$SKIP_PERF" = "1" ]; then
|
||||
: # skipped
|
||||
|
||||
Vendored
+47
@@ -4,11 +4,58 @@ Memory Cache
|
||||
Handles in-memory caching with TTL support, size limits, and automatic cleanup.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
# Historical fixed ceiling, kept as the fallback when RAM cannot be read.
|
||||
DEFAULT_MAX_SIZE = 1000
|
||||
|
||||
|
||||
def _total_memory_mb() -> Optional[float]:
|
||||
"""Physical RAM in MB, or None where /proc/meminfo is unavailable."""
|
||||
try:
|
||||
with open('/proc/meminfo', 'r', encoding='utf-8') as fh:
|
||||
for line in fh:
|
||||
if line.startswith('MemTotal:'):
|
||||
return int(line.split()[1]) / 1024
|
||||
except (OSError, ValueError, IndexError):
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def default_max_size() -> int:
|
||||
"""Entry ceiling scaled to this machine's RAM.
|
||||
|
||||
One fixed ceiling cannot serve both a 512 MB Pi Zero 2 W and an 8 GB Pi 5.
|
||||
Entries here are parsed API payloads that routinely run tens of kilobytes
|
||||
each, so a thousand of them is a comfortable cache on a large board and a
|
||||
substantial fraction of total RAM on a small one — where the process
|
||||
competing for that RAM is also driving the panel. Set
|
||||
LEDMATRIX_CACHE_MAX_ENTRIES to override.
|
||||
"""
|
||||
override = os.environ.get('LEDMATRIX_CACHE_MAX_ENTRIES')
|
||||
if override:
|
||||
try:
|
||||
value = int(override)
|
||||
if value > 0:
|
||||
return value
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
total_mb = _total_memory_mb()
|
||||
if total_mb is None:
|
||||
return DEFAULT_MAX_SIZE
|
||||
if total_mb < 1536: # 512 MB and 1 GB boards
|
||||
return 150
|
||||
if total_mb < 3072: # 2 GB
|
||||
return 400
|
||||
if total_mb < 6144: # 4 GB
|
||||
return 800
|
||||
return 1500 # 8 GB and up
|
||||
|
||||
|
||||
class MemoryCache:
|
||||
"""Manages in-memory cache with TTL and size limits."""
|
||||
|
||||
@@ -33,7 +33,7 @@ import logging
|
||||
import threading
|
||||
import tempfile
|
||||
from src.exceptions import CacheError
|
||||
from src.cache.memory_cache import MemoryCache
|
||||
from src.cache.memory_cache import MemoryCache, default_max_size
|
||||
from src.cache.disk_cache import DiskCache
|
||||
from src.cache.cache_strategy import CacheStrategy
|
||||
from src.cache.cache_metrics import CacheMetrics
|
||||
@@ -84,7 +84,9 @@ class CacheManager:
|
||||
self.logger.warning("ConfigManager not available, using default cache intervals")
|
||||
|
||||
# Initialize cache components using composition
|
||||
self._memory_cache_component = MemoryCache(max_size=1000, cleanup_interval=300.0)
|
||||
self._memory_cache_component = MemoryCache(
|
||||
max_size=default_max_size(), cleanup_interval=300.0
|
||||
)
|
||||
self._disk_cache_component = DiskCache(cache_dir=self.cache_dir, logger=self.logger)
|
||||
self._strategy_component = CacheStrategy(config_manager=self.config_manager, logger=self.logger)
|
||||
self._metrics_component = CacheMetrics(logger=self.logger)
|
||||
|
||||
@@ -14,7 +14,7 @@ import sys
|
||||
import subprocess
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, Tuple, Type
|
||||
from typing import Dict, Any, List, Optional, Tuple, Type
|
||||
import logging
|
||||
|
||||
from packaging.requirements import InvalidRequirement, Requirement
|
||||
@@ -45,6 +45,58 @@ def requirements_has_real_deps(requirements_file: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _extra_dependencies(dist_name: str, extras) -> Optional[List[Requirement]]:
|
||||
"""Dependencies a distribution declares *only* behind the given extras.
|
||||
|
||||
Returns None when the installed metadata cannot be read or parsed, so the
|
||||
caller can fall back to running pip rather than assuming anything.
|
||||
"""
|
||||
try:
|
||||
meta = importlib.metadata.metadata(dist_name)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
return None
|
||||
|
||||
gated: List[Requirement] = []
|
||||
for raw in meta.get_all('Requires-Dist') or []:
|
||||
try:
|
||||
dep = Requirement(raw)
|
||||
except InvalidRequirement:
|
||||
return None
|
||||
if dep.marker is None:
|
||||
continue
|
||||
# Keep only what the distribution gates behind an extra we asked for:
|
||||
# satisfied when `extra` is that name, but not when no extra is
|
||||
# requested. A marker that holds either way (python_version, sys_platform)
|
||||
# belongs to the base install and is already covered by the version check.
|
||||
if dep.marker.evaluate({'extra': ''}):
|
||||
continue
|
||||
if any(dep.marker.evaluate({'extra': extra}) for extra in extras):
|
||||
gated.append(dep)
|
||||
return gated
|
||||
|
||||
|
||||
def _extras_are_satisfied(req: Requirement) -> bool:
|
||||
"""Check the dependencies pulled in by req's extras are installed.
|
||||
|
||||
One level deep, not transitive: enough to tell "the extra was installed"
|
||||
from "the extra was never installed", which is all the caller needs to
|
||||
decide whether pip has work to do. Anything unreadable returns False, so
|
||||
the caller still falls through to pip.
|
||||
"""
|
||||
gated = _extra_dependencies(req.name, req.extras)
|
||||
if gated is None:
|
||||
return False
|
||||
|
||||
for dep in gated:
|
||||
try:
|
||||
dep_version = importlib.metadata.version(dep.name)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
return False
|
||||
if dep.specifier and not dep.specifier.contains(dep_version, prereleases=True):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def requirements_are_satisfied(requirements_file: str) -> bool:
|
||||
"""
|
||||
Check whether every real requirement line in requirements.txt is already
|
||||
@@ -76,9 +128,6 @@ def requirements_are_satisfied(requirements_file: str) -> bool:
|
||||
except InvalidRequirement:
|
||||
return False
|
||||
|
||||
if req.extras:
|
||||
return False # verifying extras' sub-dependencies isn't worth it here
|
||||
|
||||
if req.marker is not None and not req.marker.evaluate():
|
||||
continue # not applicable on this platform/interpreter
|
||||
|
||||
@@ -90,6 +139,9 @@ def requirements_are_satisfied(requirements_file: str) -> bool:
|
||||
if req.specifier and not req.specifier.contains(installed_version, prereleases=True):
|
||||
return False
|
||||
|
||||
if req.extras and not _extras_are_satisfied(req):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,19 @@ ExecStart=/usr/bin/python3 __PROJECT_ROOT_DIR__/run.py
|
||||
# that a successful outcome and never bringing it back.
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
# Memory ceiling as a share of physical RAM, so one unit file suits a 512 MB
|
||||
# Pi Zero 2 W and an 8 GB Pi 5 alike. This is a backstop, not a tuning knob: it
|
||||
# turns "the board runs out of memory, stops being able to fork, and takes sshd
|
||||
# and the panel down together until someone pulls the plug" into "this one
|
||||
# service restarts".
|
||||
#
|
||||
# NOTE: Raspberry Pi firmware boots the kernel with cgroup_disable=memory, and
|
||||
# systemd accepts this setting and then silently ignores it. Verify with:
|
||||
# grep memory /sys/fs/cgroup/cgroup.controllers
|
||||
# If that prints nothing, add "cgroup_enable=memory cgroup_memory=1" to
|
||||
# /boot/firmware/cmdline.txt (all on line 1) and reboot. first_time_install.sh
|
||||
# does this for you.
|
||||
MemoryMax=85%
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=ledmatrix
|
||||
|
||||
Reference in New Issue
Block a user