mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-12-20 10:28:45 +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:
@@ -237,9 +237,8 @@ class ImagePullProgress:
|
|||||||
def calculate_progress(self) -> float:
|
def calculate_progress(self) -> float:
|
||||||
"""Calculate overall progress 0-100.
|
"""Calculate overall progress 0-100.
|
||||||
|
|
||||||
Uses count-based progress where each layer contributes equally.
|
Uses count-based progress where each layer that needs pulling contributes
|
||||||
Each layer's individual progress (0-100) is weighted by 1/total_layers.
|
equally. Layers that already exist locally are excluded from the calculation.
|
||||||
This ensures progress never goes backwards when large layers appear late.
|
|
||||||
|
|
||||||
Returns 0 until we've seen the first "Downloading" event, since Docker
|
Returns 0 until we've seen the first "Downloading" event, since Docker
|
||||||
reports "Already exists" and "Pulling fs layer" events before we know
|
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:
|
if not self._seen_downloading or not self.layers:
|
||||||
return 0.0
|
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
|
# Each layer contributes equally: sum of layer progresses / total layers
|
||||||
total_progress = sum(
|
total_progress = sum(layer.calculate_progress() for layer in layers_to_pull)
|
||||||
layer.calculate_progress() for layer in self.layers.values()
|
return total_progress / len(layers_to_pull)
|
||||||
)
|
|
||||||
return total_progress / len(self.layers)
|
|
||||||
|
|
||||||
def get_stage(self) -> str | None:
|
def get_stage(self) -> str | None:
|
||||||
"""Get current stage based on layer states."""
|
"""Get current stage based on layer states."""
|
||||||
|
|||||||
@@ -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"]["event"] == WSEvent.JOB
|
||||||
and evt.args[0]["data"]["data"]["name"] == "home_assistant_core_update"
|
and evt.args[0]["data"]["data"]["name"] == "home_assistant_core_update"
|
||||||
]
|
]
|
||||||
# Count-based progress: 4 layers (2 cached = 50%, 2 pulling = 25% each)
|
# Count-based progress: 2 layers need pulling (each worth 50%)
|
||||||
# Cached layers contribute immediately when downloading starts
|
# Layers that already exist are excluded from progress calculation
|
||||||
assert events[:5] == [
|
assert events[:5] == [
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
@@ -320,34 +320,34 @@ async def test_api_progress_updates_home_assistant_update(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
"progress": 50.0,
|
"progress": 9.2,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
"progress": 54.6,
|
"progress": 25.6,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
"progress": 62.8,
|
"progress": 35.4,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
assert events[-5:] == [
|
assert events[-5:] == [
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
"progress": 95.7,
|
"progress": 95.5,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
"progress": 97.1,
|
"progress": 96.9,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
"progress": 98.4,
|
"progress": 98.2,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -761,8 +761,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"]["name"] == job_name
|
||||||
and evt.args[0]["data"]["data"]["reference"] == addon_slug
|
and evt.args[0]["data"]["data"]["reference"] == addon_slug
|
||||||
]
|
]
|
||||||
# Count-based progress: 4 layers (2 cached = 50%, 2 pulling = 25% each)
|
# Count-based progress: 2 layers need pulling (each worth 50%)
|
||||||
# Cached layers contribute immediately when downloading starts
|
# Layers that already exist are excluded from progress calculation
|
||||||
assert events[:4] == [
|
assert events[:4] == [
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
@@ -771,34 +771,34 @@ async def test_api_progress_updates_addon_install_update(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
"progress": 50.0,
|
"progress": 9.2,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
"progress": 54.6,
|
"progress": 25.6,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
"progress": 62.8,
|
"progress": 35.4,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
assert events[-5:] == [
|
assert events[-5:] == [
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
"progress": 95.7,
|
"progress": 95.5,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
"progress": 97.1,
|
"progress": 96.9,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
"progress": 98.4,
|
"progress": 98.2,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -359,8 +359,8 @@ async def test_api_progress_updates_supervisor_update(
|
|||||||
and evt.args[0]["data"]["event"] == WSEvent.JOB
|
and evt.args[0]["data"]["event"] == WSEvent.JOB
|
||||||
and evt.args[0]["data"]["data"]["name"] == "supervisor_update"
|
and evt.args[0]["data"]["data"]["name"] == "supervisor_update"
|
||||||
]
|
]
|
||||||
# Count-based progress: 4 layers (2 cached = 50%, 2 pulling = 25% each)
|
# Count-based progress: 2 layers need pulling (each worth 50%)
|
||||||
# Cached layers contribute immediately when downloading starts
|
# Layers that already exist are excluded from progress calculation
|
||||||
assert events[:4] == [
|
assert events[:4] == [
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
@@ -369,34 +369,34 @@ async def test_api_progress_updates_supervisor_update(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
"progress": 50.0,
|
"progress": 9.2,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
"progress": 54.6,
|
"progress": 25.6,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
"progress": 62.8,
|
"progress": 35.4,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
assert events[-5:] == [
|
assert events[-5:] == [
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
"progress": 95.7,
|
"progress": 95.5,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
"progress": 97.1,
|
"progress": 96.9,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": None,
|
"stage": None,
|
||||||
"progress": 98.4,
|
"progress": 98.2,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -591,11 +591,9 @@ class TestImagePullProgress:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Count-based: 2 layers total, each = 50%
|
# Only 1 layer needs pulling (cached layer excluded)
|
||||||
# cached: 100% (already exists)
|
|
||||||
# pulled: 35% (50% of 70% download weight)
|
# pulled: 35% (50% of 70% download weight)
|
||||||
# Total: (100 + 35) / 2 = 67.5%
|
assert progress.calculate_progress() == pytest.approx(35.0)
|
||||||
assert progress.calculate_progress() == pytest.approx(67.5)
|
|
||||||
|
|
||||||
# Complete the pulled layer
|
# Complete the pulled layer
|
||||||
progress.process_event(
|
progress.process_event(
|
||||||
|
|||||||
Reference in New Issue
Block a user