1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2025-12-19 18:08:40 +00:00

Exclude already-existing layers from pull progress calculation

Layers that already exist locally should not count towards download
progress since there's nothing to download for them. Only layers that
need pulling are included in the progress calculation.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Stefan Agner
2025-12-01 18:24:35 +01:00
parent e7c8700db9
commit 87e1e7a3ab
5 changed files with 39 additions and 35 deletions

View File

@@ -237,9 +237,8 @@ class ImagePullProgress:
def calculate_progress(self) -> float:
"""Calculate overall progress 0-100.
Uses count-based progress where each layer contributes equally.
Each layer's individual progress (0-100) is weighted by 1/total_layers.
This ensures progress never goes backwards when large layers appear late.
Uses count-based progress where each layer that needs pulling contributes
equally. Layers that already exist locally are excluded from the calculation.
Returns 0 until we've seen the first "Downloading" event, since Docker
reports "Already exists" and "Pulling fs layer" events before we know
@@ -250,11 +249,18 @@ class ImagePullProgress:
if not self._seen_downloading or not self.layers:
return 0.0
# Only count layers that need pulling (exclude already_exists)
layers_to_pull = [
layer for layer in self.layers.values() if not layer.already_exists
]
if not layers_to_pull:
# All layers already exist, nothing to download
return 100.0
# Each layer contributes equally: sum of layer progresses / total layers
total_progress = sum(
layer.calculate_progress() for layer in self.layers.values()
)
return total_progress / len(self.layers)
total_progress = sum(layer.calculate_progress() for layer in layers_to_pull)
return total_progress / len(layers_to_pull)
def get_stage(self) -> str | None:
"""Get current stage based on layer states."""

View File

@@ -305,8 +305,8 @@ async def test_api_progress_updates_home_assistant_update(
and evt.args[0]["data"]["event"] == WSEvent.JOB
and evt.args[0]["data"]["data"]["name"] == "home_assistant_core_update"
]
# Count-based progress: 4 layers (2 cached = 50%, 2 pulling = 25% each)
# Cached layers contribute immediately when downloading starts
# Count-based progress: 2 layers need pulling (each worth 50%)
# Layers that already exist are excluded from progress calculation
assert events[:5] == [
{
"stage": None,
@@ -320,34 +320,34 @@ async def test_api_progress_updates_home_assistant_update(
},
{
"stage": None,
"progress": 50.0,
"progress": 9.2,
"done": False,
},
{
"stage": None,
"progress": 54.6,
"progress": 25.6,
"done": False,
},
{
"stage": None,
"progress": 62.8,
"progress": 35.4,
"done": False,
},
]
assert events[-5:] == [
{
"stage": None,
"progress": 95.7,
"progress": 95.5,
"done": False,
},
{
"stage": None,
"progress": 97.1,
"progress": 96.9,
"done": False,
},
{
"stage": None,
"progress": 98.4,
"progress": 98.2,
"done": False,
},
{

View File

@@ -764,8 +764,8 @@ async def test_api_progress_updates_addon_install_update(
and evt.args[0]["data"]["data"]["name"] == job_name
and evt.args[0]["data"]["data"]["reference"] == addon_slug
]
# Count-based progress: 4 layers (2 cached = 50%, 2 pulling = 25% each)
# Cached layers contribute immediately when downloading starts
# Count-based progress: 2 layers need pulling (each worth 50%)
# Layers that already exist are excluded from progress calculation
assert events[:4] == [
{
"stage": None,
@@ -774,34 +774,34 @@ async def test_api_progress_updates_addon_install_update(
},
{
"stage": None,
"progress": 50.0,
"progress": 9.2,
"done": False,
},
{
"stage": None,
"progress": 54.6,
"progress": 25.6,
"done": False,
},
{
"stage": None,
"progress": 62.8,
"progress": 35.4,
"done": False,
},
]
assert events[-5:] == [
{
"stage": None,
"progress": 95.7,
"progress": 95.5,
"done": False,
},
{
"stage": None,
"progress": 97.1,
"progress": 96.9,
"done": False,
},
{
"stage": None,
"progress": 98.4,
"progress": 98.2,
"done": False,
},
{

View File

@@ -358,8 +358,8 @@ async def test_api_progress_updates_supervisor_update(
and evt.args[0]["data"]["event"] == WSEvent.JOB
and evt.args[0]["data"]["data"]["name"] == "supervisor_update"
]
# Count-based progress: 4 layers (2 cached = 50%, 2 pulling = 25% each)
# Cached layers contribute immediately when downloading starts
# Count-based progress: 2 layers need pulling (each worth 50%)
# Layers that already exist are excluded from progress calculation
assert events[:4] == [
{
"stage": None,
@@ -368,34 +368,34 @@ async def test_api_progress_updates_supervisor_update(
},
{
"stage": None,
"progress": 50.0,
"progress": 9.2,
"done": False,
},
{
"stage": None,
"progress": 54.6,
"progress": 25.6,
"done": False,
},
{
"stage": None,
"progress": 62.8,
"progress": 35.4,
"done": False,
},
]
assert events[-5:] == [
{
"stage": None,
"progress": 95.7,
"progress": 95.5,
"done": False,
},
{
"stage": None,
"progress": 97.1,
"progress": 96.9,
"done": False,
},
{
"stage": None,
"progress": 98.4,
"progress": 98.2,
"done": False,
},
{

View File

@@ -591,11 +591,9 @@ class TestImagePullProgress:
)
)
# Count-based: 2 layers total, each = 50%
# cached: 100% (already exists)
# Only 1 layer needs pulling (cached layer excluded)
# pulled: 35% (50% of 70% download weight)
# Total: (100 + 35) / 2 = 67.5%
assert progress.calculate_progress() == pytest.approx(67.5)
assert progress.calculate_progress() == pytest.approx(35.0)
# Complete the pulled layer
progress.process_event(