Scale progress by fraction of layers that have reported

When pulling Docker images with many layers, tiny layers that complete
immediately would show inflated progress (e.g., 70%) even when most
layers haven't started reporting yet. This made the UI jump to 70%
quickly, then appear stuck during actual download.

The fix scales progress by the fraction of layers that have reported:
- If 2/25 layers report at 70%, progress shows ~5.6% instead of 70%
- As more layers report, progress increases proportionally
- When all layers have reported, no scaling is applied

This ensures progress accurately reflects overall download status rather
than being dominated by a few tiny layers that complete first.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Stefan Agner 2025-11-27 19:54:39 +01:00
parent 2080a2719e
commit 9342456b34
No known key found for this signature in database
GPG Key ID: AE01353D1E44747D
2 changed files with 39 additions and 23 deletions

View File

@ -363,8 +363,15 @@ class DockerInterface(JobGroup, ABC):
# Check if any layers are still pending (no extra yet)
# If so, we're still in downloading phase even if all layers_with_extra are done
layers_pending = len(layer_jobs) - len(layers_with_extra)
if layers_pending > 0 and stage == PullImageLayerStage.PULL_COMPLETE:
stage = PullImageLayerStage.DOWNLOADING
if layers_pending > 0:
# Scale progress to account for unreported layers
# This prevents tiny layers that complete first from showing inflated progress
# e.g., if 2/25 layers reported at 70%, actual progress is ~70 * 2/25 = 5.6%
layers_fraction = len(layers_with_extra) / len(layer_jobs)
progress = progress * layers_fraction
if stage == PullImageLayerStage.PULL_COMPLETE:
stage = PullImageLayerStage.DOWNLOADING
# Also check if all placeholders are done but we're waiting for real layers
if placeholder_layers and stage == PullImageLayerStage.PULL_COMPLETE:

View File

@ -845,28 +845,37 @@ async def test_install_progress_containerd_snapshot(
},
}
assert [c.args[0] for c in ha_ws_client.async_send_command.call_args_list] == [
# During downloading we get continuous progress updates from download status
job_event(0),
job_event(3.4),
job_event(8.5),
job_event(10.2),
job_event(15.3),
job_event(18.8),
job_event(29.0),
job_event(35.8),
job_event(42.6),
job_event(49.5),
job_event(56.0),
job_event(62.8),
# Downloading phase is considered 70% of total. After we only get one update
# per image downloaded when extraction is finished. It uses the total size
# received during downloading to determine percent complete then.
job_event(70.0),
job_event(84.8),
job_event(100),
job_event(100, True),
# Get progress values from the events
job_events = [
c.args[0]
for c in ha_ws_client.async_send_command.call_args_list
if c.args[0].get("data", {}).get("event") == WSEvent.JOB
and c.args[0].get("data", {}).get("data", {}).get("name")
== "mock_docker_interface_install"
]
progress_values = [e["data"]["data"]["progress"] for e in job_events]
# Should have multiple progress updates (not just 0 and 100)
assert len(progress_values) >= 10, (
f"Expected >=10 progress updates, got {len(progress_values)}"
)
# Progress should be monotonically increasing
for i in range(1, len(progress_values)):
assert progress_values[i] >= progress_values[i - 1], (
f"Progress decreased at index {i}: {progress_values[i - 1]} -> {progress_values[i]}"
)
# Should start at 0 and end at 100
assert progress_values[0] == 0
assert progress_values[-1] == 100
# Should have progress values in the downloading phase (< 70%)
# Note: with layer scaling, early progress may be lower than before
downloading_progress = [p for p in progress_values if 0 < p < 70]
assert len(downloading_progress) > 0, (
"Expected progress updates during downloading phase"
)
async def test_install_progress_containerd_snapshotter_real_world(