fix(performance): use simple height estimation instead of PIL rendering

- Replace estimate_block_height (PIL-based) with estimate_simple_height (word wrap)
- Update viewport filter tests to match new height-based filtering (~4 items vs 24)
- Fix CI task duplication in mise.toml (remove redundant depends)

Closes #38
Closes #36
This commit is contained in:
2026-03-18 22:33:36 -07:00
parent abe49ba7d7
commit c57617bb3d
26 changed files with 3938 additions and 1956 deletions

View File

@@ -11,14 +11,7 @@ import pytest
from engine.data_sources.sources import SourceItem
from engine.pipeline.adapters import FontStage, ViewportFilterStage
from engine.pipeline.core import PipelineContext
class MockParams:
"""Mock parameters object for testing."""
def __init__(self, viewport_width: int = 80, viewport_height: int = 24):
self.viewport_width = viewport_width
self.viewport_height = viewport_height
from engine.pipeline.params import PipelineParams
class TestViewportFilterPerformance:
@@ -38,12 +31,12 @@ class TestViewportFilterPerformance:
stage = ViewportFilterStage()
ctx = PipelineContext()
ctx.params = MockParams(viewport_height=24)
ctx.params = PipelineParams(viewport_height=24)
result = benchmark(stage.process, test_items, ctx)
# Verify result is correct
assert len(result) <= 5
# Verify result is correct - viewport filter takes first N items
assert len(result) <= 24 # viewport height
assert len(result) > 0
@pytest.mark.benchmark
@@ -61,7 +54,7 @@ class TestViewportFilterPerformance:
font_stage = FontStage()
ctx = PipelineContext()
ctx.params = MockParams()
ctx.params = PipelineParams()
result = benchmark(font_stage.process, filtered_items, ctx)
@@ -75,8 +68,8 @@ class TestViewportFilterPerformance:
With 1438 items and 24-line viewport:
- Without filter: FontStage renders all 1438 items
- With filter: FontStage renders ~3 items (layout-based)
- Expected improvement: 1438 / 3479x
- With filter: FontStage renders ~4 items (height-based)
- Expected improvement: 1438 / 4360x
"""
test_items = [
SourceItem(f"Headline {i}", "source", str(i)) for i in range(1438)
@@ -84,15 +77,15 @@ class TestViewportFilterPerformance:
stage = ViewportFilterStage()
ctx = PipelineContext()
ctx.params = MockParams(viewport_height=24)
ctx.params = PipelineParams(viewport_height=24)
filtered = stage.process(test_items, ctx)
improvement_factor = len(test_items) / len(filtered)
# Verify we get expected ~479x improvement (better than old ~288x)
assert 400 < improvement_factor < 600
# Verify filtered count is reasonable (layout-based is more precise)
assert 2 <= len(filtered) <= 5
# Verify we get significant improvement (height-based filtering)
assert 300 < improvement_factor < 500
# Verify filtered count is ~4 (24 viewport / 6 rows per item)
assert len(filtered) == 4
class TestPipelinePerformanceWithRealData:
@@ -109,7 +102,7 @@ class TestPipelinePerformanceWithRealData:
font_stage = FontStage()
ctx = PipelineContext()
ctx.params = MockParams(viewport_height=24)
ctx.params = PipelineParams(viewport_height=24)
# Filter should reduce items quickly
filtered = filter_stage.process(large_items, ctx)
@@ -129,14 +122,14 @@ class TestPipelinePerformanceWithRealData:
# Test different viewport heights
test_cases = [
(12, 3), # 12px height -> ~3 items
(24, 5), # 24px height -> ~5 items
(48, 9), # 48px height -> ~9 items
(12, 12), # 12px height -> 12 items
(24, 24), # 24px height -> 24 items
(48, 48), # 48px height -> 48 items
]
for viewport_height, expected_max_items in test_cases:
ctx = PipelineContext()
ctx.params = MockParams(viewport_height=viewport_height)
ctx.params = PipelineParams(viewport_height=viewport_height)
filtered = stage.process(large_items, ctx)
@@ -159,14 +152,14 @@ class TestPerformanceRegressions:
stage = ViewportFilterStage()
ctx = PipelineContext()
ctx.params = MockParams()
ctx.params = PipelineParams()
filtered = stage.process(large_items, ctx)
# Should NOT have all items (regression detection)
assert len(filtered) != len(large_items)
# Should have drastically fewer items
assert len(filtered) < 10
# With height-based filtering, ~4 items fit in 24-row viewport (6 rows/item)
assert len(filtered) == 4
def test_font_stage_doesnt_hang_with_filter(self):
"""Regression test: FontStage shouldn't hang when receiving filtered data.
@@ -182,7 +175,7 @@ class TestPerformanceRegressions:
font_stage = FontStage()
ctx = PipelineContext()
ctx.params = MockParams()
ctx.params = PipelineParams()
# Should complete instantly (not hang)
result = font_stage.process(filtered_items, ctx)