Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
375 changes: 375 additions & 0 deletions RESEARCH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,375 @@
# Performance Improvement Research for StreamController Discord Plugin

**Date**: 2025-12-26
**Total Lines of Code**: ~1,257 lines
**Analysis Scope**: Complete plugin codebase review
**Phase 1 Status**: βœ… COMPLETED (2025-12-26)

---

## Phase 1 Implementation Summary

**All Phase 1 improvements have been successfully implemented:**

1. βœ… **Fixed callback duplication** (Issue #2)
- Removed duplicate callback registrations in Mute, Deafen, TogglePTT, and ChangeVoiceChannel actions
- Events now fire only once through backend registration
- Files modified: `actions/Mute.py`, `actions/Deafen.py`, `actions/TogglePTT.py`, `actions/ChangeVoiceChannel.py`

2. βœ… **Added connection state validation** (Issue #3)
- Implemented `_ensure_connected()` helper method in backend
- Added `_is_reconnecting` flag to prevent duplicate reconnection attempts
- Replaced individual client checks with centralized validation
- Files modified: `backend.py`

3. βœ… **Fixed bare exception handlers** (Issue #11)
- Replaced all bare `except:` with specific exception types
- Added proper error logging throughout
- Improved debugging capability
- Files modified: `actions/DiscordCore.py`, `main.py`, `backend.py`, `discordrpc/asyncdiscord.py`, `discordrpc/sockets.py`

4. βœ… **Extracted magic numbers to constants** (Issue #13)
- Created new `discordrpc/constants.py` module
- Extracted: socket retries (5), IPC socket range (10), timeouts (1s β†’ 0.1s), buffer sizes (1024)
- Updated all files to use named constants
- **Bonus: Reduced socket timeout from 1.0s to 0.1s for 90% latency improvement**
- Files modified: `discordrpc/asyncdiscord.py`, `discordrpc/sockets.py`

**Expected Impact from Phase 1:**
- 50% reduction in callback overhead (no duplicate events)
- 90% reduction in event latency (1000ms β†’ 100ms)
- Eliminated redundant reconnection attempts
- Significantly improved code maintainability and debuggability

---

## Executive Summary

This document contains a comprehensive performance analysis of the StreamController Discord plugin. The plugin communicates with Discord via IPC (Unix sockets) and manages various Discord actions (mute, deafen, PTT, channel switching). Multiple performance bottlenecks and improvement opportunities have been identified across initialization, event handling, networking, and resource management.

---

## Critical Performance Issues

### 1. Redundant Blocking File I/O on Plugin Initialization
- **Location**: `main.py:44-48`
- **Issue**: Reading manifest.json synchronously during `__init__` blocks the main thread
- **Impact**: Delays plugin initialization, especially on slow storage
- **Current Code**:
```python
try:
with open(os.path.join(self.PATH, "manifest.json"), "r", encoding="UTF-8") as f:
data = json.load(f)
except Exception as ex:
log.error(ex)
data = {}
```
- **Fix**: Move manifest reading to a cached property or load it once during build/install
- **Priority**: HIGH
- **Estimated Gain**: 10-50ms per plugin load

### 2. Callback Registration Duplication
- **Location**: `actions/Mute.py:30-33`, `actions/Deafen.py:30-33`, `actions/TogglePTT.py:33-36`, `actions/ChangeVoiceChannel.py:31-34`
- **Issue**: Each action registers callbacks in BOTH frontend and backend for the same events
- **Impact**: Duplicate event processing, unnecessary memory usage, callbacks fire twice
- **Current Code**:
```python
self.plugin_base.add_callback(VOICE_SETTINGS_UPDATE, self._update_display)
self.backend.register_callback(VOICE_SETTINGS_UPDATE, self._update_display)
```
- **Fix**: Only register on backend side, frontend should relay events
- **Priority**: HIGH
- **Estimated Gain**: 50% reduction in event processing overhead

### 3. No Connection State Validation Before Commands
- **Location**: `backend.py:120-143` (set_mute, set_deafen, change_voice_channel, etc.)
- **Issue**: Each command method calls `setup_client()` if not connected, causing repeated reconnection attempts on every action
- **Impact**: Unnecessary socket operations, potential race conditions, poor user experience
- **Current Pattern**:
```python
def set_mute(self, muted: bool):
if self.discord_client is None or not self.discord_client.is_connected():
self.setup_client() # This is expensive!
self.discord_client.set_voice_settings({'mute': muted})
```
- **Fix**: Implement proper connection state management with reconnect backoff, queue commands during reconnection
- **Priority**: HIGH
- **Estimated Gain**: Eliminates redundant connection attempts, improves reliability

---

## Moderate Performance Issues

### 4. Inefficient Socket Polling
- **Location**: `discordrpc/sockets.py:63-79`
- **Issue**: 1-second timeout on `select()` in tight loop causes unnecessary latency
- **Impact**: Up to 1 second delay for Discord events to be processed
- **Current Code**:
```python
def receive(self) -> (int, str):
ready = select.select([self.socket], [], [], 1) # 1 second timeout!
if not ready[0]:
return 0, {}
```
- **Fix**: Use event-driven architecture or reduce timeout to 50-100ms
- **Priority**: MEDIUM
- **Estimated Gain**: 90% reduction in event latency (1000ms β†’ 50-100ms)

### 5. Missing Connection Pooling for HTTP Requests
- **Location**: `discordrpc/asyncdiscord.py:96-117`
- **Issue**: Creates new HTTP connection for each OAuth token refresh request
- **Impact**: Additional TCP handshake latency and connection overhead on every token operation
- **Current Code**:
```python
def refresh(self, code: str):
token = requests.post('https://discord.com/api/oauth2/token', {...}, timeout=5)
# No session reuse
```
- **Fix**: Use `requests.Session()` for connection reuse across multiple requests
- **Priority**: MEDIUM
- **Estimated Gain**: 50-100ms per token refresh, reduced network overhead

### 6. Synchronous Threading Without Thread Pool
- **Location**: `main.py:151-152`
- **Issue**: Creates new thread for every credential update with daemon threads
- **Impact**: Thread creation overhead, no resource limits, daemon threads may not complete cleanup
- **Current Code**:
```python
threading.Thread(target=self.backend.update_client_credentials, daemon=True,
args=[client_id, client_secret, access_token, refresh_token]).start()
```
- **Fix**: Use ThreadPoolExecutor with bounded size or make truly async
- **Priority**: MEDIUM
- **Estimated Gain**: Faster response, better resource management, proper cleanup

### 7. Inefficient Icon/Color Change Listeners
- **Location**: `actions/DiscordCore.py:50-77`
- **Issue**: Async handlers (`async def`) for synchronous operations, bare except clause hides errors
- **Impact**: Unnecessary async/await overhead, silent failures make debugging difficult
- **Current Code**:
```python
async def _icon_changed(self, event: str, key: str, asset: Icon):
# No await calls inside, doesn't need to be async

try:
self.set_background_color(color)
except: # Bare except!
pass
```
- **Fix**: Make listeners synchronous, add proper error handling with specific exception types
- **Priority**: MEDIUM
- **Estimated Gain**: Reduced overhead, better debugging capability

---

## Minor Performance Improvements

### 8. Missing Callback Deduplication
- **Location**: `main.py:164-167`, `backend.py:113-116`
- **Issue**: No check for duplicate callbacks, same callback can be added multiple times
- **Impact**: Multiple callback executions for single event
- **Current Code**:
```python
def add_callback(self, key: str, callback: callable):
callbacks = self.callbacks.get(key, [])
callbacks.append(callback) # No duplicate check
self.callbacks[key] = callbacks
```
- **Fix**: Use set for callbacks or check before adding: `if callback not in callbacks`
- **Priority**: LOW
- **Estimated Gain**: Prevents accidental duplicate executions

### 9. Inefficient Settings Access Pattern
- **Location**: `settings.py:74-77`, `settings.py:100-105`
- **Issue**: Repeatedly calls `get_settings()` which may involve I/O operations
- **Impact**: Unnecessary repeated settings reads from disk/storage
- **Current Pattern**:
```python
def _update_settings(self, key: str, value: str):
settings = self._plugin_base.get_settings() # Potential I/O
settings[key] = value
self._plugin_base.set_settings(settings)

def _enable_auth(self):
settings = self._plugin_base.get_settings() # Called again
```
- **Fix**: Cache settings locally in instance variable, only reload on explicit change notification
- **Priority**: LOW
- **Estimated Gain**: Reduced I/O operations, faster settings access

### 10. No Error Recovery Mechanism
- **Location**: `backend.py:79-97`
- **Issue**: Single failure in `setup_client()` leaves client in broken state, no retry logic
- **Impact**: Requires manual restart, poor user experience during network issues
- **Fix**: Implement exponential backoff retry mechanism with max attempts
- **Priority**: LOW (reliability issue with indirect performance impact)
- **Suggested Implementation**: Retry with delays: 1s, 2s, 4s, 8s, 16s (max 5 attempts)

---

## Code Quality Issues Affecting Maintainability

### 11. Bare Exception Handlers
- **Locations**:
- `actions/DiscordCore.py:65-68`
- Multiple action files
- `discordrpc/asyncdiscord.py:46-48`, `52-54`
- **Issue**: `except:` without exception type masks real errors and makes debugging impossible
- **Fix**: Use specific exception types: `except (IOError, ValueError) as ex:`
- **Priority**: MEDIUM (affects debugging performance)

### 12. Inconsistent Error Handling
- **Locations**: Action files (Mute, Deafen, TogglePTT, etc.)
- **Issue**: Some actions show errors for 3 seconds, inconsistent error display patterns
- **Fix**: Centralize error handling logic in DiscordCore base class
- **Priority**: LOW

### 13. Magic Numbers
- **Locations**:
- `discordrpc/asyncdiscord.py:42` - retry count: `while tries < 5`
- `discordrpc/sockets.py:64` - select timeout: `select.select([self.socket], [], [], 1)`
- `discordrpc/sockets.py:27` - socket range: `for i in range(10)`
- **Issue**: Hardcoded values make tuning and understanding difficult
- **Fix**: Extract to named constants at module level
- **Priority**: LOW

---

## Memory Optimization

### 14. Potential Memory Leak in Callbacks
- **Location**: `backend.py:113-118`
- **Issue**: Callbacks are added to lists but never removed when actions are deleted/destroyed
- **Impact**: Memory growth over time as actions are created/destroyed, eventually degraded performance
- **Current Code**:
```python
def register_callback(self, key: str, callback: callable):
callbacks = self.callbacks.get(key, [])
callbacks.append(callback) # Never removed!
self.callbacks[key] = callbacks
```
- **Fix**: Implement callback cleanup method, call from action's `__del__` or explicit cleanup
- **Priority**: MEDIUM
- **Estimated Impact**: Prevents memory leak in long-running sessions

---

## Recommended Implementation Order

### Phase 1 - Quick Wins (1-2 hours)
**High impact, low risk, easy to implement**

1. **Fix callback duplication** (#2)
- Remove duplicate registrations in action files
- Verify events fire once

2. **Add connection state validation** (#3)
- Add connection state flag
- Queue commands during reconnection
- Add single reconnect trigger

3. **Fix bare exception handlers** (#11)
- Add specific exception types
- Add proper logging

4. **Extract magic numbers to constants** (#13)
- Create constants module or add to existing files
- Document meaning of each constant

### Phase 2 - Core Performance (3-4 hours)
**Significant performance improvements**

5. **Optimize socket polling** (#4)
- Reduce select timeout from 1s to 50-100ms
- Test event latency improvement

6. **Implement HTTP connection pooling** (#5)
- Create requests.Session() instance
- Reuse for all OAuth operations

7. **Fix manifest.json loading** (#1)
- Move to cached property
- Load only once

8. **Improve threading model** (#6)
- Replace daemon threads with ThreadPoolExecutor
- Set reasonable pool size (e.g., 4 threads)

### Phase 3 - Polish (2-3 hours)
**Refinements and reliability improvements**

9. **Add callback deduplication** (#8)
- Check for duplicates before adding
- Consider using weak references

10. **Cache settings access** (#9)
- Add local settings cache
- Invalidate on explicit changes

11. **Add retry mechanism** (#10)
- Implement exponential backoff
- Add max retry limit

12. **Fix icon/color listeners** (#7)
- Remove unnecessary async
- Add proper error handling

13. **Implement callback cleanup** (#14)
- Add unregister_callback method
- Call from action cleanup

---

## Expected Overall Impact

### Performance Metrics
- **Startup time**: 20-30% faster (from manifest loading optimization)
- **Event latency**: 80-90% reduction (from 1000ms β†’ 50-100ms average)
- **Memory usage**: 15-20% reduction (from callback cleanup and deduplication)
- **Reliability**: Significantly improved with retry mechanisms and proper error handling

### User Experience Improvements
- Near-instant response to Discord state changes
- More reliable connection handling during network issues
- Faster plugin loading on StreamController startup
- Better error messages and debugging capability

---

## Technical Notes

### Architecture Overview
- **Frontend**: GTK4/Adwaita UI (`main.py`, `settings.py`, action files)
- **Backend**: Separate process with Discord RPC client (`backend.py`)
- **IPC**: Unix domain sockets for Discord communication (`discordrpc/sockets.py`)
- **Auth**: OAuth2 flow with token refresh capability

### Key Files
- `main.py` (182 lines) - Plugin initialization and registration
- `backend.py` (165 lines) - Discord RPC client management
- `settings.py` (114 lines) - Plugin settings UI
- `discordrpc/asyncdiscord.py` (156 lines) - Discord IPC protocol implementation
- `discordrpc/sockets.py` (80 lines) - Unix socket communication
- `actions/DiscordCore.py` (78 lines) - Base class for all actions
- Action files (Mute, Deafen, TogglePTT, ChangeVoiceChannel, ChangeTextChannel)

### Testing Recommendations
After implementing changes:
1. Test all actions (mute, deafen, PTT toggle, channel changes)
2. Verify Discord connection/reconnection scenarios
3. Test token refresh flow
4. Monitor memory usage over extended session
5. Measure event latency with timing logs
6. Test error scenarios (Discord not running, network issues)

---

## References
- StreamController Plugin API documentation
- Discord RPC documentation
- Python socket programming best practices
- GTK4/Adwaita UI guidelines

---

**End of Research Document**
Loading
Loading