perf(backend): parallelize pruneManagedOnly removals (#830)

DockerController.pruneManagedOnly removed managed volumes,
networks, and images one at a time inside a serial for-await loop.
Each remove call hits the Docker daemon over the Unix socket with a
synchronous-from-the-caller's-perspective HTTP round-trip, so a
prune over N items took the sum of N round-trips. The daemon
handles concurrent removes fine for these resource types.

Wrap each loop in Promise.all so wall time tracks the slowest
single remove rather than the sum. The existing per-item try/catch
keeps the partial-failure semantics: a single resource that fails
to delete logs and continues; the rest still get removed.
JavaScript single-threading makes the shared reclaimedBytes
counter safe under the parallel awaits.
This commit is contained in:
Anso
2026-04-28 10:26:49 -04:00
committed by GitHub
parent 2000653fb4
commit 46fae21e67
+9 -6
View File
@@ -334,26 +334,29 @@ class DockerController {
return !!DockerController.resolveProjectLabel(v.Labels?.['com.docker.compose.project'], knownSet, projectToStack)
&& (v.UsageData?.RefCount ?? 1) === 0;
});
for (const vol of prunable) {
// Removals are independent and Docker handles concurrent volume
// deletes; parallelize so wall time matches the slowest single
// remove rather than the sum of all of them.
await Promise.all(prunable.map(async (vol) => {
try {
await this.docker.getVolume(vol.Name).remove({ force: true });
reclaimedBytes += vol.UsageData?.Size ?? 0;
} catch (e) {
console.error(`[pruneManagedOnly] Failed to remove volume ${vol.Name}:`, e);
}
}
}));
} else if (target === 'networks') {
const rawNetworks = await this.docker.listNetworks();
const prunable = (rawNetworks as any[]).filter((n: any) => {
return !!DockerController.resolveProjectLabel(n.Labels?.['com.docker.compose.project'], knownSet, projectToStack);
});
for (const net of prunable) {
await Promise.all(prunable.map(async (net) => {
try {
await this.docker.getNetwork(net.Id).remove({ force: true });
} catch (e) {
console.error(`[pruneManagedOnly] Failed to remove network ${net.Name}:`, e);
}
}
}));
} else if (target === 'images') {
const allContainers = await this.docker.listContainers({ all: true });
const resolvedBase = path.resolve(COMPOSE_DIR);
@@ -369,14 +372,14 @@ class DockerController {
const prunable = (rawImages as any[]).filter((img: any) =>
img.Containers === 0 && !unmanagedImageIds.has(img.Id)
);
for (const img of prunable) {
await Promise.all(prunable.map(async (img) => {
try {
await this.docker.getImage(img.Id).remove({ force: true });
reclaimedBytes += img.Size ?? 0;
} catch (e) {
console.error(`[pruneManagedOnly] Failed to remove image ${img.Id}:`, e);
}
}
}));
}
return { success: true, reclaimedBytes };