Initial commit: in-process object_info cache + refresh UI
ComfyUI-Tenaciousload speeds up ComfyUI page loads for large model/LoRA collections by caching the slow /api/object_info response in memory and on disk (survives restarts) and serving it gzipped in ~milliseconds, instead of rebuilding it (and freezing the event loop) on every load. Adds a "Refresh Models / LoRAs" menu button, a graph node, and a POST /tenaciousload/refresh endpoint to rebuild the cache after adding or removing models. No external dependencies; no nginx/docker required. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
cache/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Ethanfel
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# ComfyUI-Tenaciousload
|
||||||
|
|
||||||
|
Self-contained fix for slow / black-screen ComfyUI loading when you have a huge
|
||||||
|
model/LoRA collection (especially on a network mount). **Just install the pack
|
||||||
|
and restart ComfyUI — no nginx, no docker, no extra port.**
|
||||||
|
|
||||||
|
## The problem
|
||||||
|
ComfyUI's `/api/object_info` enumerates every node's inputs. With thousands of
|
||||||
|
LoRAs (worse on a network mount) it becomes tens of MB and takes **minutes to
|
||||||
|
build on every page load** — and the build **freezes ComfyUI's whole event
|
||||||
|
loop**, so you get a long black screen, worst over a remote network.
|
||||||
|
|
||||||
|
## How this pack fixes it
|
||||||
|
On load it injects an aiohttp **middleware** into ComfyUI that intercepts
|
||||||
|
`/object_info` and `/api/object_info` and:
|
||||||
|
|
||||||
|
- **caches the built response in memory _and_ on disk** (`./cache/`), so it is
|
||||||
|
built once instead of on every load — and the disk copy makes **restarts
|
||||||
|
instant** (no rebuild);
|
||||||
|
- **serves it gzipped in ~milliseconds** (≈85% smaller, independent of any CLI
|
||||||
|
flag);
|
||||||
|
- serves from cache **without running the build**, so the event-loop freeze is
|
||||||
|
gone for normal loads.
|
||||||
|
|
||||||
|
The only time a build runs is the first load after install, or when you
|
||||||
|
explicitly refresh (below).
|
||||||
|
|
||||||
|
## Refreshing after you add / remove models or LoRAs
|
||||||
|
The cache holds the old model lists until you refresh, so new files won't appear
|
||||||
|
until you do one of:
|
||||||
|
|
||||||
|
- **Menu:** `Extensions ▸ 🔄 Refresh Models / LoRAs` (also in the command palette).
|
||||||
|
- **Graph node:** `🔄 Refresh Models/LoRAs (Tenaciousload)` (for automated workflows).
|
||||||
|
- **HTTP:** `POST /tenaciousload/refresh`, then `GET /object_info?nocache=1`.
|
||||||
|
|
||||||
|
A refresh re-walks your model folders (slow over a network mount, ~minutes) — the
|
||||||
|
button shows a "refreshing…" toast meanwhile. Normal loads stay instant.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
**None to install.** Only ComfyUI itself (tested on 0.23.0) and Python ≥ 3.8.
|
||||||
|
Everything used is Python stdlib or already bundled with ComfyUI (`aiohttp`,
|
||||||
|
`folder_paths`, `server`). The web button needs no npm packages.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
Clone (or copy) this repo into your ComfyUI `custom_nodes/` folder and restart
|
||||||
|
ComfyUI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ComfyUI/custom_nodes
|
||||||
|
git clone https://github.com/ethanfel/ComfyUI-Tenaciousload.git
|
||||||
|
# then restart ComfyUI
|
||||||
|
```
|
||||||
|
Nothing to `pip install`. ComfyUI-Manager can also install it from the registry.
|
||||||
|
|
||||||
|
## Verify it's working
|
||||||
|
After restart, load the page once (first time builds + caches), then:
|
||||||
|
```bash
|
||||||
|
curl -s -H 'Accept-Encoding: gzip' -o /dev/null \
|
||||||
|
-w '%{time_total}s | %{size_download} bytes | %header{x-tenaciousload-cache} | %header{content-encoding}\n' \
|
||||||
|
http://127.0.0.1:8188/api/object_info # use your ComfyUI port
|
||||||
|
# expect after the first load: ~0.00Xs | ~10 MB | HIT | gzip
|
||||||
|
```
|
||||||
|
ComfyUI's startup log should show `Tenaciousload: object_info cache middleware installed`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The disk cache lives in `./cache/` (git-ignored). Delete it, or use the refresh
|
||||||
|
button, to force a rebuild.
|
||||||
|
- An nginx reverse proxy can cache `object_info` at the HTTP layer too, but this
|
||||||
|
pack does it in-process so no extra service, container, or port is needed.
|
||||||
|
|
||||||
|
## License
|
||||||
|
MIT — see [LICENSE](LICENSE).
|
||||||
+203
@@ -0,0 +1,203 @@
|
|||||||
|
"""
|
||||||
|
ComfyUI-Tenaciousload
|
||||||
|
=====================
|
||||||
|
Self-contained fix for slow / black-screen ComfyUI loading when you have a huge
|
||||||
|
model/LoRA collection (especially on a network mount).
|
||||||
|
|
||||||
|
The cause: /api/object_info is tens of MB and takes minutes to build on EVERY
|
||||||
|
page load (it re-walks the model folders), and the build blocks ComfyUI's event
|
||||||
|
loop the whole time. This pack installs an in-process caching layer that:
|
||||||
|
|
||||||
|
* caches the built object_info in memory AND on disk (so it survives restarts),
|
||||||
|
* serves it gzipped in ~milliseconds instead of rebuilding every load,
|
||||||
|
* exposes a one-click "Refresh Models / LoRAs" button + graph node to rebuild
|
||||||
|
the cache after you add/remove models.
|
||||||
|
|
||||||
|
No nginx, no docker, no extra port. Just install the pack and restart ComfyUI.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import gzip
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
import folder_paths
|
||||||
|
from server import PromptServer
|
||||||
|
|
||||||
|
log = logging.getLogger("Tenaciousload")
|
||||||
|
|
||||||
|
WEB_DIRECTORY = "./web"
|
||||||
|
|
||||||
|
# --- cache storage --------------------------------------------------------------
|
||||||
|
_CACHE_DIR = os.path.join(os.path.dirname(__file__), "cache")
|
||||||
|
_RAW_PATH = os.path.join(_CACHE_DIR, "object_info.json")
|
||||||
|
_GZ_PATH = os.path.join(_CACHE_DIR, "object_info.json.gz")
|
||||||
|
_OBJECT_INFO_PATHS = ("/object_info", "/api/object_info")
|
||||||
|
_GZIP_LEVEL = 5
|
||||||
|
|
||||||
|
_lock = threading.Lock()
|
||||||
|
_mem = {"raw": None, "gz": None} # bytes / bytes
|
||||||
|
_disk_loaded = False
|
||||||
|
|
||||||
|
|
||||||
|
def _load_from_disk():
|
||||||
|
"""Populate the in-memory cache from disk once (cheap, makes restarts instant)."""
|
||||||
|
global _disk_loaded
|
||||||
|
_disk_loaded = True
|
||||||
|
try:
|
||||||
|
if os.path.exists(_GZ_PATH):
|
||||||
|
with open(_GZ_PATH, "rb") as f:
|
||||||
|
_mem["gz"] = f.read()
|
||||||
|
if os.path.exists(_RAW_PATH):
|
||||||
|
with open(_RAW_PATH, "rb") as f:
|
||||||
|
_mem["raw"] = f.read()
|
||||||
|
if _mem["gz"] is not None and _mem["raw"] is None:
|
||||||
|
_mem["raw"] = gzip.decompress(_mem["gz"])
|
||||||
|
if _mem["raw"] is not None:
|
||||||
|
log.info("Tenaciousload: loaded object_info cache from disk (%d bytes raw)", len(_mem["raw"]))
|
||||||
|
except Exception as e: # pragma: no cover
|
||||||
|
log.warning("Tenaciousload: failed to load disk cache: %s", e)
|
||||||
|
|
||||||
|
|
||||||
|
def _store(raw_bytes):
|
||||||
|
"""Cache a freshly built object_info body (memory + disk)."""
|
||||||
|
with _lock:
|
||||||
|
gz = gzip.compress(raw_bytes, _GZIP_LEVEL)
|
||||||
|
_mem["raw"] = raw_bytes
|
||||||
|
_mem["gz"] = gz
|
||||||
|
try:
|
||||||
|
os.makedirs(_CACHE_DIR, exist_ok=True)
|
||||||
|
with open(_RAW_PATH, "wb") as f:
|
||||||
|
f.write(raw_bytes)
|
||||||
|
with open(_GZ_PATH, "wb") as f:
|
||||||
|
f.write(gz)
|
||||||
|
except Exception as e: # pragma: no cover
|
||||||
|
log.warning("Tenaciousload: failed to persist disk cache: %s", e)
|
||||||
|
log.info("Tenaciousload: cached object_info (%d bytes raw / %d gz)", len(raw_bytes), len(gz))
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_object_info_cache():
|
||||||
|
"""Drop the cached object_info so the next request rebuilds it."""
|
||||||
|
with _lock:
|
||||||
|
_mem["raw"] = None
|
||||||
|
_mem["gz"] = None
|
||||||
|
for p in (_RAW_PATH, _GZ_PATH):
|
||||||
|
try:
|
||||||
|
os.remove(p)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except Exception as e: # pragma: no cover
|
||||||
|
log.warning("Tenaciousload: could not delete %s: %s", p, e)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_comfy_model_cache():
|
||||||
|
"""Force ComfyUI to re-scan every model/LoRA folder on the next build."""
|
||||||
|
try:
|
||||||
|
folder_paths.filename_list_cache.clear()
|
||||||
|
except Exception as e: # pragma: no cover
|
||||||
|
log.warning("Tenaciousload: could not clear filename_list_cache: %s", e)
|
||||||
|
try:
|
||||||
|
folder_paths.cache_helper.clear()
|
||||||
|
except Exception as e: # pragma: no cover
|
||||||
|
log.warning("Tenaciousload: could not clear cache_helper: %s", e)
|
||||||
|
|
||||||
|
|
||||||
|
def _serve_cached(request):
|
||||||
|
raw = _mem["raw"]
|
||||||
|
gz = _mem["gz"]
|
||||||
|
accepts_gzip = "gzip" in request.headers.get("Accept-Encoding", "")
|
||||||
|
if accepts_gzip and gz is not None:
|
||||||
|
return web.Response(
|
||||||
|
body=gz, status=200, content_type="application/json",
|
||||||
|
headers={"Content-Encoding": "gzip", "X-Tenaciousload-Cache": "HIT",
|
||||||
|
"Cache-Control": "no-store"},
|
||||||
|
)
|
||||||
|
if raw is None and gz is not None:
|
||||||
|
raw = gzip.decompress(gz)
|
||||||
|
return web.Response(
|
||||||
|
body=raw, status=200, content_type="application/json",
|
||||||
|
headers={"X-Tenaciousload-Cache": "HIT", "Cache-Control": "no-store"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@web.middleware
|
||||||
|
async def _object_info_cache_mw(request, handler):
|
||||||
|
# Only touch the full object_info list (not /object_info/{node_class}).
|
||||||
|
if request.method != "GET" or request.path not in _OBJECT_INFO_PATHS:
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
|
global _disk_loaded
|
||||||
|
if not _disk_loaded:
|
||||||
|
_load_from_disk()
|
||||||
|
|
||||||
|
nocache = "nocache" in request.query
|
||||||
|
if not nocache and _mem["raw"] is not None:
|
||||||
|
return _serve_cached(request)
|
||||||
|
|
||||||
|
# MISS (or forced refresh): build via the real handler, then cache the body.
|
||||||
|
resp = await handler(request)
|
||||||
|
try:
|
||||||
|
body = getattr(resp, "body", None)
|
||||||
|
if resp.status == 200 and isinstance(body, (bytes, bytearray)) and len(body) > 0:
|
||||||
|
_store(bytes(body))
|
||||||
|
return _serve_cached(request)
|
||||||
|
except Exception as e: # pragma: no cover
|
||||||
|
log.warning("Tenaciousload: caching skipped: %s", e)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
def _install_middleware():
|
||||||
|
try:
|
||||||
|
app = PromptServer.instance.app
|
||||||
|
app.middlewares.insert(0, _object_info_cache_mw)
|
||||||
|
log.info("Tenaciousload: object_info cache middleware installed")
|
||||||
|
except Exception as e:
|
||||||
|
log.error("Tenaciousload: could not install cache middleware (loads will be slow): %s", e)
|
||||||
|
|
||||||
|
|
||||||
|
_install_middleware()
|
||||||
|
|
||||||
|
|
||||||
|
# --- refresh API ----------------------------------------------------------------
|
||||||
|
@PromptServer.instance.routes.post("/tenaciousload/refresh")
|
||||||
|
async def _refresh(request):
|
||||||
|
clear_comfy_model_cache()
|
||||||
|
invalidate_object_info_cache()
|
||||||
|
log.info("Tenaciousload: caches cleared via refresh endpoint")
|
||||||
|
return web.json_response({"status": "ok"})
|
||||||
|
|
||||||
|
|
||||||
|
# --- optional graph node (for workflow automation) ------------------------------
|
||||||
|
class _AnyType(str):
|
||||||
|
def __ne__(self, other):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
ANY = _AnyType("*")
|
||||||
|
|
||||||
|
|
||||||
|
class TenaciousloadRefresh:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {"optional": {"trigger": (ANY, {})}}
|
||||||
|
|
||||||
|
RETURN_TYPES = (ANY,)
|
||||||
|
RETURN_NAMES = ("trigger",)
|
||||||
|
FUNCTION = "refresh"
|
||||||
|
CATEGORY = "Tenaciousload"
|
||||||
|
OUTPUT_NODE = True
|
||||||
|
DESCRIPTION = "Clear ComfyUI's model/LoRA cache and invalidate the object_info cache."
|
||||||
|
|
||||||
|
def refresh(self, trigger=None):
|
||||||
|
clear_comfy_model_cache()
|
||||||
|
invalidate_object_info_cache()
|
||||||
|
return (trigger,)
|
||||||
|
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {"TenaciousloadRefresh": TenaciousloadRefresh}
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {"TenaciousloadRefresh": "🔄 Refresh Models/LoRAs (Tenaciousload)"}
|
||||||
|
|
||||||
|
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[project]
|
||||||
|
name = "comfyui-tenaciousload"
|
||||||
|
description = "Fast ComfyUI loading behind an nginx cache, with an in-UI button to refresh model/LoRA lists."
|
||||||
|
version = "1.0.0"
|
||||||
|
license = { text = "MIT" }
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Repository = "https://github.com/ethanfel/ComfyUI-Tenaciousload"
|
||||||
|
|
||||||
|
[tool.comfy]
|
||||||
|
PublisherId = "ethanfel"
|
||||||
|
DisplayName = "Tenaciousload"
|
||||||
|
Icon = ""
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# ComfyUI-Tenaciousload has NO external dependencies.
|
||||||
|
# Everything it uses is either the Python standard library
|
||||||
|
# (os, gzip, json, logging, threading) or already provided by ComfyUI:
|
||||||
|
# - aiohttp (ComfyUI's web server, aiohttp>=3.11.8)
|
||||||
|
# - folder_paths, server (ComfyUI internal modules)
|
||||||
|
#
|
||||||
|
# Requirements to run:
|
||||||
|
# - ComfyUI (any recent version; tested on 0.23.0)
|
||||||
|
# - Python >= 3.8
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { app } from "../../scripts/app.js";
|
||||||
|
import { api } from "../../scripts/api.js";
|
||||||
|
|
||||||
|
let busy = false;
|
||||||
|
|
||||||
|
function notify(severity, detail, life = 6000) {
|
||||||
|
const toast = app.extensionManager?.toast;
|
||||||
|
if (toast?.add) {
|
||||||
|
toast.add({ severity, summary: "Tenaciousload", detail, life });
|
||||||
|
} else if (severity === "error") {
|
||||||
|
alert("Tenaciousload: " + detail);
|
||||||
|
}
|
||||||
|
console[severity === "error" ? "error" : "log"]("[Tenaciousload]", detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doRefresh() {
|
||||||
|
if (busy) {
|
||||||
|
notify("warn", "A refresh is already running…");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
busy = true;
|
||||||
|
notify("info", "Refreshing models/LoRAs — scanning over the network, this can take a few minutes…", 10000);
|
||||||
|
try {
|
||||||
|
// 1) clear ComfyUI's folder cache + drop the object_info cache.
|
||||||
|
await api.fetchApi("/tenaciousload/refresh", { method: "POST" });
|
||||||
|
// 2) force a fresh object_info build and re-cache it (this is the slow step).
|
||||||
|
await api.fetchApi("/object_info?nocache=1").then((r) => r.arrayBuffer());
|
||||||
|
// 3) update open node dropdowns live if the frontend supports it.
|
||||||
|
try { await app.refreshComboInNodes?.(); } catch (e) { /* non-fatal */ }
|
||||||
|
notify("success", "Done — new models are now available (reload the page if a dropdown still looks stale).", 8000);
|
||||||
|
} catch (e) {
|
||||||
|
notify("error", "Refresh failed: " + (e?.message || e), 10000);
|
||||||
|
} finally {
|
||||||
|
busy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.registerExtension({
|
||||||
|
name: "Tenaciousload.Refresh",
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
id: "Tenaciousload.refresh",
|
||||||
|
label: "🔄 Refresh Models / LoRAs",
|
||||||
|
icon: "pi pi-refresh",
|
||||||
|
function: doRefresh,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Adds the command under the top "Extensions" menu (and the command palette).
|
||||||
|
menuCommands: [
|
||||||
|
{
|
||||||
|
path: ["Extensions"],
|
||||||
|
commands: ["Tenaciousload.refresh"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user