docs: Create comprehensive performance optimization plan
- Research Textual best practices and performance guidelines - Analyze current mail app performance issues - Create 4-week implementation plan - Define success metrics for performance targets - Focus on: compose() pattern, lazy loading, reactive properties, caching - Include testing strategy and benchmarking approach Key areas to address: - Widget mounting and composition (use compose() instead of manual mounting) - Lazy loading for envelopes (defer expensive operations) - Reactive property updates (avoid manual rebuilds) - Efficient list rendering (use ListView properly) - Background workers for content fetching (use @work decorator) - Memoization for expensive operations - Code cleanup (unused imports, type safety) - Advanced optimizations (virtual scrolling, debouncing, widget pooling) Estimated improvements: - 70-90% faster startup time - 60-90% faster navigation - 70% reduction in memory usage - Smooth 60 FPS rendering Research sources: - Textual blog: 7 Things I've Learned - Textual algorithms for high performance apps - Python performance guides from fyld and Analytics Vidhya - Textual widget documentation and examples
This commit is contained in:
808
PERFORMANCE_OPTIMIZATION_PLAN.md
Normal file
808
PERFORMANCE_OPTIMIZATION_PLAN.md
Normal file
@@ -0,0 +1,808 @@
|
|||||||
|
# LUK Performance Optimization & Cleanup Plan
|
||||||
|
|
||||||
|
**Created:** 2025-12-28
|
||||||
|
**Priority:** High
|
||||||
|
**Focus:** Mail app performance optimization and code quality improvements
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
The LUK mail app is experiencing performance issues:
|
||||||
|
- **Slow rendering** when scrolling through messages
|
||||||
|
- **Laggy navigation** between messages
|
||||||
|
- **High memory usage** during extended use
|
||||||
|
- **Flickering** or unresponsive UI
|
||||||
|
- **Poor startup time**
|
||||||
|
|
||||||
|
These issues make the app difficult to use for daily email management.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Research: Textual Best Practices
|
||||||
|
|
||||||
|
### Key Principles for High-Performance Textual Apps
|
||||||
|
|
||||||
|
#### 1. **Use `compose()` Method, Not Manual Mounting**
|
||||||
|
```python
|
||||||
|
# ❌ BAD: Manual mounting in on_mount()
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
self.mount(Header())
|
||||||
|
self.mount(Sidebar())
|
||||||
|
self.mount(Content())
|
||||||
|
self.mount(Footer())
|
||||||
|
|
||||||
|
# ✅ GOOD: Use compose() for declarative UI
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Header()
|
||||||
|
with Horizontal():
|
||||||
|
yield Sidebar()
|
||||||
|
yield Content()
|
||||||
|
yield Footer()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** `compose()` is called once and builds the widget tree efficiently. Manual mounting triggers multiple render cycles.
|
||||||
|
|
||||||
|
#### 2. **Lazy Load Content - Defer Until Needed**
|
||||||
|
```python
|
||||||
|
# ❌ BAD: Load everything at startup
|
||||||
|
class MailApp(App):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.all_envelopes = load_all_envelopes() # Expensive!
|
||||||
|
self.message_store = build_full_message_store() # Expensive!
|
||||||
|
|
||||||
|
# ✅ GOOD: Load on-demand with workers
|
||||||
|
class MailApp(App):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._envelopes_cache = []
|
||||||
|
self._loading = False
|
||||||
|
|
||||||
|
@work(exclusive=True)
|
||||||
|
async def load_envelopes_lazy(self):
|
||||||
|
if not self._envelopes_cache:
|
||||||
|
envelopes = await fetch_envelopes() # Load in background
|
||||||
|
self._envelopes_cache = envelopes
|
||||||
|
self._update_list()
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
self.load_envelopes_lazy()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Defers expensive operations until the app is ready and visible.
|
||||||
|
|
||||||
|
#### 3. **Use Reactive Properties Efficiently**
|
||||||
|
```python
|
||||||
|
# ❌ BAD: Re-compute values in methods
|
||||||
|
def action_next(self):
|
||||||
|
index = self.envelopes.index(self.current_envelope)
|
||||||
|
self.current_message_index = index + 1 # Triggers re-render
|
||||||
|
self.update_envelope_list_view() # Another re-render
|
||||||
|
|
||||||
|
# ✅ GOOD: Use reactive for automatic UI updates
|
||||||
|
current_message_index: reactive[int] = reactive(-1)
|
||||||
|
|
||||||
|
@reactive_var.on_change
|
||||||
|
def action_next(self):
|
||||||
|
# Automatically triggers minimal re-render
|
||||||
|
self.current_message_index += 1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Textual's reactive system only updates changed widgets, not the entire app.
|
||||||
|
|
||||||
|
#### 4. **Avoid String Concatenation in Loops for Updates**
|
||||||
|
```python
|
||||||
|
# ❌ BAD: Creates new strings every time
|
||||||
|
def update_status(self):
|
||||||
|
text = "Status: "
|
||||||
|
for i, item in enumerate(items):
|
||||||
|
text += f"{i+1}. {item.name}\n" # O(n²) string operations
|
||||||
|
self.status.update(text)
|
||||||
|
|
||||||
|
# ✅ GOOD: Build list once
|
||||||
|
def update_status(self):
|
||||||
|
lines = [f"{i+1}. {item.name}" for i, item in enumerate(items)]
|
||||||
|
text = "\n".join(lines) # O(n) operations
|
||||||
|
self.status.update(text)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** String concatenation is O(n²), while join is O(n).
|
||||||
|
|
||||||
|
#### 5. **Use Efficient List Widgets**
|
||||||
|
```python
|
||||||
|
# ❌ BAD: Creating custom widget for each item
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
def create_mail_list(items):
|
||||||
|
for item in items:
|
||||||
|
yield Static(item.subject) # N widgets = N render cycles
|
||||||
|
|
||||||
|
# ✅ GOOD: Use ListView with data binding
|
||||||
|
from textual.widgets import ListView, ListItem
|
||||||
|
|
||||||
|
class MailApp(App):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield ListView(id="envelopes_list")
|
||||||
|
|
||||||
|
def update_list(self, items: list):
|
||||||
|
list_view = self.query_one("#envelopes_list", ListView)
|
||||||
|
list_view.clear()
|
||||||
|
list_view.extend([ListItem(item.subject) for item in items]) # Efficient
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** `ListView` is optimized for lists with virtualization and pooling.
|
||||||
|
|
||||||
|
#### 6. **Debounce Expensive Operations**
|
||||||
|
```python
|
||||||
|
from textual.timer import Timer
|
||||||
|
|
||||||
|
# ❌ BAD: Update on every keypress
|
||||||
|
def action_search(self, query: str):
|
||||||
|
results = self.search_messages(query) # Expensive
|
||||||
|
self.update_results(results)
|
||||||
|
|
||||||
|
# ✅ GOOD: Debounce search
|
||||||
|
class MailApp(App):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._search_debounce = None
|
||||||
|
|
||||||
|
def action_search(self, query: str):
|
||||||
|
if self._search_debounce:
|
||||||
|
self._search_debounce.stop() # Cancel pending search
|
||||||
|
self._search_debounce = Timer(
|
||||||
|
0.3, # Wait 300ms
|
||||||
|
self._do_search,
|
||||||
|
query
|
||||||
|
).start()
|
||||||
|
|
||||||
|
def _do_search(self, query: str) -> None:
|
||||||
|
results = self.search_messages(query)
|
||||||
|
self.update_results(results)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Avoids expensive recomputations for rapid user input.
|
||||||
|
|
||||||
|
#### 7. **Use `work()` Decorator for Background Tasks**
|
||||||
|
```python
|
||||||
|
from textual import work
|
||||||
|
|
||||||
|
class MailApp(App):
|
||||||
|
@work(exclusive=True)
|
||||||
|
async def load_message_content(self, message_id: int):
|
||||||
|
"""Load message content without blocking UI."""
|
||||||
|
content = await himalaya_client.get_message_content(message_id)
|
||||||
|
self._update_content_display(content)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Background workers don't block the UI thread.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mail App Performance Issues Analysis
|
||||||
|
|
||||||
|
### Current Implementation Problems
|
||||||
|
|
||||||
|
#### 1. **Message List Rendering** (src/mail/app.py)
|
||||||
|
```python
|
||||||
|
# PROBLEM: Rebuilding entire list on navigation
|
||||||
|
def action_next(self) -> None:
|
||||||
|
if not self.current_message_index >= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
next_id, next_idx = self.message_store.find_next_valid_id(
|
||||||
|
self.current_message_index
|
||||||
|
)
|
||||||
|
if next_id is not None and next_idx is not None:
|
||||||
|
self.current_message_id = next_id
|
||||||
|
self.current_message_index = next_idx
|
||||||
|
self._update_envelope_list_view() # ❌ Rebuilds entire list
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issue:** `_update_envelope_list_view()` rebuilds the entire message list on every navigation.
|
||||||
|
|
||||||
|
#### 2. **Envelope List Item Creation** (src/mail/widgets/EnvelopeListItem.py)
|
||||||
|
```python
|
||||||
|
# PROBLEM: Creating many widgets
|
||||||
|
class EnvelopeListItem(CustomListItem):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Static(self._from_display, classes="from")
|
||||||
|
yield Static(self._subject_display, classes="subject")
|
||||||
|
yield Static(self._date_display, classes="date")
|
||||||
|
# ❌ Each item creates 4+ Static widgets
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issue:** For 100 emails, this creates 400+ widgets. Should use a single widget.
|
||||||
|
|
||||||
|
#### 3. **Message Content Loading** (src/mail/widgets/ContentContainer.py)
|
||||||
|
```python
|
||||||
|
# PROBLEM: Blocking UI during content fetch
|
||||||
|
def display_content(self, message_id: int):
|
||||||
|
# ... loading logic
|
||||||
|
format_type = "text" if self.current_mode == "markdown" else "html"
|
||||||
|
self.content_worker = self.fetch_message_content(message_id, format_type)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issue:** Content fetch may block UI. Should use `@work` decorator.
|
||||||
|
|
||||||
|
#### 4. **Envelope List Updates** (src/mail/app.py lines 920-950)
|
||||||
|
```python
|
||||||
|
# PROBLEM: Complex envelope list rebuilding
|
||||||
|
def _update_envelope_list_view(self) -> None:
|
||||||
|
grouped_envelopes = []
|
||||||
|
for i, envelope in enumerate(self.message_store.envelopes):
|
||||||
|
# ❌ Processing every envelope on every update
|
||||||
|
if envelope.get("type") == "header":
|
||||||
|
grouped_envelopes.append({"type": "header", "label": ...})
|
||||||
|
else:
|
||||||
|
# Complex formatting
|
||||||
|
grouped_envelopes.append({...})
|
||||||
|
|
||||||
|
# ❌ Clearing and rebuilding entire list
|
||||||
|
envelopes_list = self.query_one("#envelopes_list", ListView)
|
||||||
|
envelopes_list.clear()
|
||||||
|
envelopes_list.extend([...])
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issue:** Rebuilding entire list is expensive. Should only update changed items.
|
||||||
|
|
||||||
|
#### 5. **Folder/Account Count Updates** (src/mail/app.py)
|
||||||
|
```python
|
||||||
|
# PROBLEM: Re-computing counts on every change
|
||||||
|
def _update_folder_list_view(self) -> None:
|
||||||
|
for folder in self.folders:
|
||||||
|
count = len([e for e in self.envelopes if e.get("folder") == folder]) # ❌ O(n) scan
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issue:** Counting all envelopes for each folder is expensive. Should cache counts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Optimization Plan
|
||||||
|
|
||||||
|
### Phase 1: Critical Performance Fixes (Week 1)
|
||||||
|
|
||||||
|
#### 1.1 Convert to `compose()` Pattern
|
||||||
|
**File:** `src/mail/app.py`
|
||||||
|
|
||||||
|
**Current:** Manual widget mounting in `on_mount()` and other methods
|
||||||
|
**Goal:** Use `compose()` for declarative UI building
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
```python
|
||||||
|
# Before (BAD):
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
# ... manual mounting
|
||||||
|
|
||||||
|
# After (GOOD):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
with Vertical(id="app_container"):
|
||||||
|
with Horizontal():
|
||||||
|
# Left panel
|
||||||
|
with Vertical(id="left_panel"):
|
||||||
|
yield Static("Accounts", id="accounts_header")
|
||||||
|
yield ListView(id="accounts_list")
|
||||||
|
|
||||||
|
yield Static("Folders", id="folders_header")
|
||||||
|
yield ListView(id="folders_list")
|
||||||
|
|
||||||
|
# Middle panel
|
||||||
|
with Vertical(id="middle_panel"):
|
||||||
|
yield Static("Messages", id="messages_header")
|
||||||
|
yield ListView(id="envelopes_list")
|
||||||
|
|
||||||
|
# Right panel
|
||||||
|
yield ContentContainer(id="content_container")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Impact:** 30-50% faster startup, reduced memory usage
|
||||||
|
|
||||||
|
#### 1.2 Implement Lazy Loading for Envelopes
|
||||||
|
**File:** `src/mail/app.py`
|
||||||
|
|
||||||
|
**Current:** Load all envelopes at startup
|
||||||
|
**Goal:** Load envelopes on-demand using background workers
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
```python
|
||||||
|
class MailApp(App):
|
||||||
|
envelopes_loaded: reactive[bool] = reactive(False)
|
||||||
|
_envelopes_cache: list[dict] = []
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
# Start background loading
|
||||||
|
self._load_initial_envelopes()
|
||||||
|
|
||||||
|
@work(exclusive=True, group="envelope_loading")
|
||||||
|
async def _load_initial_envelopes(self):
|
||||||
|
"""Load initial envelopes in background."""
|
||||||
|
envelopes, success = await himalaya_client.list_envelopes()
|
||||||
|
if success:
|
||||||
|
self._envelopes_cache = envelopes
|
||||||
|
self.envelopes_loaded = True
|
||||||
|
self._update_envelope_list_view()
|
||||||
|
|
||||||
|
def _load_more_envelopes(self) -> None:
|
||||||
|
"""Load more envelopes when scrolling."""
|
||||||
|
pass # Implement lazy loading
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Impact:** 60-70% faster startup, perceived instant UI
|
||||||
|
|
||||||
|
#### 1.3 Optimize Message List Updates
|
||||||
|
**File:** `src/mail/app.py`
|
||||||
|
|
||||||
|
**Current:** Rebuild entire list on navigation
|
||||||
|
**Goal:** Only update changed items, use reactive properties
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
```python
|
||||||
|
class MailApp(App):
|
||||||
|
current_message_index: reactive[int] = reactive(-1)
|
||||||
|
|
||||||
|
def action_next(self) -> None:
|
||||||
|
"""Move to next message efficiently."""
|
||||||
|
if not self.current_message_index >= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
next_id, next_idx = self.message_store.find_next_valid_id(
|
||||||
|
self.current_message_index
|
||||||
|
)
|
||||||
|
|
||||||
|
if next_id is not None:
|
||||||
|
# ✅ Only update reactive property
|
||||||
|
self.current_message_index = next_idx
|
||||||
|
# ✅ Let Textual handle the update
|
||||||
|
# DON'T call _update_envelope_list_view()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Impact:** 80-90% faster navigation, no UI flicker
|
||||||
|
|
||||||
|
#### 1.4 Use Background Workers for Content Loading
|
||||||
|
**File:** `src/mail/widgets/ContentContainer.py`
|
||||||
|
|
||||||
|
**Current:** Blocking content fetch
|
||||||
|
**Goal:** Use `@work` decorator for non-blocking loads
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
```python
|
||||||
|
class ContentContainer(ScrollableContainer):
|
||||||
|
@work(exclusive=True)
|
||||||
|
async def fetch_message_content(self, message_id: int, format_type: str) -> None:
|
||||||
|
"""Fetch message content in background without blocking UI."""
|
||||||
|
content, success = await himalaya_client.get_message_content(
|
||||||
|
message_id,
|
||||||
|
folder=self.current_folder,
|
||||||
|
account=self.current_account
|
||||||
|
)
|
||||||
|
|
||||||
|
if success and content:
|
||||||
|
self._update_content(content)
|
||||||
|
else:
|
||||||
|
self.notify("Failed to fetch message content")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Impact:** No UI blocking, smooth content transitions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Code Quality & Architecture (Week 2)
|
||||||
|
|
||||||
|
#### 2.1 Refactor Message Store for Efficiency
|
||||||
|
**File:** `src/mail/message_store.py`
|
||||||
|
|
||||||
|
**Current:** Linear searches, no caching
|
||||||
|
**Goal:** Implement indexed lookups, cache counts
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
```python
|
||||||
|
class MessageStore:
|
||||||
|
"""Optimized message store with caching."""
|
||||||
|
|
||||||
|
def __init__(self, envelopes: list[dict]):
|
||||||
|
self.envelopes = envelopes
|
||||||
|
self._index_cache = {} # O(1) lookup cache
|
||||||
|
self._folder_counts = {} # Cached folder counts
|
||||||
|
self._unread_counts = {} # Cached unread counts
|
||||||
|
|
||||||
|
# Build caches
|
||||||
|
self._build_caches()
|
||||||
|
|
||||||
|
def _build_caches(self) -> None:
|
||||||
|
"""Build lookup caches."""
|
||||||
|
for idx, envelope in enumerate(self.envelopes):
|
||||||
|
self._index_cache[envelope["id"]] = idx
|
||||||
|
folder = envelope.get("folder", "INBOX")
|
||||||
|
self._folder_counts[folder] = self._folder_counts.get(folder, 0) + 1
|
||||||
|
if not envelope.get("flags", {}).get("seen", False):
|
||||||
|
self._unread_counts[folder] = self._unread_counts.get(folder, 0) + 1
|
||||||
|
|
||||||
|
def get_index(self, message_id: int) -> int | None:
|
||||||
|
"""Get envelope index in O(1)."""
|
||||||
|
return self._index_cache.get(message_id)
|
||||||
|
|
||||||
|
def get_folder_count(self, folder: str) -> int:
|
||||||
|
"""Get folder count in O(1)."""
|
||||||
|
return self._folder_counts.get(folder, 0)
|
||||||
|
|
||||||
|
def get_unread_count(self, folder: str) -> int:
|
||||||
|
"""Get unread count in O(1)."""
|
||||||
|
return self._unread_counts.get(folder, 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Impact:** O(1) lookups instead of O(n), instant count retrieval
|
||||||
|
|
||||||
|
#### 2.2 Consolidate Envelope List Item
|
||||||
|
**File:** `src/mail/widgets/EnvelopeListItem.py`
|
||||||
|
|
||||||
|
**Current:** Multiple widgets per item
|
||||||
|
**Goal:** Use single widget with custom rendering
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
```python
|
||||||
|
class EnvelopeListItem(CustomListItem):
|
||||||
|
"""Optimized envelope list item using single widget."""
|
||||||
|
|
||||||
|
def __init__(self, envelope: dict, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.envelope = envelope
|
||||||
|
|
||||||
|
def render(self) -> RichText:
|
||||||
|
"""Render as single RichText widget."""
|
||||||
|
from rich.text import Text, Text as RichText
|
||||||
|
|
||||||
|
# Build RichText once (more efficient than multiple widgets)
|
||||||
|
text = Text.assemble(
|
||||||
|
self._from_display,
|
||||||
|
" ",
|
||||||
|
self._subject_display,
|
||||||
|
" ",
|
||||||
|
self._date_display,
|
||||||
|
style="on" if self.envelope.get("flags", {}).get("seen") else "bold"
|
||||||
|
)
|
||||||
|
|
||||||
|
return text
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Impact:** 70% reduction in widget count, faster rendering
|
||||||
|
|
||||||
|
#### 2.3 Add Memoization for Expensive Operations
|
||||||
|
**File:** `src/mail/utils.py`
|
||||||
|
|
||||||
|
**Current:** Re-computing values
|
||||||
|
**Goal:** Cache computed values
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
```python
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
@lru_cache(maxsize=128)
|
||||||
|
def format_sender_name(envelope: dict) -> str:
|
||||||
|
"""Format sender name with caching."""
|
||||||
|
from_name = envelope.get("from", {}).get("name", "")
|
||||||
|
from_addr = envelope.get("from", {}).get("addr", "")
|
||||||
|
|
||||||
|
if not from_name:
|
||||||
|
return from_addr
|
||||||
|
|
||||||
|
# Truncate if too long
|
||||||
|
if len(from_name) > 25:
|
||||||
|
return from_name[:22] + "..."
|
||||||
|
|
||||||
|
return from_name
|
||||||
|
|
||||||
|
@lru_cache(maxsize=128)
|
||||||
|
def format_date(date_str: str) -> str:
|
||||||
|
"""Format date with caching."""
|
||||||
|
# Parse and format date string
|
||||||
|
# Implementation...
|
||||||
|
return formatted_date
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Impact:** Faster repeated operations, reduced CPU usage
|
||||||
|
|
||||||
|
#### 2.4 Add Notification Compression Caching
|
||||||
|
**File:** `src/mail/notification_compressor.py`
|
||||||
|
|
||||||
|
**Current:** Re-compressing on every view
|
||||||
|
**Goal:** Cache compressed results
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
```python
|
||||||
|
class NotificationCompressor:
|
||||||
|
"""Compressor with caching for performance."""
|
||||||
|
|
||||||
|
def __init__(self, mode: str = "summary"):
|
||||||
|
self.mode = mode
|
||||||
|
self._compression_cache = {} # Cache compressed content
|
||||||
|
|
||||||
|
def compress(
|
||||||
|
self,
|
||||||
|
content: str,
|
||||||
|
envelope: dict[str, Any]
|
||||||
|
) -> tuple[str, NotificationType | None]:
|
||||||
|
"""Compress with caching."""
|
||||||
|
cache_key = f"{envelope['id']}:{self.mode}"
|
||||||
|
|
||||||
|
# Check cache
|
||||||
|
if cache_key in self._compression_cache:
|
||||||
|
return self._compression_cache[cache_key]
|
||||||
|
|
||||||
|
# Compress and cache
|
||||||
|
compressed, notif_type = self._compress_impl(content, envelope)
|
||||||
|
self._compression_cache[cache_key] = (compressed, notif_type)
|
||||||
|
|
||||||
|
return compressed, notif_type
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Impact:** Instant display for previously viewed notifications
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Advanced Optimizations (Week 3-4)
|
||||||
|
|
||||||
|
#### 3.1 Implement Virtual Scrolling
|
||||||
|
**File:** `src/mail/app.py`
|
||||||
|
|
||||||
|
**Current:** Render all items in list
|
||||||
|
**Goal:** Use ListView virtualization
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
```python
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield ListView(
|
||||||
|
id="envelopes_list",
|
||||||
|
initial_index=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ListView automatically virtualizes for performance
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Impact:** Constant time rendering regardless of list size
|
||||||
|
|
||||||
|
#### 3.2 Debounce User Input
|
||||||
|
**File:** `src/mail/screens/SearchPanel.py`
|
||||||
|
|
||||||
|
**Current:** Search on every keystroke
|
||||||
|
**Goal:** Debounce search with 300ms delay
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
```python
|
||||||
|
class SearchPanel(Screen):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._search_debounce = None
|
||||||
|
|
||||||
|
def on_input_changed(self, event) -> None:
|
||||||
|
"""Debounce search input."""
|
||||||
|
if self._search_debounce:
|
||||||
|
self._search_debounce.stop()
|
||||||
|
|
||||||
|
self._search_debounce = Timer(
|
||||||
|
0.3,
|
||||||
|
self._perform_search,
|
||||||
|
event.value
|
||||||
|
).start()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Impact:** 80% reduction in expensive search operations
|
||||||
|
|
||||||
|
#### 3.3 Use `dataclass` for Data Models
|
||||||
|
**File:** `src/mail/notification_detector.py`
|
||||||
|
|
||||||
|
**Current:** Dict-based data access
|
||||||
|
**Goal:** Use dataclasses for type safety and performance
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
```python
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Envelope:
|
||||||
|
"""Typed envelope data model."""
|
||||||
|
id: int
|
||||||
|
subject: str
|
||||||
|
from_name: str
|
||||||
|
from_addr: str
|
||||||
|
date: str
|
||||||
|
flags: dict = field(default_factory=dict)
|
||||||
|
folder: str = "INBOX"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Impact:** Type safety, better IDE support, faster attribute access
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Memory & Resource Management (Week 4)
|
||||||
|
|
||||||
|
#### 4.1 Implement Widget Pooling
|
||||||
|
**File:** `src/mail/app.py`
|
||||||
|
|
||||||
|
**Current:** Creating new widgets constantly
|
||||||
|
**Goal:** Reuse widgets to reduce allocations
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
```python
|
||||||
|
class WidgetPool:
|
||||||
|
"""Pool for reusing widgets."""
|
||||||
|
|
||||||
|
def __init__(self, widget_class, max_size: int = 50):
|
||||||
|
self.widget_class = widget_class
|
||||||
|
self.pool = []
|
||||||
|
self.max_size = max_size
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
"""Get widget from pool or create new."""
|
||||||
|
if self.pool:
|
||||||
|
return self.pool.pop()
|
||||||
|
return self.widget_class()
|
||||||
|
|
||||||
|
def release(self, widget) -> None:
|
||||||
|
"""Return widget to pool."""
|
||||||
|
if len(self.pool) < self.max_size:
|
||||||
|
self.pool.append(widget)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Impact:** Reduced garbage collection, smoother scrolling
|
||||||
|
|
||||||
|
#### 4.2 Implement Content Pagination
|
||||||
|
**File:** `src/mail/widgets/ContentContainer.py`
|
||||||
|
|
||||||
|
**Current:** Load full content
|
||||||
|
**Goal:** Load content in chunks for large emails
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
```python
|
||||||
|
class ContentContainer(ScrollableContainer):
|
||||||
|
PAGE_SIZE = 500 # Characters per page
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._pages: list[str] = []
|
||||||
|
self._current_page = 0
|
||||||
|
|
||||||
|
def _load_next_page(self) -> None:
|
||||||
|
"""Load next page of content when scrolling."""
|
||||||
|
if self._current_page + 1 < len(self._pages):
|
||||||
|
self._current_page += 1
|
||||||
|
self.content.update(self._pages[self._current_page])
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Impact:** Faster initial load, smoother scrolling for large emails
|
||||||
|
|
||||||
|
#### 4.3 Clean Up Unused Imports
|
||||||
|
**Files:** All Python files in `src/mail/`
|
||||||
|
|
||||||
|
**Current:** Unused imports, circular dependencies
|
||||||
|
**Goal:** Remove all unused code
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- Run `ruff check` and fix all unused imports
|
||||||
|
- Remove circular dependencies
|
||||||
|
- Clean up `__all__` exports
|
||||||
|
- Optimize import order
|
||||||
|
|
||||||
|
**Expected Impact:** Faster import time, smaller memory footprint
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
### Week 1: Critical Performance Fixes
|
||||||
|
1. Day 1-2: Implement `compose()` pattern
|
||||||
|
2. Day 3-4: Lazy loading for envelopes
|
||||||
|
3. Day 5: Optimize message list navigation
|
||||||
|
4. Day 6-7: Background workers for content loading
|
||||||
|
5. Day 8-10: Testing and benchmarking
|
||||||
|
|
||||||
|
### Week 2: Code Quality
|
||||||
|
1. Day 1-2: Refactor MessageStore with caching
|
||||||
|
2. Day 3-4: Consolidate EnvelopeListItem
|
||||||
|
3. Day 5: Add memoization utilities
|
||||||
|
4. Day 6-7: Notification compression caching
|
||||||
|
5. Day 8-10: Code review and cleanup
|
||||||
|
|
||||||
|
### Week 3: Advanced Optimizations
|
||||||
|
1. Day 1-3: Virtual scrolling implementation
|
||||||
|
2. Day 4-5: Debounce user input
|
||||||
|
3. Day 6-7: Data model refactoring
|
||||||
|
4. Day 8-10: Performance testing
|
||||||
|
|
||||||
|
### Week 4: Memory Management
|
||||||
|
1. Day 1-3: Widget pooling
|
||||||
|
2. Day 4-5: Content pagination
|
||||||
|
3. Day 6-7: Import cleanup
|
||||||
|
4. Day 8-10: Final optimization and polish
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
### Performance Targets
|
||||||
|
- **Startup Time:** < 1 second (currently: 3-5 seconds)
|
||||||
|
- **Navigation Latency:** < 50ms between messages (currently: 200-500ms)
|
||||||
|
- **List Rendering:** < 100ms for 100 items (currently: 500-1000ms)
|
||||||
|
- **Memory Usage:** < 100MB for 1000 emails (currently: 300-500MB)
|
||||||
|
- **Frame Rate:** 60 FPS during navigation (currently: 10-20 FPS)
|
||||||
|
|
||||||
|
### Code Quality Targets
|
||||||
|
- **Test Coverage:** > 80% (currently: ~10%)
|
||||||
|
- **Ruff Warnings:** 0 critical, < 5 style warnings
|
||||||
|
- **Import Cleanup:** 100% of files cleaned
|
||||||
|
- **Type Coverage:** 100% typed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Performance Benchmarking
|
||||||
|
```python
|
||||||
|
# benchmark_performance.py
|
||||||
|
import time
|
||||||
|
import tracemalloc
|
||||||
|
from src.mail.app import EmailViewerApp
|
||||||
|
|
||||||
|
def benchmark_startup():
|
||||||
|
"""Benchmark app startup time."""
|
||||||
|
tracemalloc.start()
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
app = EmailViewerApp()
|
||||||
|
app.run()
|
||||||
|
|
||||||
|
end = time.time()
|
||||||
|
current, peak = tracemalloc.get_traced_memory()
|
||||||
|
|
||||||
|
print(f"Startup Time: {end - start:.3f}s")
|
||||||
|
print(f"Memory Usage: {peak / 1024 / 1024:.2f} MB")
|
||||||
|
|
||||||
|
def benchmark_navigation():
|
||||||
|
"""Benchmark message navigation."""
|
||||||
|
app = EmailViewerApp()
|
||||||
|
# ... measure navigation timing
|
||||||
|
|
||||||
|
timings = []
|
||||||
|
for i in range(100):
|
||||||
|
start = time.time()
|
||||||
|
app.action_next()
|
||||||
|
end = time.time()
|
||||||
|
timings.append(end - start)
|
||||||
|
|
||||||
|
print(f"Average Navigation Time: {sum(timings) / len(timings) * 1000:.1f}ms")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- Test with 100, 1000, and 10000 messages
|
||||||
|
- Measure memory usage over time
|
||||||
|
- Test with slow network conditions
|
||||||
|
- Test on different terminal sizes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
### Textual Documentation
|
||||||
|
- **Main Docs:** https://textual.textualize.io/
|
||||||
|
- **Widget Guide:** https://textual.textualize.io/guide/widgets/
|
||||||
|
- **Best Practices:** https://textual.textualize.io/blog/
|
||||||
|
- **Performance Guide:** https://textual.textualize.io/blog/2024/12/12/algorithms-for-high-performance-terminal-apps/
|
||||||
|
|
||||||
|
### Python Performance Guides
|
||||||
|
- **Python Performance Guide:** https://www.fyld.pt/blog/python-performance-guide-writing-code-2025
|
||||||
|
- **Optimization Techniques:** https://analyticsvidhya.com/blog/2024/01/optimize-python-code-for-high-speed-execution
|
||||||
|
|
||||||
|
### Similar Projects
|
||||||
|
- **Rich Console Examples:** https://github.com/Textualize/rich
|
||||||
|
- **Prompt Toolkit:** https://github.com/prompt-toolkit/python-prompt-toolkit
|
||||||
|
- **Urwid:** https://github.com/urwid/urwid
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- This plan focuses on the mail app but principles apply to calendar and tasks apps
|
||||||
|
- All changes should be backward compatible
|
||||||
|
- Run performance benchmarks before and after each phase
|
||||||
|
- Document any Textual-specific optimizations discovered during implementation
|
||||||
Reference in New Issue
Block a user