- Rename VERTICAL camera mode to FEED (rapid single-item view) - Add SCROLL camera mode with float accumulation for smooth movie-credits style scrolling - Add estimate_block_height() for cheap layout calculation without full rendering - Replace ViewportFilterStage with layout-aware filtering that tracks camera position - Add render caching to FontStage to avoid re-rendering items - Fix CameraStage to use global canvas height for scrolling bounds - Add horizontal padding in Camera.apply() to prevent ghosting - Add get_dimensions() to MultiDisplay for proper viewport sizing - Fix PygameDisplay to auto-detect viewport from window size - Update presets to use scroll camera with appropriate speeds
193 lines
6.6 KiB
Python
193 lines
6.6 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
|
|
|
|
|
|
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
|
|
|
|
|
|
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 = MockParams(viewport_height=24)
|
|
|
|
result = benchmark(stage.process, test_items, ctx)
|
|
|
|
# Verify result is correct
|
|
assert len(result) <= 5
|
|
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 = MockParams()
|
|
|
|
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 ~3 items (layout-based)
|
|
- Expected improvement: 1438 / 3 ≈ 479x
|
|
"""
|
|
test_items = [
|
|
SourceItem(f"Headline {i}", "source", str(i)) for i in range(1438)
|
|
]
|
|
|
|
stage = ViewportFilterStage()
|
|
ctx = PipelineContext()
|
|
ctx.params = MockParams(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
|
|
|
|
|
|
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 = MockParams(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, 3), # 12px height -> ~3 items
|
|
(24, 5), # 24px height -> ~5 items
|
|
(48, 9), # 48px height -> ~9 items
|
|
]
|
|
|
|
for viewport_height, expected_max_items in test_cases:
|
|
ctx = PipelineContext()
|
|
ctx.params = MockParams(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 = MockParams()
|
|
|
|
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
|
|
|
|
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 = MockParams()
|
|
|
|
# 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)
|