Repository navigation
fix: load the stack list faster - #177
Merged
Merged
Conversation
getProfiles() caches the profiles docker compose config --profiles reports in the stack's profiles file, but only when there are some. For a stack without profiles (most stacks) the command printed nothing, nothing was cached, and it ran again on every read: on every load of the stack list, once per stack, about 0.1 s each. An empty answer is now cached too. A command that fails (compose cannot read the stack, or docker is missing) is still not cached, so the next read asks again; the exit status now tells the two apart, where an empty output did not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stack list loads its rows one by one (ComposeList.php mode=row), and each row asked docker compose ps --all for the stack's containers. Compose reads the stack's compose files first, so that took 0.1 to 0.15 s per stack on every load of the page. The full list (StackInfo::allFromRoot()) already uses one docker ps call filtered on the compose project label, which takes about 30 ms. A row now gets its containers the same way, through StackInfo::containersByProject(), which allFromRoot() uses too. The rows are the same docker ps rows the full list renders from. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new container-discovery path needs automated coverage for filtering and label grouping.
2 open findings
What changed in this PR
Optimizes stack-list loading by caching empty profile results and using filtered docker ps data.
Changes:
- Cache successful empty profile queries.
- Reuse project-filtered container discovery.
- Add profile caching and failure tests.
| File | Description |
|---|---|
tests/unit/StackInfoTest.php |
Tests profile caching behavior. |
source/compose.manager/include/Util.php |
Adds profile caching and container lookup optimization. |
source/compose.manager/include/ComposeList.php |
Uses the optimized lookup for individual rows. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The stack list's rows get their containers from StackInfo::containersByProject(), but the existing tests hand StackInfo its rows with setContainerList(), so nothing checked the docker ps call itself. A mistake in the label filter or in reading the project from Labels would show every affected stack as stopped. The docker stand-in on PATH now prints given output and records each call's arguments. Two tests use it: one with containers of two projects, where the project label is not always first and another compose label starts the same way, and one asking for a single project. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
|
Thanks, both fixed as new commits on top:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Testing the upcoming UI changes for git stacks was driving me crazy with the page taking 10 seconds or more to load every time with 18 stacks running. Claude and I (mostly Claude) dug into why, and found a way to make it about 50% faster. There's more optimization that could be done (noted at the end), but this was a quick and easy win.
The Compose page loads its stack rows one by one, and each row ran two
docker composecommands that it did not need to:docker compose config --profiles, on every load.getProfiles()caches the stack's profiles in itsprofilesfile, but only when it has some. For a stack without profiles (most stacks) nothing was cached, so the command ran again for every stack on every page load, about 0.1 s each. An empty answer is now cached too; a command that fails is still not cached, and the exit status now tells the two apart.docker compose ps --allfor each row. Compose reads the stack's files before it lists anything, so this took 0.1 to 0.15 s per stack. The full list (StackInfo::allFromRoot()) already gets containers from onedocker psfiltered on the compose project label, about 30 ms. A row now does the same, through a sharedStackInfo::containersByProject(), so it renders from the same rows the full list does.Measured on an Unraid 7.3.2 test server with 18 stacks: the page's own "compose list ready" time went from about 6 s to about 3.3 s (three loads each, after the first load had written the caches).
Reading it: one commit per cause, each with its reason in the message. The first adds two tests: a stack without profiles asks compose once and caches
[], and a failing compose is asked again.Worth knowing:
isProfilesCacheStale()watches the compose file, the override, the repository override and the env file, but notextra_compose_files, files named byCOMPOSE_FILE, orinclude/extendstargets. A stack that gains its first profile only through one of those shows none until a watched file changes.saveProfilesinExec.phpdeletes theprofilesfile for an empty list, while the cache now writes[]. Both read back as "no profiles" everywhere.Not in this PR:
getStackContainers, which the page also calls for every stack, still runsdocker compose psand adocker inspectper container. It reads fields onlydocker compose psgives, so it is not a like-for-like swap.