Files
ComfyUI-Tenaciousload/README.md
T
Ethanfel 85552d8b25 fix: single-flight object_info build (prevents concurrent-build hang)
The off-loop (threaded) build introduced a concurrency bug: ComfyUI's
cache_helper is a global, so a manual refresh (R) fired while a rebuild was
still running started a SECOND build; when the first finished it cleared the
shared cache_helper, making the second re-walk the CIFS mount per-node = hang.

Now an asyncio lock serialises builds: concurrent object_info requests wait for
the in-flight build and serve its result instead of starting another. Verified:
3 concurrent requests -> exactly one build.

Docs: note that Quick refresh detects changes by directory mtime, which network
mounts (cache=loose CIFS) can report stale/coarse, so it may miss a brand-new
file -- use Full refresh for just-added models.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 09:27:10 +02:00

145 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<p align="center">
<img src="assets/banner.svg" alt="ComfyUI-Tenaciousload" width="100%">
</p>
# 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
<p align="center">
<img src="assets/how-it-works.svg" alt="How it works: requests are served straight from an in-process cache; the slow build only runs on a miss or refresh" width="100%">
</p>
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** (≈85% smaller transfer, independent of any CLI flag),
straight from cache **without running the build**;
- because the build never runs on a normal load, the event-loop freeze (and the
long black screen) is gone — page loads drop from **minutes to seconds**.
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. Three modes are available
from the **`Extensions`** menu (and the command palette):
| Mode | What it does | Speed |
|------|--------------|-------|
| ⚡ **Quick refresh** | Re-walks only the folders whose timestamp **changed** since the last scan; reuses the cache for the rest. Catches new / removed / renamed files. | Fast on local disks; **~2× faster** on a slow network mount (it still has to stat every folder to find which changed). |
| 🔄 **Full refresh** | Clears ComfyUI's folder cache and re-walks **everything**, ignoring timestamps. Catches moves/deletes anywhere. **Use this for files you *just* added.** | Slowest (the original behaviour). |
| **Register new file…** | You give it the path(s) of the file(s) you just added; it appends them to the cache with **no folder walk**. | Instant disk-wise — only the `object_info` rebuild remains. |
Also available:
- **Graph node** `🔄 Refresh Models/LoRAs (Tenaciousload)` with a `mode` widget
(`quick` / `full`), for automated workflows.
- **HTTP:** `POST /tenaciousload/refresh` with
`{"mode": "quick" | "full" | "register", "folder": "loras", "files": ["pack/new.safetensors"]}`,
then `GET /object_info?nocache=1`.
> The **first** Quick refresh after install builds a folder index (one full walk),
> so it's as slow as a Full refresh that one time; every Quick refresh after that
> is incremental. The index is saved to `./cache/scan_snapshot.json`.
> **Network mounts (CIFS/SMB/NFS):** Quick refresh detects changes by directory
> timestamp, which network filesystems can report with a delay or coarse
> resolution (e.g. a `cache=loose` CIFS mount), so it may *occasionally miss a
> brand-new file*. If a just-added model doesn't show up after a Quick refresh,
> use **Full refresh** — it re-walks everything and doesn't rely on timestamps.
Whichever mode you pick, the button shows a "refreshing…" toast and 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`.
## Recommended: gzip the rest of ComfyUI
This pack already gzips the cached `object_info` on its own. To **also** gzip
everything else ComfyUI serves — most importantly the *hundreds* of frontend
extension scripts, plus the other API responses — launch ComfyUI with its
built-in compression flag:
```bash
python main.py --listen --port 8188 --enable-compress-response-body
```
- It's a **stock ComfyUI** option (defined in `comfy/cli_args.py`), not part of
this pack, and it's **optional** — Tenaciousload works fine without it.
- It's strongly recommended for **remote access**: those extension scripts are
many small requests that compress very well, so the flag noticeably cuts the
total transfer on top of the `object_info` cache.
- It costs a little CPU per response to compress; on a fast machine this is
negligible compared to the bytes saved over the network.
## Notes
- **Loading status:** instead of ComfyUI's silent "Comfy" splash, a small status
line shows whether it's *serving from cache* or *building* (with node count +
elapsed time), so a long rebuild isn't a black screen with no feedback. It
removes itself once the app is ready. Status is also at `GET /tenaciousload/status`.
- 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.
- **New files in the `input` folder** are picked up by a **refresh button** by
default (same as new models) — they do *not* trigger an automatic rebuild on
restart. To auto-detect them on restart instead, set
`TENACIOUSLOAD_WATCH_INPUT=1`. Only do this if your input folder is fairly
static: on a busy input folder (e.g. a video workflow that adds clips
constantly) the input dir changes on nearly every restart, which would
invalidate the cache and force a slow rebuild each time — defeating the point.
## Installing / updating other custom nodes
This pack is a quiet neighbour:
- **No dependencies** — its `requirements.txt` is empty, so it can't cause the
pip version conflicts that break other nodes' installs/updates. It also never
touches other nodes' files or the ComfyUI-Manager installer.
- **Auto-detects node changes** — it fingerprints the installed node set
(`NODE_CLASS_MAPPINGS`) and, on the first page load after a restart, drops the
cache automatically if a node was **installed, updated, enabled or removed**
so new nodes appear with no manual refresh.
- The only thing it gates is the full `/object_info` list (a cached snapshot);
it passes every other request straight through, so other nodes' own routes,
sidebars and refresh buttons are unaffected. For an in-place node tweak that
changes an existing node's inputs *without* adding/removing a node class, use a
refresh button.
## License
MIT — see [LICENSE](LICENSE).