forked from genewildish/Mainline
- 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
186 lines
6.5 KiB
Python
186 lines
6.5 KiB
Python
"""Performance regression tests for pipeline stages with realistic data volumes.
|
|
|
|
These tests verify that the pipeline maintains performance with large datasets
|
|
by ensuring ViewportFilterStage prevents FontStage from rendering excessive items.
|
|
|
|
Uses pytest-benchmark for statistical benchmarking with automatic regression detection.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from engine.data_sources.sources import SourceItem
|
|
from engine.pipeline.adapters import FontStage, ViewportFilterStage
|
|
from engine.pipeline.core import PipelineContext
|
|
from engine.pipeline.params import PipelineParams
|
|
|
|
|
|
class TestViewportFilterPerformance:
|
|
"""Test ViewportFilterStage performance with realistic data volumes."""
|
|
|
|
@pytest.mark.benchmark
|
|
def test_filter_2000_items_to_viewport(self, benchmark):
|
|
"""Benchmark: Filter 2000 items to viewport size.
|
|
|
|
Performance threshold: Must complete in < 1ms per iteration
|
|
This tests the filtering overhead is negligible.
|
|
"""
|
|
# Create 2000 test items (more than real headline sources)
|
|
test_items = [
|
|
SourceItem(f"Headline {i}", f"source-{i % 10}", str(i)) for i in range(2000)
|
|
]
|
|
|
|
stage = ViewportFilterStage()
|
|
ctx = PipelineContext()
|
|
ctx.params = PipelineParams(viewport_height=24)
|
|
|
|
result = benchmark(stage.process, test_items, ctx)
|
|
|
|
# Verify result is correct - viewport filter takes first N items
|
|
assert len(result) <= 24 # viewport height
|
|
assert len(result) > 0
|
|
|
|
@pytest.mark.benchmark
|
|
def test_font_stage_with_filtered_items(self, benchmark):
|
|
"""Benchmark: FontStage rendering filtered (5) items.
|
|
|
|
Performance threshold: Must complete in < 50ms per iteration
|
|
This tests that filtering saves significant time by reducing FontStage work.
|
|
"""
|
|
# Create filtered items (what ViewportFilterStage outputs)
|
|
filtered_items = [
|
|
SourceItem(f"Headline {i}", "source", str(i))
|
|
for i in range(5) # Filtered count
|
|
]
|
|
|
|
font_stage = FontStage()
|
|
ctx = PipelineContext()
|
|
ctx.params = PipelineParams()
|
|
|
|
result = benchmark(font_stage.process, filtered_items, ctx)
|
|
|
|
# Should render successfully
|
|
assert result is not None
|
|
assert isinstance(result, list)
|
|
assert len(result) > 0
|
|
|
|
def test_filter_reduces_work_by_288x(self):
|
|
"""Verify ViewportFilterStage achieves expected performance improvement.
|
|
|
|
With 1438 items and 24-line viewport:
|
|
- Without filter: FontStage renders all 1438 items
|
|
- With filter: FontStage renders ~4 items (height-based)
|
|
- Expected improvement: 1438 / 4 ≈ 360x
|
|
"""
|
|
test_items = [
|
|
SourceItem(f"Headline {i}", "source", str(i)) for i in range(1438)
|
|
]
|
|
|
|
stage = ViewportFilterStage()
|
|
ctx = PipelineContext()
|
|
ctx.params = PipelineParams(viewport_height=24)
|
|
|
|
filtered = stage.process(test_items, ctx)
|
|
improvement_factor = len(test_items) / len(filtered)
|
|
|
|
# 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:
|
|
"""Integration tests for full pipeline performance with large datasets."""
|
|
|
|
def test_pipeline_handles_large_item_count(self):
|
|
"""Test that pipeline doesn't hang with 2000+ items due to filtering."""
|
|
# Create large dataset
|
|
large_items = [
|
|
SourceItem(f"Headline {i}", f"source-{i % 5}", str(i)) for i in range(2000)
|
|
]
|
|
|
|
filter_stage = ViewportFilterStage()
|
|
font_stage = FontStage()
|
|
|
|
ctx = PipelineContext()
|
|
ctx.params = PipelineParams(viewport_height=24)
|
|
|
|
# Filter should reduce items quickly
|
|
filtered = filter_stage.process(large_items, ctx)
|
|
assert len(filtered) < len(large_items)
|
|
|
|
# FontStage should process filtered items quickly
|
|
rendered = font_stage.process(filtered, ctx)
|
|
assert rendered is not None
|
|
|
|
def test_multiple_viewports_filter_correctly(self):
|
|
"""Test that filter respects different viewport configurations."""
|
|
large_items = [
|
|
SourceItem(f"Headline {i}", "source", str(i)) for i in range(1000)
|
|
]
|
|
|
|
stage = ViewportFilterStage()
|
|
|
|
# Test different viewport heights
|
|
test_cases = [
|
|
(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 = PipelineParams(viewport_height=viewport_height)
|
|
|
|
filtered = stage.process(large_items, ctx)
|
|
|
|
# Verify filtering is proportional to viewport
|
|
assert len(filtered) <= expected_max_items + 1
|
|
assert len(filtered) > 0
|
|
|
|
|
|
class TestPerformanceRegressions:
|
|
"""Tests that catch common performance regressions."""
|
|
|
|
def test_filter_doesnt_render_all_items(self):
|
|
"""Regression test: Ensure filter doesn't accidentally render all items.
|
|
|
|
This would indicate that ViewportFilterStage is broken or bypassed.
|
|
"""
|
|
large_items = [
|
|
SourceItem(f"Headline {i}", "source", str(i)) for i in range(1438)
|
|
]
|
|
|
|
stage = ViewportFilterStage()
|
|
ctx = PipelineContext()
|
|
ctx.params = PipelineParams()
|
|
|
|
filtered = stage.process(large_items, ctx)
|
|
|
|
# Should NOT have all items (regression detection)
|
|
assert len(filtered) != len(large_items)
|
|
# 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.
|
|
|
|
Previously, FontStage would render all items, causing 10+ second hangs.
|
|
Now it should receive only ~5 items and complete quickly.
|
|
"""
|
|
# Simulate what happens after ViewportFilterStage
|
|
filtered_items = [
|
|
SourceItem(f"Headline {i}", "source", str(i))
|
|
for i in range(5) # What filter outputs
|
|
]
|
|
|
|
font_stage = FontStage()
|
|
ctx = PipelineContext()
|
|
ctx.params = PipelineParams()
|
|
|
|
# Should complete instantly (not hang)
|
|
result = font_stage.process(filtered_items, ctx)
|
|
|
|
# Verify it actually worked
|
|
assert result is not None
|
|
assert isinstance(result, list)
|