From a5cd8d625bcae423d8ba53f77d2fbbd798e672d1 Mon Sep 17 00:00:00 2001 From: librelad Date: Fri, 17 Jul 2026 21:35:24 +0100 Subject: [PATCH] perf(backup): throttle + dedupe the WebUI backup dashboard refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Refreshing backup data..." step on every WebUI update fired one `restic stats` (restore-size mode — the slowest restic op) plus ~4 identical unfiltered `restic snapshots --json` pulls per enabled location (dashboard, snapshots, app-status, migrate each pulled their own), all over a fresh SSH connection for remote repos, on every pass with no throttle — the slow, "frozen"-looking line users hit on poor links. Two fixes: 1. Dedupe. engineSnapshotsJson transparently memoises the first unfiltered whole-repo pull per location to a shared cache dir (LP_SNAP_CACHE_DIR, set by webui_updater around the chain), so the four generators reuse one restic call instead of four. Filtered and failed/empty pulls always fall through to a live call. 2. Throttle. Backups and location changes already regenerate this data live when they happen, so the routine pass is only a drift catch-up. Split the cheap local-only generators (engines/schema/passwords — no remote I/O) out to always run, and gate the remote pull behind CFG_BACKUP_DASHBOARD_REFRESH_INTERVAL (minutes, default 30, 0 = every update). A completed backup touches a dirty marker that forces the next pass to pull; WEBUI_UPDATER_FORCE still forces a full refresh. Net: N locations x 5 remote restic calls every update -> 1 call per location, only when something actually changed. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: librelad --- configs/backup/backup_general | 1 + scripts/backup/app/backup_app_start.sh | 5 ++ scripts/backup/engine/engine_dispatch.sh | 21 ++++++- .../generators/config/webui_update_config.sh | 2 +- scripts/webui/webui_updater.sh | 57 ++++++++++++++++--- 5 files changed, 76 insertions(+), 10 deletions(-) diff --git a/configs/backup/backup_general b/configs/backup/backup_general index 832a563..375b11e 100755 --- a/configs/backup/backup_general +++ b/configs/backup/backup_general @@ -3,3 +3,4 @@ # @icon 💾 # ================================================================================ CFG_BACKUP_CRONTAB_APP="0 5 * * *" # App Backup Schedule - Crontab schedule for application backups +CFG_BACKUP_DASHBOARD_REFRESH_INTERVAL=30 # Dashboard Refresh Interval - Minutes between routine restic pulls that refresh the Backups dashboard (a completed backup always refreshes immediately). 0 refreshes on every WebUI update. diff --git a/scripts/backup/app/backup_app_start.sh b/scripts/backup/app/backup_app_start.sh index 6f4fa87..9df0eed 100755 --- a/scripts/backup/app/backup_app_start.sh +++ b/scripts/backup/app/backup_app_start.sh @@ -133,6 +133,11 @@ backupAppStart() webuiGenerateBackupDashboard webuiGenerateBackupSnapshots all webuiGenerateBackupAppStatus "$stored_app_name" + # Signal the throttled routine refresh (webuiLibrePortalUpdate) that a + # new snapshot exists, so its next pass does a full remote pull to + # reconcile the bits this targeted regen skips (migrate + other apps' + # status) even while the refresh window is otherwise closed. + touch "/tmp/libreportal_webui_backup_dirty" 2>/dev/null || true fi echo "" diff --git a/scripts/backup/engine/engine_dispatch.sh b/scripts/backup/engine/engine_dispatch.sh index bc13839..616f4ee 100644 --- a/scripts/backup/engine/engine_dispatch.sh +++ b/scripts/backup/engine/engine_dispatch.sh @@ -52,7 +52,26 @@ engineBackupSystem() { local i="$1"; shift; backupLocationLocalGuard "$i engineRestoreSystemLatest() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")RestoreSystemLatest" "$i" "$@"; } engineRestoreSnapshot() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")RestoreSnapshot" "$i" "$@"; } engineSnapshotLatestId() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")SnapshotLatestId" "$i" "$@"; } -engineSnapshotsJson() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")SnapshotsJson" "$i" "$@"; } +# Whole-repo snapshot list. The WebUI backup refresh pulls this the same way +# (no filters) from four generators per location — dashboard, snapshots, +# app-status and migrate — which on a remote (SSH) repo is four identical restic +# round-trips. When LP_SNAP_CACHE_DIR is set (webui_updater wraps the refresh +# chain with it), memoise the first unfiltered pull per location to a file the +# sibling generators reuse; filtered/parameterised calls (extra args) and any +# failed/empty pull always fall through to a live restic call. +engineSnapshotsJson() { + local i="$1"; shift + if [[ $# -eq 0 && -n "${LP_SNAP_CACHE_DIR:-}" ]]; then + local _cf="${LP_SNAP_CACHE_DIR}/snapshots_${i}.json" + [[ -s "$_cf" ]] && { cat "$_cf"; return 0; } + local _out _rc + _out=$(engineDispatch "$(engineForLocation "$i")SnapshotsJson" "$i"); _rc=$? + [[ $_rc -eq 0 && -n "$_out" ]] && printf '%s' "$_out" > "$_cf" 2>/dev/null + printf '%s' "$_out" + return $_rc + fi + engineDispatch "$(engineForLocation "$i")SnapshotsJson" "$i" "$@" +} engineSystemSnapshotsJson() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")SystemSnapshotsJson" "$i" "$@"; } engineSnapshotListFiles() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")SnapshotListFiles" "$i" "$@"; } engineForgetApp() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")ForgetApp" "$i" "$@"; } diff --git a/scripts/webui/data/generators/config/webui_update_config.sh b/scripts/webui/data/generators/config/webui_update_config.sh index d33ff8c..59c5b05 100755 --- a/scripts/webui/data/generators/config/webui_update_config.sh +++ b/scripts/webui/data/generators/config/webui_update_config.sh @@ -72,7 +72,7 @@ webuiValidateConfigValue() { isError " Invalid crontab format for $var_name" fi ;; - CFG_BACKUP_KEEP_LAST|CFG_BACKUP_KEEP_DAILY|CFG_BACKUP_KEEP_WEEKLY|CFG_BACKUP_KEEP_MONTHLY|CFG_BACKUP_KEEP_YEARLY|CFG_BACKUP_VERIFY_DATA_PERCENT|CFG_UPDATER_CHECK|CFG_UPDATER_SCAN_INTERVAL|CFG_SWAPFILE_SIZE|CFG_GENERATED_PASS_LENGTH|CFG_WEBUI_LOG_STREAM_IDLE_TIMEOUT_MINUTES|CFG_WEBUI_LOG_STREAM_MAX_DURATION_MINUTES|CFG_WEBUI_LOG_STREAM_MAX_LINES_PER_SEC) + CFG_BACKUP_KEEP_LAST|CFG_BACKUP_KEEP_DAILY|CFG_BACKUP_KEEP_WEEKLY|CFG_BACKUP_KEEP_MONTHLY|CFG_BACKUP_KEEP_YEARLY|CFG_BACKUP_VERIFY_DATA_PERCENT|CFG_BACKUP_DASHBOARD_REFRESH_INTERVAL|CFG_UPDATER_CHECK|CFG_UPDATER_SCAN_INTERVAL|CFG_SWAPFILE_SIZE|CFG_GENERATED_PASS_LENGTH|CFG_WEBUI_LOG_STREAM_IDLE_TIMEOUT_MINUTES|CFG_WEBUI_LOG_STREAM_MAX_DURATION_MINUTES|CFG_WEBUI_LOG_STREAM_MAX_LINES_PER_SEC) # Validate numeric values if ! echo "$var_value" | grep -qE '^[0-9]+$'; then isError " $var_name must be a positive integer" diff --git a/scripts/webui/webui_updater.sh b/scripts/webui/webui_updater.sh index 9d720ec..ee8c111 100755 --- a/scripts/webui/webui_updater.sh +++ b/scripts/webui/webui_updater.sh @@ -92,14 +92,55 @@ webuiLibrePortalUpdate() { checkSuccess "Refreshed ${_app} WebUI data..." done - # Generate Backup locations / snapshots / engines / dashboards. - # Announce before running: snapshot generation reaches remote backup - # locations (restic over SSH), and the whole chain's output is - # captured below, so without this the update looks frozen mid-fetch - # — noticeable on slow connections. - isNotice "Refreshing backup data (may be slow on poor connections)..." - local result; result=$(webuiGenerateBackupLocations && webuiGenerateBackupDashboard && webuiGenerateBackupSnapshots all && webuiGenerateBackupAppStatus && webuiGenerateBackupEngines && webuiGenerateBackupSchema && webuiGenerateBackupPasswords && webuiGenerateBackupMigrate) - checkSuccess "Refreshed backup dashboard data..." + # Backup dashboard data, split by cost: + # * local-only (engines/schema/passwords) — cheap file + config + # reads, no remote I/O; always run so engine/password/config + # edits surface on the next pass. + # * remote (locations stats, dashboard, snapshots, app-status, + # migrate) — each spawns restic against every enabled location + # (over SSH for remote repos). Backups and location changes + # already regen this live at the moment they happen + # (backup_app_start.sh / location_*.sh), so this routine pass is + # only a drift catch-up. Throttle it: pull at most once per + # CFG_BACKUP_DASHBOARD_REFRESH_INTERVAL minutes unless a backup + # marked the data dirty since, or the caller forced a full + # refresh (WEBUI_UPDATER_FORCE). 0 disables the throttle. + local result + result=$(webuiGenerateBackupEngines && webuiGenerateBackupSchema && webuiGenerateBackupPasswords) + checkSuccess "Refreshed backup engine/schema/password data..." + + local backup_refresh_min="${CFG_BACKUP_DASHBOARD_REFRESH_INTERVAL:-30}" + local backup_stamp="/tmp/libreportal_webui_backup_refreshed" + # Touched by backup_app_start.sh when a backup completes; a value + # newer than the stamp forces the next routine pass to pull. + local backup_dirty="/tmp/libreportal_webui_backup_dirty" + local do_remote=1 + if [[ -z "$WEBUI_UPDATER_FORCE" && "$backup_refresh_min" != "0" && -f "$backup_stamp" ]]; then + local _bnow _blast _bdirty + _bnow=$(date +%s) + _blast=$(stat -c '%Y' "$backup_stamp" 2>/dev/null || echo 0) + _bdirty=$(stat -c '%Y' "$backup_dirty" 2>/dev/null || echo 0) + if (( _bnow - _blast < backup_refresh_min * 60 )) && (( _bdirty <= _blast )); then + do_remote=0 + fi + fi + + if (( do_remote )); then + # Announce before running: the chain reaches remote backup + # locations (restic over SSH) and its output is captured below, + # so without this the update looks frozen mid-fetch on slow links. + isNotice "Refreshing backup data (may be slow on poor connections)..." + # One unfiltered restic snapshots pull per location, shared across + # the generators below instead of one each — see engineSnapshotsJson. + export LP_SNAP_CACHE_DIR; LP_SNAP_CACHE_DIR="$(mktemp -d 2>/dev/null)" + result=$(webuiGenerateBackupLocations && webuiGenerateBackupDashboard && webuiGenerateBackupSnapshots all && webuiGenerateBackupAppStatus && webuiGenerateBackupMigrate) + checkSuccess "Refreshed backup dashboard data..." + [[ -n "$LP_SNAP_CACHE_DIR" ]] && rm -rf "$LP_SNAP_CACHE_DIR" + unset LP_SNAP_CACHE_DIR + touch "$backup_stamp" 2>/dev/null || true + else + isNotice "Backup data current — remote refresh throttled (last pull <${backup_refresh_min}m ago)." + fi # Peers (named other LibrePortal instances) — small, cheap; lives # in its own data/peers/generated/peers.json file consumed by