]> ##2026.07.31 - Fix: on some systems the boot-time run patched DockerClient.php, failed its own php -l verification before the rest of userspace was up, and rolled back to stock, leaving the page-load speedup silently off until something re-ran it. If the cache is enabled but not present at the end of a run, it now retries once after the system settles. - Added the Community Applications submission metadata (ca_profile.xml, plugin wrapper) and an MIT LICENSE. - New: Settings page (Settings -> Utilities -> Docker Page Speedup, or click the bolt icon) to toggle the page-load speedup and set the Docker stats refresh interval. - New: configurable Docker stats refresh (default 5s) throttles nchan/docker_load, cutting CPU/GPU load from the stock 1s live-stats repaint while the Docker page is open. - Page-load speedup measured ~9x cold / ~56x warm on a server with well over 100 containers. - Fix: Plugins-page description renders at normal size. The README is a bold title with a plain-text body (the webGUI styles headings and body bold at 1.3rem, i.e. oversized). - Config-driven, idempotent, anchor-guarded, php -l verified with auto-rollback; both patches fully reverted on uninstall. memoize the per-request template dir listing # Fix D/E getTemplateValue() -> parse each template XML at most once/request # # Without these, a full Docker page render calls getTemplateValue() ~9x per # container (WebUI, MyIP, Shell, Registry, Support, Project, DonateLink, ReadMe, # Icon) and EACH call re-parses every template XML off the USB flash. That is # O(containers * fields * templates) flash reads. The caches collapse it to # O(templates) parses per request. # # Safety: writes only if all 4 exact anchors match (so it cleanly no-ops on any # Unraid build whose code differs), and is a no-op if already patched. The # caller is expected to `php -l` the result and roll back on any lint failure. # # Exit codes: 0 patched, 2 already-patched, 1 anchors-didn't-match (no write). import sys path = sys.argv[1] if len(sys.argv) > 1 else \ "/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerClient.php" with open(path) as f: c = f.read() orig = c if "templatesCache" in c or "cachedDoc" in c: print("ALREADY_PATCHED") sys.exit(2) fixes = 0 # --- Fix A: declare getTemplates() memo cache + early return ----------------- A_old = ("\tpublic function getTemplates($type) {\n" "\t\tglobal $dockerManPaths;\n" "\t\t$tmpls = $dirs = [];") A_new = ("\tprivate static $templatesCache = [];\n" "\tpublic function getTemplates($type) {\n" "\t\tglobal $dockerManPaths;\n" "\t\tif (isset(self::$templatesCache[$type])) return self::$templatesCache[$type];\n" "\t\t$tmpls = $dirs = [];") if A_old in c: c = c.replace(A_old, A_new, 1); fixes += 1 # --- Fix B: populate getTemplates() memo cache before return ---------------- B_old = ("\t\tarray_multisort(array_column($tmpls,'name'), SORT_NATURAL|SORT_FLAG_CASE, $tmpls);\n" "\t\treturn $tmpls;\n" "\t}") B_new = ("\t\tarray_multisort(array_column($tmpls,'name'), SORT_NATURAL|SORT_FLAG_CASE, $tmpls);\n" "\t\tself::$templatesCache[$type] = $tmpls;\n" "\t\treturn $tmpls;\n" "\t}") if B_old in c: c = c.replace(B_old, B_new, 1); fixes += 1 # --- Fix D: add per-request XML-parse cache helper before getTemplateValue -- D_old = "\tpublic function getTemplateValue($Repository, $field, $scope='all',$name='') {" D_new = ("\tprivate static $xmlCache = [];\n" "\tprivate function cachedDoc($path) {\n" "\t\tif (!isset(self::$xmlCache[$path])) { $d = new DOMDocument(); @$d->load($path); self::$xmlCache[$path] = $d; }\n" "\t\treturn self::$xmlCache[$path];\n" "\t}\n" "\tpublic function getTemplateValue($Repository, $field, $scope='all',$name='') {") if D_old in c: c = c.replace(D_old, D_new, 1); fixes += 1 # --- Fix E: use the cache inside getTemplateValue --------------------------- E_old = ("\t\t\t$doc = new DOMDocument();\n" "\t\t\t$doc->load($file['path']);") E_new = "\t\t\t$doc = $this->cachedDoc($file['path']);" if E_old in c: c = c.replace(E_old, E_new, 1); fixes += 1 if fixes != 4 or c == orig: sys.stderr.write("PATCH_ABORT: expected 4 anchors, matched %d (file left untouched)\n" % fixes) sys.exit(1) with open(path, "w") as f: f.write(c) print("PATCHED_OK 4") sys.exit(0) ]]> ) # * after Settings -> Apply (via the page's #command) # Reads /boot/config/plugins/docker.page.speedup/docker.page.speedup.cfg (which # update.php has already written before calling us) and makes the live webGUI # match it. Idempotent, php -l verified with auto-rollback, fully reversible. TAG="docker-page-speedup" DIR="/usr/local/emhttp/plugins/docker.page.speedup" CFG="/boot/config/plugins/docker.page.speedup/docker.page.speedup.cfg" DC="/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerClient.php" DCBAK="$DC.dpspeedup.orig" DL="/usr/local/emhttp/plugins/dynamix.docker.manager/nchan/docker_load" DLBAK="$DL.dpspeedup.orig" # --- read config (fall back to sane defaults) ------------------------------ TEMPLATE_CACHE=1 STATS_INTERVAL=5 if [ -f "$CFG" ]; then v=$(grep -E '^TEMPLATE_CACHE=' "$CFG" | head -1 | cut -d'"' -f2); [ -n "$v" ] && TEMPLATE_CACHE="$v" v=$(grep -E '^STATS_INTERVAL=' "$CFG" | head -1 | cut -d'"' -f2); [ -n "$v" ] && STATS_INTERVAL="$v" fi case "$STATS_INTERVAL" in ''|*[!0-9]*) STATS_INTERVAL=5;; esac [ "$STATS_INTERVAL" -lt 1 ] && STATS_INTERVAL=1 [ "$STATS_INTERVAL" -gt 60 ] && STATS_INTERVAL=60 # --- 1) DockerClient.php template-read caching (page-load speedup) ---------- if [ -f "$DC" ]; then if [ "$TEMPLATE_CACHE" = "1" ]; then if ! grep -q 'templatesCache' "$DC"; then cp -f "$DC" "$DCBAK" if python3 "$DIR/dockerclient_speedup.py" "$DC" >/dev/null 2>&1 && php -l "$DC" >/dev/null 2>&1; then logger -t "$TAG" "template cache ON (Unraid $(grep -oP 'version="\K[^"]+' /etc/unraid-version 2>/dev/null))" else cp -f "$DCBAK" "$DC"; rm -f "$DCBAK" logger -t "$TAG" "template cache patch failed php -l, rolled back to stock" fi fi else if grep -q 'templatesCache' "$DC" && [ -f "$DCBAK" ]; then cp -f "$DCBAK" "$DC"; rm -f "$DCBAK" logger -t "$TAG" "template cache OFF, restored stock DockerClient.php" fi fi fi # --- 2) docker_load live-stats refresh interval (CPU/GPU load) -------------- # The stock nchan pusher runs `docker stats` on every container every 1s while # the Docker page is open, forcing a full table repaint each second (heavy on # CPU and on the browser GPU). We reset to stock, then set the chosen interval. if [ -f "$DL" ]; then [ -f "$DLBAK" ] || cp -f "$DL" "$DLBAK" # capture pristine stock once (boot = stock) cp -f "$DLBAK" "$DL" # always start from stock so it's re-configurable if [ "$STATS_INTERVAL" -gt 1 ]; then sed -i "s/sleep(1);/sleep(${STATS_INTERVAL});/" "$DL" logger -t "$TAG" "docker stats refresh = ${STATS_INTERVAL}s" else logger -t "$TAG" "docker stats refresh = 1s (stock)" fi # respawn the running pusher (if the page is open) so the change takes effect now. # Only kill the real pusher (a php process running the script), never a shell # that merely references the path (matched by cmdline). Check the actual exe. for p in $(pgrep -f "nchan/docker_load" 2>/dev/null); do [ "$p" = "$$" ] && continue case "$(readlink -f /proc/$p/exe 2>/dev/null)" in */php*) kill "$p" 2>/dev/null;; esac done fi # Boot hardening. Unraid installs plugins very early: on this build the boot-time # run patched DockerClient.php, failed its php -l verification, and rolled back to # stock, leaving the speedup silently off until something re-ran this script. The # same run produced no syslog output at all, which is the tell that the userspace # it needs is not up yet. So if the cache was asked for but is not present when we # finish, retry once after the system settles. DPS_RETRY stops it looping. if [ "$TEMPLATE_CACHE" = "1" ] && [ "${DPS_RETRY:-0}" != "1" ] && ! grep -q 'templatesCache' "$DC" 2>/dev/null; then logger -t "$TAG" "template cache not applied on this pass; retrying in 90s" nohup bash -c "sleep 90; DPS_RETRY=1 bash '$DIR/apply.sh'" >/dev/null 2>&1 & fi exit 0 ]]> /dev/null); do [ "$p" = "$$" ] && continue case "$(readlink -f /proc/$p/exe 2>/dev/null)" in */php*) kill "$p" 2>/dev/null;; esac done exit 0 ]]>
_Page-load speedup_: : > Caches container-template reads in the webGUI (DockerClient.php) so the Docker > page builds far faster on systems with many containers. Disabling immediately > restores the stock file. _Docker stats refresh interval_: : > How often the Docker page refreshes live container CPU/memory stats. The stock > 1-second refresh runs `docker stats` on every container each second and repaints > the whole table, which is heavy on CPU and on the browser's GPU while the page is open. > A higher interval sharply cuts that load. Takes effect on the next Docker page load.   :
]]>
Utilities -> Docker Page Speedup (or the bolt icon)." echo " Re-applied on every boot. syslog tag: docker-page-speedup" echo "----------------------------------------------------" ]]> /dev/null rm -rf /usr/local/emhttp/plugins/docker.page.speedup rm -rf /boot/config/plugins/docker.page.speedup ]]>