forked from genewildish/Mainline
Major changes: - Pipeline architecture with capability-based dependency resolution - Effects plugin system with performance monitoring - Display abstraction with multiple backends (terminal, null, websocket) - Camera system for viewport scrolling - Sensor framework for real-time input - Command-and-control system via ntfy - WebSocket display backend for browser clients - Comprehensive test suite and documentation Issue #48: ADR for preset scripting language included This commit consolidates 110 individual commits into a single feature integration that can be reviewed and tested before further refinement.
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)
|