Skip to content

fix: load the stack list faster - #177

Merged
mstrhakr merged 4 commits into
mstrhakr:devfrom
grantemsley:fix/stack-list-speed
Oct 12, 2026
Merged

mstrhakr merged 4 commits into
mstrhakr:devfrom
grantemsley:fix/stack-list-speed

Conversation

@grantemsley

Copy link
Copy Markdown

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 compose commands that it did not need to:

  • docker compose config --profiles, on every load. getProfiles() caches the stack's profiles in its profiles file, 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 --all for 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 one docker ps filtered on the compose project label, about 30 ms. A row now does the same, through a shared StackInfo::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:

  • An empty profiles list now has the same cache rules a non-empty one already had: isProfilesCacheStale() watches the compose file, the override, the repository override and the env file, but not extra_compose_files, files named by COMPOSE_FILE, or include/extends targets. A stack that gains its first profile only through one of those shows none until a watched file changes.
  • saveProfiles in Exec.php deletes the profiles file for an empty list, while the cache now writes []. Both read back as "no profiles" everywhere.
  • A row for a stack compose cannot read (a broken compose file, say) now shows its real containers instead of "stopped", as the full list already did.

Not in this PR: getStackContainers, which the page also calls for every stack, still runs docker compose ps and a docker inspect per container. It reads fields only docker compose ps gives, so it is not a like-for-like swap.

grantemsley and others added 2 commits October 9, 2026 19:26
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment thread source/compose.manager/include/Util.php
Comment thread source/compose.manager/include/Util.php Outdated
grantemsley and others added 2 commits October 12, 2026 11:28
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>
@grantemsley

Copy link
Copy Markdown
Author

Thanks, both fixed as new commits on top:

  • Tests for container discovery: 0ca9351. The docker stand-in on PATH now prints canned docker ps rows
    and records its arguments. One test groups containers of two projects (the project label isn't always first,
    and com.docker.compose.project.working_dir starts the same way); the other checks a single-project lookup
    asks docker with label=com.docker.compose.project=<name>.
  • Comment wording: 229c847.

@mstrhakr
mstrhakr merged commit 86beea7 into mstrhakr:dev Oct 12, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants