pomera-ai-commander 0.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +105 -680
- package/bin/pomera-ai-commander.js +62 -62
- package/core/__init__.py +65 -65
- package/core/app_context.py +482 -482
- package/core/async_text_processor.py +421 -421
- package/core/backup_manager.py +655 -655
- package/core/backup_recovery_manager.py +1033 -1033
- package/core/content_hash_cache.py +508 -508
- package/core/context_menu.py +313 -313
- package/core/data_validator.py +1066 -1066
- package/core/database_connection_manager.py +744 -744
- package/core/database_curl_settings_manager.py +608 -608
- package/core/database_promera_ai_settings_manager.py +446 -446
- package/core/database_schema.py +411 -411
- package/core/database_schema_manager.py +395 -395
- package/core/database_settings_manager.py +1507 -1507
- package/core/database_settings_manager_interface.py +456 -456
- package/core/dialog_manager.py +734 -734
- package/core/efficient_line_numbers.py +510 -510
- package/core/error_handler.py +746 -746
- package/core/error_service.py +431 -431
- package/core/event_consolidator.py +511 -511
- package/core/mcp/__init__.py +43 -43
- package/core/mcp/protocol.py +288 -288
- package/core/mcp/schema.py +251 -251
- package/core/mcp/server_stdio.py +299 -299
- package/core/mcp/tool_registry.py +2372 -2345
- package/core/memory_efficient_text_widget.py +711 -711
- package/core/migration_manager.py +914 -914
- package/core/migration_test_suite.py +1085 -1085
- package/core/migration_validator.py +1143 -1143
- package/core/optimized_find_replace.py +714 -714
- package/core/optimized_pattern_engine.py +424 -424
- package/core/optimized_search_highlighter.py +552 -552
- package/core/performance_monitor.py +674 -674
- package/core/persistence_manager.py +712 -712
- package/core/progressive_stats_calculator.py +632 -632
- package/core/regex_pattern_cache.py +529 -529
- package/core/regex_pattern_library.py +350 -350
- package/core/search_operation_manager.py +434 -434
- package/core/settings_defaults_registry.py +1087 -1087
- package/core/settings_integrity_validator.py +1111 -1111
- package/core/settings_serializer.py +557 -557
- package/core/settings_validator.py +1823 -1823
- package/core/smart_stats_calculator.py +709 -709
- package/core/statistics_update_manager.py +619 -619
- package/core/stats_config_manager.py +858 -858
- package/core/streaming_text_handler.py +723 -723
- package/core/task_scheduler.py +596 -596
- package/core/update_pattern_library.py +168 -168
- package/core/visibility_monitor.py +596 -596
- package/core/widget_cache.py +498 -498
- package/mcp.json +51 -61
- package/package.json +61 -57
- package/pomera.py +7482 -7482
- package/pomera_mcp_server.py +183 -144
- package/requirements.txt +32 -0
- package/tools/__init__.py +4 -4
- package/tools/ai_tools.py +2891 -2891
- package/tools/ascii_art_generator.py +352 -352
- package/tools/base64_tools.py +183 -183
- package/tools/base_tool.py +511 -511
- package/tools/case_tool.py +308 -308
- package/tools/column_tools.py +395 -395
- package/tools/cron_tool.py +884 -884
- package/tools/curl_history.py +600 -600
- package/tools/curl_processor.py +1207 -1207
- package/tools/curl_settings.py +502 -502
- package/tools/curl_tool.py +5467 -5467
- package/tools/diff_viewer.py +1071 -1071
- package/tools/email_extraction_tool.py +248 -248
- package/tools/email_header_analyzer.py +425 -425
- package/tools/extraction_tools.py +250 -250
- package/tools/find_replace.py +1750 -1750
- package/tools/folder_file_reporter.py +1463 -1463
- package/tools/folder_file_reporter_adapter.py +480 -480
- package/tools/generator_tools.py +1216 -1216
- package/tools/hash_generator.py +255 -255
- package/tools/html_tool.py +656 -656
- package/tools/jsonxml_tool.py +729 -729
- package/tools/line_tools.py +419 -419
- package/tools/markdown_tools.py +561 -561
- package/tools/mcp_widget.py +1417 -1417
- package/tools/notes_widget.py +973 -973
- package/tools/number_base_converter.py +372 -372
- package/tools/regex_extractor.py +571 -571
- package/tools/slug_generator.py +310 -310
- package/tools/sorter_tools.py +458 -458
- package/tools/string_escape_tool.py +392 -392
- package/tools/text_statistics_tool.py +365 -365
- package/tools/text_wrapper.py +430 -430
- package/tools/timestamp_converter.py +421 -421
- package/tools/tool_loader.py +710 -710
- package/tools/translator_tools.py +522 -522
- package/tools/url_link_extractor.py +261 -261
- package/tools/url_parser.py +204 -204
- package/tools/whitespace_tools.py +355 -355
- package/tools/word_frequency_counter.py +146 -146
- package/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/__pycache__/app_context.cpython-313.pyc +0 -0
- package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
- package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
- package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
- package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/error_service.cpython-313.pyc +0 -0
- package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
- package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
- package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
- package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
- package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
- package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
- package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
- package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
- package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
- package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
- package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
- package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
- package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
|
@@ -1,619 +1,619 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Statistics Update Manager for Pomera AI Commander.
|
|
3
|
-
|
|
4
|
-
This module provides visibility-aware statistics update coordination to optimize
|
|
5
|
-
performance by skipping updates for hidden tabs, inactive components, and minimized windows.
|
|
6
|
-
|
|
7
|
-
Requirements addressed:
|
|
8
|
-
- 3.1: Skip statistics updates for inactive tabs
|
|
9
|
-
- 3.2: Pause statistics updates when application window is minimized
|
|
10
|
-
- 3.3: Skip calculations when statistics bars are not visible
|
|
11
|
-
- 3.4: Reduce update frequency when user is idle
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
import time
|
|
15
|
-
import threading
|
|
16
|
-
import tkinter as tk
|
|
17
|
-
from typing import Dict, Optional, Callable, Any, List, Set
|
|
18
|
-
from dataclasses import dataclass, field
|
|
19
|
-
from enum import Enum
|
|
20
|
-
from collections import deque
|
|
21
|
-
import weakref
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class UpdatePriority(Enum):
|
|
25
|
-
"""Priority levels for statistics updates."""
|
|
26
|
-
IMMEDIATE = 0 # Update immediately (user just switched to this tab)
|
|
27
|
-
HIGH = 1 # Update soon (active tab, user typing)
|
|
28
|
-
NORMAL = 2 # Update when convenient (visible but not active)
|
|
29
|
-
LOW = 3 # Update when idle (background tab)
|
|
30
|
-
DEFERRED = 4 # Update only when explicitly requested
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class VisibilityState(Enum):
|
|
34
|
-
"""Visibility states for components."""
|
|
35
|
-
VISIBLE_ACTIVE = "visible_active" # Currently visible and active
|
|
36
|
-
VISIBLE_INACTIVE = "visible_inactive" # Visible but not active
|
|
37
|
-
HIDDEN = "hidden" # Not visible (hidden tab)
|
|
38
|
-
MINIMIZED = "minimized" # Window is minimized
|
|
39
|
-
UNKNOWN = "unknown" # State not yet determined
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@dataclass
|
|
43
|
-
class UpdateRequest:
|
|
44
|
-
"""Request for a statistics update."""
|
|
45
|
-
widget_id: str
|
|
46
|
-
priority: UpdatePriority
|
|
47
|
-
timestamp: float = field(default_factory=time.time)
|
|
48
|
-
content_hash: str = ""
|
|
49
|
-
visibility_state: VisibilityState = VisibilityState.UNKNOWN
|
|
50
|
-
callback: Optional[Callable] = None
|
|
51
|
-
|
|
52
|
-
def __post_init__(self):
|
|
53
|
-
if not self.timestamp:
|
|
54
|
-
self.timestamp = time.time()
|
|
55
|
-
|
|
56
|
-
@property
|
|
57
|
-
def age_seconds(self) -> float:
|
|
58
|
-
"""Get the age of this request in seconds."""
|
|
59
|
-
return time.time() - self.timestamp
|
|
60
|
-
|
|
61
|
-
@property
|
|
62
|
-
def is_expired(self) -> bool:
|
|
63
|
-
"""Check if this request has expired (older than 5 seconds)."""
|
|
64
|
-
return self.age_seconds > 5.0
|
|
65
|
-
|
|
66
|
-
def should_batch_with(self, other: 'UpdateRequest') -> bool:
|
|
67
|
-
"""Check if this request can be batched with another."""
|
|
68
|
-
# Can batch if same widget and similar priority
|
|
69
|
-
if self.widget_id != other.widget_id:
|
|
70
|
-
return False
|
|
71
|
-
|
|
72
|
-
# Can batch if priorities are within 1 level
|
|
73
|
-
return abs(self.priority.value - other.priority.value) <= 1
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
@dataclass
|
|
77
|
-
class ComponentInfo:
|
|
78
|
-
"""Information about a component being monitored."""
|
|
79
|
-
component_id: str
|
|
80
|
-
widget_ref: weakref.ref
|
|
81
|
-
visibility_state: VisibilityState = VisibilityState.UNKNOWN
|
|
82
|
-
last_update_time: float = 0.0
|
|
83
|
-
update_count: int = 0
|
|
84
|
-
skip_count: int = 0
|
|
85
|
-
|
|
86
|
-
@property
|
|
87
|
-
def widget(self):
|
|
88
|
-
"""Get the actual widget from the weak reference."""
|
|
89
|
-
return self.widget_ref() if self.widget_ref else None
|
|
90
|
-
|
|
91
|
-
@property
|
|
92
|
-
def is_valid(self) -> bool:
|
|
93
|
-
"""Check if the widget still exists."""
|
|
94
|
-
return self.widget is not None
|
|
95
|
-
|
|
96
|
-
@property
|
|
97
|
-
def time_since_last_update(self) -> float:
|
|
98
|
-
"""Get time since last update in seconds."""
|
|
99
|
-
return time.time() - self.last_update_time if self.last_update_time > 0 else float('inf')
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
class StatisticsUpdateManager:
|
|
103
|
-
"""
|
|
104
|
-
Manages statistics updates with visibility awareness and performance optimization.
|
|
105
|
-
|
|
106
|
-
This class coordinates statistics updates across multiple widgets, implementing
|
|
107
|
-
visibility-aware updates that skip hidden tabs and inactive components, automatic
|
|
108
|
-
pause during window minimization, and priority-based update queuing.
|
|
109
|
-
"""
|
|
110
|
-
|
|
111
|
-
def __init__(self):
|
|
112
|
-
"""Initialize the statistics update manager."""
|
|
113
|
-
# Component registry
|
|
114
|
-
self.components: Dict[str, ComponentInfo] = {}
|
|
115
|
-
|
|
116
|
-
# Update queue organized by priority
|
|
117
|
-
self.update_queues: Dict[UpdatePriority, deque] = {
|
|
118
|
-
priority: deque() for priority in UpdatePriority
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
# Global state
|
|
122
|
-
self.paused = False
|
|
123
|
-
self.window_minimized = False
|
|
124
|
-
self.user_idle = False
|
|
125
|
-
self.last_user_activity = time.time()
|
|
126
|
-
|
|
127
|
-
# Configuration
|
|
128
|
-
self.idle_threshold_seconds = 5.0
|
|
129
|
-
self.min_update_interval_ms = 100 # Minimum time between updates for same widget
|
|
130
|
-
|
|
131
|
-
# Statistics
|
|
132
|
-
self.stats = {
|
|
133
|
-
'updates_requested': 0,
|
|
134
|
-
'updates_executed': 0,
|
|
135
|
-
'updates_skipped_visibility': 0,
|
|
136
|
-
'updates_skipped_paused': 0,
|
|
137
|
-
'updates_batched': 0
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
# Thread safety
|
|
141
|
-
self.lock = threading.RLock()
|
|
142
|
-
|
|
143
|
-
# Tkinter root reference
|
|
144
|
-
self._tk_root: Optional[tk.Tk] = None
|
|
145
|
-
|
|
146
|
-
# Processing state
|
|
147
|
-
self._processing_queue = False
|
|
148
|
-
self._queue_process_after_id: Optional[str] = None
|
|
149
|
-
|
|
150
|
-
def set_tk_root(self, root: tk.Tk):
|
|
151
|
-
"""Set the Tkinter root for scheduling callbacks."""
|
|
152
|
-
self._tk_root = root
|
|
153
|
-
|
|
154
|
-
# Bind window state events
|
|
155
|
-
try:
|
|
156
|
-
root.bind("<Unmap>", self._on_window_minimized)
|
|
157
|
-
root.bind("<Map>", self._on_window_restored)
|
|
158
|
-
except tk.TclError:
|
|
159
|
-
pass # Binding might fail in some cases
|
|
160
|
-
|
|
161
|
-
def register_component(self, component_id: str, widget: tk.Widget,
|
|
162
|
-
initial_visibility: VisibilityState = VisibilityState.VISIBLE_ACTIVE) -> None:
|
|
163
|
-
"""
|
|
164
|
-
Register a component for visibility-aware updates.
|
|
165
|
-
|
|
166
|
-
Args:
|
|
167
|
-
component_id: Unique identifier for the component
|
|
168
|
-
widget: The widget to monitor
|
|
169
|
-
initial_visibility: Initial visibility state
|
|
170
|
-
"""
|
|
171
|
-
with self.lock:
|
|
172
|
-
self.components[component_id] = ComponentInfo(
|
|
173
|
-
component_id=component_id,
|
|
174
|
-
widget_ref=weakref.ref(widget),
|
|
175
|
-
visibility_state=initial_visibility,
|
|
176
|
-
last_update_time=0.0
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
def unregister_component(self, component_id: str) -> None:
|
|
180
|
-
"""
|
|
181
|
-
Unregister a component and remove any pending updates.
|
|
182
|
-
|
|
183
|
-
Args:
|
|
184
|
-
component_id: Identifier of the component to unregister
|
|
185
|
-
"""
|
|
186
|
-
with self.lock:
|
|
187
|
-
# Remove from components
|
|
188
|
-
self.components.pop(component_id, None)
|
|
189
|
-
|
|
190
|
-
# Remove from all update queues
|
|
191
|
-
for queue in self.update_queues.values():
|
|
192
|
-
# Filter out requests for this component
|
|
193
|
-
filtered = deque([req for req in queue if req.widget_id != component_id])
|
|
194
|
-
queue.clear()
|
|
195
|
-
queue.extend(filtered)
|
|
196
|
-
|
|
197
|
-
def set_visibility_state(self, component_id: str, state: VisibilityState) -> None:
|
|
198
|
-
"""
|
|
199
|
-
Set the visibility state for a component.
|
|
200
|
-
|
|
201
|
-
Args:
|
|
202
|
-
component_id: Component identifier
|
|
203
|
-
state: New visibility state
|
|
204
|
-
"""
|
|
205
|
-
with self.lock:
|
|
206
|
-
component = self.components.get(component_id)
|
|
207
|
-
if component:
|
|
208
|
-
old_state = component.visibility_state
|
|
209
|
-
component.visibility_state = state
|
|
210
|
-
|
|
211
|
-
# If component became visible and active, process any pending updates immediately
|
|
212
|
-
if (old_state != VisibilityState.VISIBLE_ACTIVE and
|
|
213
|
-
state == VisibilityState.VISIBLE_ACTIVE):
|
|
214
|
-
self._promote_pending_updates(component_id)
|
|
215
|
-
|
|
216
|
-
def get_visibility_state(self, component_id: str) -> Optional[VisibilityState]:
|
|
217
|
-
"""
|
|
218
|
-
Get the current visibility state of a component.
|
|
219
|
-
|
|
220
|
-
Args:
|
|
221
|
-
component_id: Component identifier
|
|
222
|
-
|
|
223
|
-
Returns:
|
|
224
|
-
Current visibility state or None if component not found
|
|
225
|
-
"""
|
|
226
|
-
with self.lock:
|
|
227
|
-
component = self.components.get(component_id)
|
|
228
|
-
return component.visibility_state if component else None
|
|
229
|
-
|
|
230
|
-
def request_update(self, widget_id: str, priority: UpdatePriority = UpdatePriority.NORMAL,
|
|
231
|
-
callback: Optional[Callable] = None, content_hash: str = "") -> None:
|
|
232
|
-
"""
|
|
233
|
-
Request a statistics update for a widget.
|
|
234
|
-
|
|
235
|
-
Args:
|
|
236
|
-
widget_id: Widget identifier
|
|
237
|
-
priority: Update priority level
|
|
238
|
-
callback: Optional callback to execute for the update
|
|
239
|
-
content_hash: Optional hash of content for deduplication
|
|
240
|
-
"""
|
|
241
|
-
with self.lock:
|
|
242
|
-
self.stats['updates_requested'] += 1
|
|
243
|
-
|
|
244
|
-
# Check if component is registered
|
|
245
|
-
component = self.components.get(widget_id)
|
|
246
|
-
if not component or not component.is_valid:
|
|
247
|
-
return
|
|
248
|
-
|
|
249
|
-
# Check if we should skip this update
|
|
250
|
-
if self._should_skip_update(component, priority):
|
|
251
|
-
self.stats['updates_skipped_visibility'] += 1
|
|
252
|
-
component.skip_count += 1
|
|
253
|
-
return
|
|
254
|
-
|
|
255
|
-
# Check if paused
|
|
256
|
-
if self.paused or self.window_minimized:
|
|
257
|
-
# Only process IMMEDIATE priority updates when paused
|
|
258
|
-
if priority != UpdatePriority.IMMEDIATE:
|
|
259
|
-
self.stats['updates_skipped_paused'] += 1
|
|
260
|
-
return
|
|
261
|
-
|
|
262
|
-
# Create update request
|
|
263
|
-
request = UpdateRequest(
|
|
264
|
-
widget_id=widget_id,
|
|
265
|
-
priority=priority,
|
|
266
|
-
content_hash=content_hash,
|
|
267
|
-
visibility_state=component.visibility_state,
|
|
268
|
-
callback=callback
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
# Add to appropriate queue
|
|
272
|
-
self.update_queues[priority].append(request)
|
|
273
|
-
|
|
274
|
-
# Schedule queue processing
|
|
275
|
-
self._schedule_queue_processing()
|
|
276
|
-
|
|
277
|
-
def _should_skip_update(self, component: ComponentInfo, priority: UpdatePriority) -> bool:
|
|
278
|
-
"""
|
|
279
|
-
Determine if an update should be skipped based on visibility and timing.
|
|
280
|
-
|
|
281
|
-
Args:
|
|
282
|
-
component: Component information
|
|
283
|
-
priority: Update priority
|
|
284
|
-
|
|
285
|
-
Returns:
|
|
286
|
-
True if update should be skipped
|
|
287
|
-
"""
|
|
288
|
-
# Never skip IMMEDIATE priority
|
|
289
|
-
if priority == UpdatePriority.IMMEDIATE:
|
|
290
|
-
return False
|
|
291
|
-
|
|
292
|
-
# Skip if component is hidden
|
|
293
|
-
if component.visibility_state == VisibilityState.HIDDEN:
|
|
294
|
-
return True
|
|
295
|
-
|
|
296
|
-
# Skip if minimized (unless IMMEDIATE)
|
|
297
|
-
if component.visibility_state == VisibilityState.MINIMIZED:
|
|
298
|
-
return True
|
|
299
|
-
|
|
300
|
-
# Check minimum update interval
|
|
301
|
-
if component.time_since_last_update < (self.min_update_interval_ms / 1000.0):
|
|
302
|
-
# Too soon since last update
|
|
303
|
-
return True
|
|
304
|
-
|
|
305
|
-
# Skip low priority updates for inactive components
|
|
306
|
-
if (component.visibility_state == VisibilityState.VISIBLE_INACTIVE and
|
|
307
|
-
priority in [UpdatePriority.LOW, UpdatePriority.DEFERRED]):
|
|
308
|
-
return True
|
|
309
|
-
|
|
310
|
-
return False
|
|
311
|
-
|
|
312
|
-
def _promote_pending_updates(self, component_id: str) -> None:
|
|
313
|
-
"""
|
|
314
|
-
Promote pending updates for a component to higher priority.
|
|
315
|
-
|
|
316
|
-
Called when a component becomes visible and active.
|
|
317
|
-
|
|
318
|
-
Args:
|
|
319
|
-
component_id: Component identifier
|
|
320
|
-
"""
|
|
321
|
-
# Move LOW and DEFERRED updates to NORMAL priority
|
|
322
|
-
for low_priority in [UpdatePriority.LOW, UpdatePriority.DEFERRED]:
|
|
323
|
-
queue = self.update_queues[low_priority]
|
|
324
|
-
promoted = []
|
|
325
|
-
remaining = deque()
|
|
326
|
-
|
|
327
|
-
for request in queue:
|
|
328
|
-
if request.widget_id == component_id:
|
|
329
|
-
# Promote to NORMAL priority
|
|
330
|
-
request.priority = UpdatePriority.NORMAL
|
|
331
|
-
promoted.append(request)
|
|
332
|
-
else:
|
|
333
|
-
remaining.append(request)
|
|
334
|
-
|
|
335
|
-
# Update queue
|
|
336
|
-
queue.clear()
|
|
337
|
-
queue.extend(remaining)
|
|
338
|
-
|
|
339
|
-
# Add promoted requests to NORMAL queue
|
|
340
|
-
self.update_queues[UpdatePriority.NORMAL].extend(promoted)
|
|
341
|
-
|
|
342
|
-
def _schedule_queue_processing(self) -> None:
|
|
343
|
-
"""Schedule processing of the update queue."""
|
|
344
|
-
if self._processing_queue or not self._tk_root:
|
|
345
|
-
return
|
|
346
|
-
|
|
347
|
-
# Cancel any existing scheduled processing
|
|
348
|
-
if self._queue_process_after_id:
|
|
349
|
-
try:
|
|
350
|
-
self._tk_root.after_cancel(self._queue_process_after_id)
|
|
351
|
-
except tk.TclError:
|
|
352
|
-
pass
|
|
353
|
-
|
|
354
|
-
# Schedule new processing
|
|
355
|
-
self._queue_process_after_id = self._tk_root.after(10, self._process_update_queue)
|
|
356
|
-
|
|
357
|
-
def _process_update_queue(self) -> None:
|
|
358
|
-
"""Process pending update requests from the queue."""
|
|
359
|
-
if self._processing_queue:
|
|
360
|
-
return
|
|
361
|
-
|
|
362
|
-
self._processing_queue = True
|
|
363
|
-
|
|
364
|
-
try:
|
|
365
|
-
with self.lock:
|
|
366
|
-
# Process queues in priority order
|
|
367
|
-
for priority in UpdatePriority:
|
|
368
|
-
queue = self.update_queues[priority]
|
|
369
|
-
|
|
370
|
-
# Process up to 5 requests per priority level per cycle
|
|
371
|
-
batch_size = 5 if priority in [UpdatePriority.IMMEDIATE, UpdatePriority.HIGH] else 3
|
|
372
|
-
processed = 0
|
|
373
|
-
|
|
374
|
-
while queue and processed < batch_size:
|
|
375
|
-
request = queue.popleft()
|
|
376
|
-
|
|
377
|
-
# Skip expired requests
|
|
378
|
-
if request.is_expired:
|
|
379
|
-
continue
|
|
380
|
-
|
|
381
|
-
# Execute the update
|
|
382
|
-
self._execute_update(request)
|
|
383
|
-
processed += 1
|
|
384
|
-
|
|
385
|
-
# Check if there are more requests to process
|
|
386
|
-
has_pending = any(len(queue) > 0 for queue in self.update_queues.values())
|
|
387
|
-
|
|
388
|
-
if has_pending:
|
|
389
|
-
# Schedule next processing cycle
|
|
390
|
-
self._queue_process_after_id = None
|
|
391
|
-
self._schedule_queue_processing()
|
|
392
|
-
|
|
393
|
-
finally:
|
|
394
|
-
self._processing_queue = False
|
|
395
|
-
|
|
396
|
-
def _execute_update(self, request: UpdateRequest) -> None:
|
|
397
|
-
"""
|
|
398
|
-
Execute a statistics update request.
|
|
399
|
-
|
|
400
|
-
Args:
|
|
401
|
-
request: Update request to execute
|
|
402
|
-
"""
|
|
403
|
-
component = self.components.get(request.widget_id)
|
|
404
|
-
if not component or not component.is_valid:
|
|
405
|
-
return
|
|
406
|
-
|
|
407
|
-
# Double-check visibility before executing
|
|
408
|
-
if self._should_skip_update(component, request.priority):
|
|
409
|
-
self.stats['updates_skipped_visibility'] += 1
|
|
410
|
-
component.skip_count += 1
|
|
411
|
-
return
|
|
412
|
-
|
|
413
|
-
try:
|
|
414
|
-
# Execute callback if provided
|
|
415
|
-
if request.callback:
|
|
416
|
-
request.callback(request.widget_id)
|
|
417
|
-
|
|
418
|
-
# Update component info
|
|
419
|
-
component.last_update_time = time.time()
|
|
420
|
-
component.update_count += 1
|
|
421
|
-
|
|
422
|
-
self.stats['updates_executed'] += 1
|
|
423
|
-
|
|
424
|
-
except Exception as e:
|
|
425
|
-
# Log error but don't crash
|
|
426
|
-
print(f"Error executing statistics update for {request.widget_id}: {e}")
|
|
427
|
-
|
|
428
|
-
def pause_updates(self, paused: bool = True) -> None:
|
|
429
|
-
"""
|
|
430
|
-
Pause or resume all statistics updates.
|
|
431
|
-
|
|
432
|
-
Args:
|
|
433
|
-
paused: True to pause, False to resume
|
|
434
|
-
"""
|
|
435
|
-
with self.lock:
|
|
436
|
-
self.paused = paused
|
|
437
|
-
|
|
438
|
-
if not paused:
|
|
439
|
-
# Resume - process any pending high-priority updates
|
|
440
|
-
self._schedule_queue_processing()
|
|
441
|
-
|
|
442
|
-
def set_user_idle(self, idle: bool) -> None:
|
|
443
|
-
"""
|
|
444
|
-
Set the user idle state.
|
|
445
|
-
|
|
446
|
-
Args:
|
|
447
|
-
idle: True if user is idle, False if active
|
|
448
|
-
"""
|
|
449
|
-
with self.lock:
|
|
450
|
-
self.user_idle = idle
|
|
451
|
-
if not idle:
|
|
452
|
-
self.last_user_activity = time.time()
|
|
453
|
-
|
|
454
|
-
def mark_user_activity(self) -> None:
|
|
455
|
-
"""Mark that user activity has occurred."""
|
|
456
|
-
self.set_user_idle(False)
|
|
457
|
-
|
|
458
|
-
def _on_window_minimized(self, event=None) -> None:
|
|
459
|
-
"""Handle window minimization event."""
|
|
460
|
-
with self.lock:
|
|
461
|
-
self.window_minimized = True
|
|
462
|
-
|
|
463
|
-
# Update all components to minimized state
|
|
464
|
-
for component in self.components.values():
|
|
465
|
-
if component.visibility_state != VisibilityState.HIDDEN:
|
|
466
|
-
component.visibility_state = VisibilityState.MINIMIZED
|
|
467
|
-
|
|
468
|
-
def _on_window_restored(self, event=None) -> None:
|
|
469
|
-
"""Handle window restoration event."""
|
|
470
|
-
with self.lock:
|
|
471
|
-
self.window_minimized = False
|
|
472
|
-
|
|
473
|
-
# Restore component visibility states (will need to be updated by app)
|
|
474
|
-
# For now, just mark as unknown so they get re-evaluated
|
|
475
|
-
for component in self.components.values():
|
|
476
|
-
if component.visibility_state == VisibilityState.MINIMIZED:
|
|
477
|
-
component.visibility_state = VisibilityState.UNKNOWN
|
|
478
|
-
|
|
479
|
-
# Resume processing
|
|
480
|
-
self._schedule_queue_processing()
|
|
481
|
-
|
|
482
|
-
def get_update_queue_status(self) -> Dict[str, Any]:
|
|
483
|
-
"""
|
|
484
|
-
Get the current status of update queues.
|
|
485
|
-
|
|
486
|
-
Returns:
|
|
487
|
-
Dictionary with queue status information
|
|
488
|
-
"""
|
|
489
|
-
with self.lock:
|
|
490
|
-
queue_sizes = {
|
|
491
|
-
priority.name: len(queue)
|
|
492
|
-
for priority, queue in self.update_queues.items()
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
return {
|
|
496
|
-
'queue_sizes': queue_sizes,
|
|
497
|
-
'total_pending': sum(queue_sizes.values()),
|
|
498
|
-
'paused': self.paused,
|
|
499
|
-
'window_minimized': self.window_minimized,
|
|
500
|
-
'user_idle': self.user_idle,
|
|
501
|
-
'processing': self._processing_queue,
|
|
502
|
-
'registered_components': len(self.components),
|
|
503
|
-
'statistics': self.stats.copy()
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
def get_component_info(self, component_id: str) -> Optional[Dict[str, Any]]:
|
|
507
|
-
"""
|
|
508
|
-
Get information about a registered component.
|
|
509
|
-
|
|
510
|
-
Args:
|
|
511
|
-
component_id: Component identifier
|
|
512
|
-
|
|
513
|
-
Returns:
|
|
514
|
-
Dictionary with component information or None if not found
|
|
515
|
-
"""
|
|
516
|
-
with self.lock:
|
|
517
|
-
component = self.components.get(component_id)
|
|
518
|
-
if not component:
|
|
519
|
-
return None
|
|
520
|
-
|
|
521
|
-
return {
|
|
522
|
-
'component_id': component.component_id,
|
|
523
|
-
'visibility_state': component.visibility_state.value,
|
|
524
|
-
'is_valid': component.is_valid,
|
|
525
|
-
'last_update_time': component.last_update_time,
|
|
526
|
-
'time_since_last_update': component.time_since_last_update,
|
|
527
|
-
'update_count': component.update_count,
|
|
528
|
-
'skip_count': component.skip_count,
|
|
529
|
-
'skip_rate': (component.skip_count / max(1, component.update_count + component.skip_count)) * 100
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
def get_statistics(self) -> Dict[str, Any]:
|
|
533
|
-
"""
|
|
534
|
-
Get statistics about update management.
|
|
535
|
-
|
|
536
|
-
Returns:
|
|
537
|
-
Dictionary with statistics
|
|
538
|
-
"""
|
|
539
|
-
with self.lock:
|
|
540
|
-
stats = self.stats.copy()
|
|
541
|
-
|
|
542
|
-
# Calculate derived statistics
|
|
543
|
-
total_requests = stats['updates_requested']
|
|
544
|
-
if total_requests > 0:
|
|
545
|
-
stats['execution_rate'] = (stats['updates_executed'] / total_requests) * 100
|
|
546
|
-
stats['skip_rate'] = (
|
|
547
|
-
(stats['updates_skipped_visibility'] + stats['updates_skipped_paused']) /
|
|
548
|
-
total_requests
|
|
549
|
-
) * 100
|
|
550
|
-
else:
|
|
551
|
-
stats['execution_rate'] = 0.0
|
|
552
|
-
stats['skip_rate'] = 0.0
|
|
553
|
-
|
|
554
|
-
# Add component statistics
|
|
555
|
-
stats['total_components'] = len(self.components)
|
|
556
|
-
stats['valid_components'] = sum(1 for c in self.components.values() if c.is_valid)
|
|
557
|
-
|
|
558
|
-
# Add visibility breakdown
|
|
559
|
-
visibility_counts = {}
|
|
560
|
-
for component in self.components.values():
|
|
561
|
-
state = component.visibility_state.value
|
|
562
|
-
visibility_counts[state] = visibility_counts.get(state, 0) + 1
|
|
563
|
-
stats['visibility_breakdown'] = visibility_counts
|
|
564
|
-
|
|
565
|
-
return stats
|
|
566
|
-
|
|
567
|
-
def cleanup_invalid_components(self) -> int:
|
|
568
|
-
"""
|
|
569
|
-
Clean up components whose widgets have been destroyed.
|
|
570
|
-
|
|
571
|
-
Returns:
|
|
572
|
-
Number of components cleaned up
|
|
573
|
-
"""
|
|
574
|
-
with self.lock:
|
|
575
|
-
invalid_ids = [
|
|
576
|
-
comp_id for comp_id, comp in self.components.items()
|
|
577
|
-
if not comp.is_valid
|
|
578
|
-
]
|
|
579
|
-
|
|
580
|
-
for comp_id in invalid_ids:
|
|
581
|
-
self.unregister_component(comp_id)
|
|
582
|
-
|
|
583
|
-
return len(invalid_ids)
|
|
584
|
-
|
|
585
|
-
def clear_all_queues(self) -> None:
|
|
586
|
-
"""Clear all pending update requests."""
|
|
587
|
-
with self.lock:
|
|
588
|
-
for queue in self.update_queues.values():
|
|
589
|
-
queue.clear()
|
|
590
|
-
|
|
591
|
-
def force_update_all_visible(self) -> None:
|
|
592
|
-
"""Force immediate update of all visible components."""
|
|
593
|
-
with self.lock:
|
|
594
|
-
for component_id, component in self.components.items():
|
|
595
|
-
if component.visibility_state in [VisibilityState.VISIBLE_ACTIVE,
|
|
596
|
-
VisibilityState.VISIBLE_INACTIVE]:
|
|
597
|
-
self.request_update(component_id, UpdatePriority.IMMEDIATE)
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
# Global instance for easy access
|
|
601
|
-
_global_update_manager: Optional[StatisticsUpdateManager] = None
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
def get_statistics_update_manager() -> StatisticsUpdateManager:
|
|
605
|
-
"""Get the global statistics update manager instance."""
|
|
606
|
-
global _global_update_manager
|
|
607
|
-
if _global_update_manager is None:
|
|
608
|
-
_global_update_manager = StatisticsUpdateManager()
|
|
609
|
-
return _global_update_manager
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
def create_statistics_update_manager() -> StatisticsUpdateManager:
|
|
613
|
-
"""
|
|
614
|
-
Create a new statistics update manager instance.
|
|
615
|
-
|
|
616
|
-
Returns:
|
|
617
|
-
New StatisticsUpdateManager instance
|
|
618
|
-
"""
|
|
619
|
-
return StatisticsUpdateManager()
|
|
1
|
+
"""
|
|
2
|
+
Statistics Update Manager for Pomera AI Commander.
|
|
3
|
+
|
|
4
|
+
This module provides visibility-aware statistics update coordination to optimize
|
|
5
|
+
performance by skipping updates for hidden tabs, inactive components, and minimized windows.
|
|
6
|
+
|
|
7
|
+
Requirements addressed:
|
|
8
|
+
- 3.1: Skip statistics updates for inactive tabs
|
|
9
|
+
- 3.2: Pause statistics updates when application window is minimized
|
|
10
|
+
- 3.3: Skip calculations when statistics bars are not visible
|
|
11
|
+
- 3.4: Reduce update frequency when user is idle
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
import threading
|
|
16
|
+
import tkinter as tk
|
|
17
|
+
from typing import Dict, Optional, Callable, Any, List, Set
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from collections import deque
|
|
21
|
+
import weakref
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UpdatePriority(Enum):
|
|
25
|
+
"""Priority levels for statistics updates."""
|
|
26
|
+
IMMEDIATE = 0 # Update immediately (user just switched to this tab)
|
|
27
|
+
HIGH = 1 # Update soon (active tab, user typing)
|
|
28
|
+
NORMAL = 2 # Update when convenient (visible but not active)
|
|
29
|
+
LOW = 3 # Update when idle (background tab)
|
|
30
|
+
DEFERRED = 4 # Update only when explicitly requested
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class VisibilityState(Enum):
|
|
34
|
+
"""Visibility states for components."""
|
|
35
|
+
VISIBLE_ACTIVE = "visible_active" # Currently visible and active
|
|
36
|
+
VISIBLE_INACTIVE = "visible_inactive" # Visible but not active
|
|
37
|
+
HIDDEN = "hidden" # Not visible (hidden tab)
|
|
38
|
+
MINIMIZED = "minimized" # Window is minimized
|
|
39
|
+
UNKNOWN = "unknown" # State not yet determined
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class UpdateRequest:
|
|
44
|
+
"""Request for a statistics update."""
|
|
45
|
+
widget_id: str
|
|
46
|
+
priority: UpdatePriority
|
|
47
|
+
timestamp: float = field(default_factory=time.time)
|
|
48
|
+
content_hash: str = ""
|
|
49
|
+
visibility_state: VisibilityState = VisibilityState.UNKNOWN
|
|
50
|
+
callback: Optional[Callable] = None
|
|
51
|
+
|
|
52
|
+
def __post_init__(self):
|
|
53
|
+
if not self.timestamp:
|
|
54
|
+
self.timestamp = time.time()
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def age_seconds(self) -> float:
|
|
58
|
+
"""Get the age of this request in seconds."""
|
|
59
|
+
return time.time() - self.timestamp
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def is_expired(self) -> bool:
|
|
63
|
+
"""Check if this request has expired (older than 5 seconds)."""
|
|
64
|
+
return self.age_seconds > 5.0
|
|
65
|
+
|
|
66
|
+
def should_batch_with(self, other: 'UpdateRequest') -> bool:
|
|
67
|
+
"""Check if this request can be batched with another."""
|
|
68
|
+
# Can batch if same widget and similar priority
|
|
69
|
+
if self.widget_id != other.widget_id:
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
# Can batch if priorities are within 1 level
|
|
73
|
+
return abs(self.priority.value - other.priority.value) <= 1
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class ComponentInfo:
|
|
78
|
+
"""Information about a component being monitored."""
|
|
79
|
+
component_id: str
|
|
80
|
+
widget_ref: weakref.ref
|
|
81
|
+
visibility_state: VisibilityState = VisibilityState.UNKNOWN
|
|
82
|
+
last_update_time: float = 0.0
|
|
83
|
+
update_count: int = 0
|
|
84
|
+
skip_count: int = 0
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def widget(self):
|
|
88
|
+
"""Get the actual widget from the weak reference."""
|
|
89
|
+
return self.widget_ref() if self.widget_ref else None
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def is_valid(self) -> bool:
|
|
93
|
+
"""Check if the widget still exists."""
|
|
94
|
+
return self.widget is not None
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def time_since_last_update(self) -> float:
|
|
98
|
+
"""Get time since last update in seconds."""
|
|
99
|
+
return time.time() - self.last_update_time if self.last_update_time > 0 else float('inf')
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class StatisticsUpdateManager:
|
|
103
|
+
"""
|
|
104
|
+
Manages statistics updates with visibility awareness and performance optimization.
|
|
105
|
+
|
|
106
|
+
This class coordinates statistics updates across multiple widgets, implementing
|
|
107
|
+
visibility-aware updates that skip hidden tabs and inactive components, automatic
|
|
108
|
+
pause during window minimization, and priority-based update queuing.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self):
|
|
112
|
+
"""Initialize the statistics update manager."""
|
|
113
|
+
# Component registry
|
|
114
|
+
self.components: Dict[str, ComponentInfo] = {}
|
|
115
|
+
|
|
116
|
+
# Update queue organized by priority
|
|
117
|
+
self.update_queues: Dict[UpdatePriority, deque] = {
|
|
118
|
+
priority: deque() for priority in UpdatePriority
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Global state
|
|
122
|
+
self.paused = False
|
|
123
|
+
self.window_minimized = False
|
|
124
|
+
self.user_idle = False
|
|
125
|
+
self.last_user_activity = time.time()
|
|
126
|
+
|
|
127
|
+
# Configuration
|
|
128
|
+
self.idle_threshold_seconds = 5.0
|
|
129
|
+
self.min_update_interval_ms = 100 # Minimum time between updates for same widget
|
|
130
|
+
|
|
131
|
+
# Statistics
|
|
132
|
+
self.stats = {
|
|
133
|
+
'updates_requested': 0,
|
|
134
|
+
'updates_executed': 0,
|
|
135
|
+
'updates_skipped_visibility': 0,
|
|
136
|
+
'updates_skipped_paused': 0,
|
|
137
|
+
'updates_batched': 0
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Thread safety
|
|
141
|
+
self.lock = threading.RLock()
|
|
142
|
+
|
|
143
|
+
# Tkinter root reference
|
|
144
|
+
self._tk_root: Optional[tk.Tk] = None
|
|
145
|
+
|
|
146
|
+
# Processing state
|
|
147
|
+
self._processing_queue = False
|
|
148
|
+
self._queue_process_after_id: Optional[str] = None
|
|
149
|
+
|
|
150
|
+
def set_tk_root(self, root: tk.Tk):
|
|
151
|
+
"""Set the Tkinter root for scheduling callbacks."""
|
|
152
|
+
self._tk_root = root
|
|
153
|
+
|
|
154
|
+
# Bind window state events
|
|
155
|
+
try:
|
|
156
|
+
root.bind("<Unmap>", self._on_window_minimized)
|
|
157
|
+
root.bind("<Map>", self._on_window_restored)
|
|
158
|
+
except tk.TclError:
|
|
159
|
+
pass # Binding might fail in some cases
|
|
160
|
+
|
|
161
|
+
def register_component(self, component_id: str, widget: tk.Widget,
|
|
162
|
+
initial_visibility: VisibilityState = VisibilityState.VISIBLE_ACTIVE) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Register a component for visibility-aware updates.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
component_id: Unique identifier for the component
|
|
168
|
+
widget: The widget to monitor
|
|
169
|
+
initial_visibility: Initial visibility state
|
|
170
|
+
"""
|
|
171
|
+
with self.lock:
|
|
172
|
+
self.components[component_id] = ComponentInfo(
|
|
173
|
+
component_id=component_id,
|
|
174
|
+
widget_ref=weakref.ref(widget),
|
|
175
|
+
visibility_state=initial_visibility,
|
|
176
|
+
last_update_time=0.0
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def unregister_component(self, component_id: str) -> None:
|
|
180
|
+
"""
|
|
181
|
+
Unregister a component and remove any pending updates.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
component_id: Identifier of the component to unregister
|
|
185
|
+
"""
|
|
186
|
+
with self.lock:
|
|
187
|
+
# Remove from components
|
|
188
|
+
self.components.pop(component_id, None)
|
|
189
|
+
|
|
190
|
+
# Remove from all update queues
|
|
191
|
+
for queue in self.update_queues.values():
|
|
192
|
+
# Filter out requests for this component
|
|
193
|
+
filtered = deque([req for req in queue if req.widget_id != component_id])
|
|
194
|
+
queue.clear()
|
|
195
|
+
queue.extend(filtered)
|
|
196
|
+
|
|
197
|
+
def set_visibility_state(self, component_id: str, state: VisibilityState) -> None:
|
|
198
|
+
"""
|
|
199
|
+
Set the visibility state for a component.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
component_id: Component identifier
|
|
203
|
+
state: New visibility state
|
|
204
|
+
"""
|
|
205
|
+
with self.lock:
|
|
206
|
+
component = self.components.get(component_id)
|
|
207
|
+
if component:
|
|
208
|
+
old_state = component.visibility_state
|
|
209
|
+
component.visibility_state = state
|
|
210
|
+
|
|
211
|
+
# If component became visible and active, process any pending updates immediately
|
|
212
|
+
if (old_state != VisibilityState.VISIBLE_ACTIVE and
|
|
213
|
+
state == VisibilityState.VISIBLE_ACTIVE):
|
|
214
|
+
self._promote_pending_updates(component_id)
|
|
215
|
+
|
|
216
|
+
def get_visibility_state(self, component_id: str) -> Optional[VisibilityState]:
|
|
217
|
+
"""
|
|
218
|
+
Get the current visibility state of a component.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
component_id: Component identifier
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Current visibility state or None if component not found
|
|
225
|
+
"""
|
|
226
|
+
with self.lock:
|
|
227
|
+
component = self.components.get(component_id)
|
|
228
|
+
return component.visibility_state if component else None
|
|
229
|
+
|
|
230
|
+
def request_update(self, widget_id: str, priority: UpdatePriority = UpdatePriority.NORMAL,
|
|
231
|
+
callback: Optional[Callable] = None, content_hash: str = "") -> None:
|
|
232
|
+
"""
|
|
233
|
+
Request a statistics update for a widget.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
widget_id: Widget identifier
|
|
237
|
+
priority: Update priority level
|
|
238
|
+
callback: Optional callback to execute for the update
|
|
239
|
+
content_hash: Optional hash of content for deduplication
|
|
240
|
+
"""
|
|
241
|
+
with self.lock:
|
|
242
|
+
self.stats['updates_requested'] += 1
|
|
243
|
+
|
|
244
|
+
# Check if component is registered
|
|
245
|
+
component = self.components.get(widget_id)
|
|
246
|
+
if not component or not component.is_valid:
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
# Check if we should skip this update
|
|
250
|
+
if self._should_skip_update(component, priority):
|
|
251
|
+
self.stats['updates_skipped_visibility'] += 1
|
|
252
|
+
component.skip_count += 1
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
# Check if paused
|
|
256
|
+
if self.paused or self.window_minimized:
|
|
257
|
+
# Only process IMMEDIATE priority updates when paused
|
|
258
|
+
if priority != UpdatePriority.IMMEDIATE:
|
|
259
|
+
self.stats['updates_skipped_paused'] += 1
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
# Create update request
|
|
263
|
+
request = UpdateRequest(
|
|
264
|
+
widget_id=widget_id,
|
|
265
|
+
priority=priority,
|
|
266
|
+
content_hash=content_hash,
|
|
267
|
+
visibility_state=component.visibility_state,
|
|
268
|
+
callback=callback
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Add to appropriate queue
|
|
272
|
+
self.update_queues[priority].append(request)
|
|
273
|
+
|
|
274
|
+
# Schedule queue processing
|
|
275
|
+
self._schedule_queue_processing()
|
|
276
|
+
|
|
277
|
+
def _should_skip_update(self, component: ComponentInfo, priority: UpdatePriority) -> bool:
|
|
278
|
+
"""
|
|
279
|
+
Determine if an update should be skipped based on visibility and timing.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
component: Component information
|
|
283
|
+
priority: Update priority
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
True if update should be skipped
|
|
287
|
+
"""
|
|
288
|
+
# Never skip IMMEDIATE priority
|
|
289
|
+
if priority == UpdatePriority.IMMEDIATE:
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
# Skip if component is hidden
|
|
293
|
+
if component.visibility_state == VisibilityState.HIDDEN:
|
|
294
|
+
return True
|
|
295
|
+
|
|
296
|
+
# Skip if minimized (unless IMMEDIATE)
|
|
297
|
+
if component.visibility_state == VisibilityState.MINIMIZED:
|
|
298
|
+
return True
|
|
299
|
+
|
|
300
|
+
# Check minimum update interval
|
|
301
|
+
if component.time_since_last_update < (self.min_update_interval_ms / 1000.0):
|
|
302
|
+
# Too soon since last update
|
|
303
|
+
return True
|
|
304
|
+
|
|
305
|
+
# Skip low priority updates for inactive components
|
|
306
|
+
if (component.visibility_state == VisibilityState.VISIBLE_INACTIVE and
|
|
307
|
+
priority in [UpdatePriority.LOW, UpdatePriority.DEFERRED]):
|
|
308
|
+
return True
|
|
309
|
+
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
def _promote_pending_updates(self, component_id: str) -> None:
|
|
313
|
+
"""
|
|
314
|
+
Promote pending updates for a component to higher priority.
|
|
315
|
+
|
|
316
|
+
Called when a component becomes visible and active.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
component_id: Component identifier
|
|
320
|
+
"""
|
|
321
|
+
# Move LOW and DEFERRED updates to NORMAL priority
|
|
322
|
+
for low_priority in [UpdatePriority.LOW, UpdatePriority.DEFERRED]:
|
|
323
|
+
queue = self.update_queues[low_priority]
|
|
324
|
+
promoted = []
|
|
325
|
+
remaining = deque()
|
|
326
|
+
|
|
327
|
+
for request in queue:
|
|
328
|
+
if request.widget_id == component_id:
|
|
329
|
+
# Promote to NORMAL priority
|
|
330
|
+
request.priority = UpdatePriority.NORMAL
|
|
331
|
+
promoted.append(request)
|
|
332
|
+
else:
|
|
333
|
+
remaining.append(request)
|
|
334
|
+
|
|
335
|
+
# Update queue
|
|
336
|
+
queue.clear()
|
|
337
|
+
queue.extend(remaining)
|
|
338
|
+
|
|
339
|
+
# Add promoted requests to NORMAL queue
|
|
340
|
+
self.update_queues[UpdatePriority.NORMAL].extend(promoted)
|
|
341
|
+
|
|
342
|
+
def _schedule_queue_processing(self) -> None:
|
|
343
|
+
"""Schedule processing of the update queue."""
|
|
344
|
+
if self._processing_queue or not self._tk_root:
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
# Cancel any existing scheduled processing
|
|
348
|
+
if self._queue_process_after_id:
|
|
349
|
+
try:
|
|
350
|
+
self._tk_root.after_cancel(self._queue_process_after_id)
|
|
351
|
+
except tk.TclError:
|
|
352
|
+
pass
|
|
353
|
+
|
|
354
|
+
# Schedule new processing
|
|
355
|
+
self._queue_process_after_id = self._tk_root.after(10, self._process_update_queue)
|
|
356
|
+
|
|
357
|
+
def _process_update_queue(self) -> None:
|
|
358
|
+
"""Process pending update requests from the queue."""
|
|
359
|
+
if self._processing_queue:
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
self._processing_queue = True
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
with self.lock:
|
|
366
|
+
# Process queues in priority order
|
|
367
|
+
for priority in UpdatePriority:
|
|
368
|
+
queue = self.update_queues[priority]
|
|
369
|
+
|
|
370
|
+
# Process up to 5 requests per priority level per cycle
|
|
371
|
+
batch_size = 5 if priority in [UpdatePriority.IMMEDIATE, UpdatePriority.HIGH] else 3
|
|
372
|
+
processed = 0
|
|
373
|
+
|
|
374
|
+
while queue and processed < batch_size:
|
|
375
|
+
request = queue.popleft()
|
|
376
|
+
|
|
377
|
+
# Skip expired requests
|
|
378
|
+
if request.is_expired:
|
|
379
|
+
continue
|
|
380
|
+
|
|
381
|
+
# Execute the update
|
|
382
|
+
self._execute_update(request)
|
|
383
|
+
processed += 1
|
|
384
|
+
|
|
385
|
+
# Check if there are more requests to process
|
|
386
|
+
has_pending = any(len(queue) > 0 for queue in self.update_queues.values())
|
|
387
|
+
|
|
388
|
+
if has_pending:
|
|
389
|
+
# Schedule next processing cycle
|
|
390
|
+
self._queue_process_after_id = None
|
|
391
|
+
self._schedule_queue_processing()
|
|
392
|
+
|
|
393
|
+
finally:
|
|
394
|
+
self._processing_queue = False
|
|
395
|
+
|
|
396
|
+
def _execute_update(self, request: UpdateRequest) -> None:
|
|
397
|
+
"""
|
|
398
|
+
Execute a statistics update request.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
request: Update request to execute
|
|
402
|
+
"""
|
|
403
|
+
component = self.components.get(request.widget_id)
|
|
404
|
+
if not component or not component.is_valid:
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
# Double-check visibility before executing
|
|
408
|
+
if self._should_skip_update(component, request.priority):
|
|
409
|
+
self.stats['updates_skipped_visibility'] += 1
|
|
410
|
+
component.skip_count += 1
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
# Execute callback if provided
|
|
415
|
+
if request.callback:
|
|
416
|
+
request.callback(request.widget_id)
|
|
417
|
+
|
|
418
|
+
# Update component info
|
|
419
|
+
component.last_update_time = time.time()
|
|
420
|
+
component.update_count += 1
|
|
421
|
+
|
|
422
|
+
self.stats['updates_executed'] += 1
|
|
423
|
+
|
|
424
|
+
except Exception as e:
|
|
425
|
+
# Log error but don't crash
|
|
426
|
+
print(f"Error executing statistics update for {request.widget_id}: {e}")
|
|
427
|
+
|
|
428
|
+
def pause_updates(self, paused: bool = True) -> None:
|
|
429
|
+
"""
|
|
430
|
+
Pause or resume all statistics updates.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
paused: True to pause, False to resume
|
|
434
|
+
"""
|
|
435
|
+
with self.lock:
|
|
436
|
+
self.paused = paused
|
|
437
|
+
|
|
438
|
+
if not paused:
|
|
439
|
+
# Resume - process any pending high-priority updates
|
|
440
|
+
self._schedule_queue_processing()
|
|
441
|
+
|
|
442
|
+
def set_user_idle(self, idle: bool) -> None:
|
|
443
|
+
"""
|
|
444
|
+
Set the user idle state.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
idle: True if user is idle, False if active
|
|
448
|
+
"""
|
|
449
|
+
with self.lock:
|
|
450
|
+
self.user_idle = idle
|
|
451
|
+
if not idle:
|
|
452
|
+
self.last_user_activity = time.time()
|
|
453
|
+
|
|
454
|
+
def mark_user_activity(self) -> None:
|
|
455
|
+
"""Mark that user activity has occurred."""
|
|
456
|
+
self.set_user_idle(False)
|
|
457
|
+
|
|
458
|
+
def _on_window_minimized(self, event=None) -> None:
|
|
459
|
+
"""Handle window minimization event."""
|
|
460
|
+
with self.lock:
|
|
461
|
+
self.window_minimized = True
|
|
462
|
+
|
|
463
|
+
# Update all components to minimized state
|
|
464
|
+
for component in self.components.values():
|
|
465
|
+
if component.visibility_state != VisibilityState.HIDDEN:
|
|
466
|
+
component.visibility_state = VisibilityState.MINIMIZED
|
|
467
|
+
|
|
468
|
+
def _on_window_restored(self, event=None) -> None:
|
|
469
|
+
"""Handle window restoration event."""
|
|
470
|
+
with self.lock:
|
|
471
|
+
self.window_minimized = False
|
|
472
|
+
|
|
473
|
+
# Restore component visibility states (will need to be updated by app)
|
|
474
|
+
# For now, just mark as unknown so they get re-evaluated
|
|
475
|
+
for component in self.components.values():
|
|
476
|
+
if component.visibility_state == VisibilityState.MINIMIZED:
|
|
477
|
+
component.visibility_state = VisibilityState.UNKNOWN
|
|
478
|
+
|
|
479
|
+
# Resume processing
|
|
480
|
+
self._schedule_queue_processing()
|
|
481
|
+
|
|
482
|
+
def get_update_queue_status(self) -> Dict[str, Any]:
|
|
483
|
+
"""
|
|
484
|
+
Get the current status of update queues.
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
Dictionary with queue status information
|
|
488
|
+
"""
|
|
489
|
+
with self.lock:
|
|
490
|
+
queue_sizes = {
|
|
491
|
+
priority.name: len(queue)
|
|
492
|
+
for priority, queue in self.update_queues.items()
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
'queue_sizes': queue_sizes,
|
|
497
|
+
'total_pending': sum(queue_sizes.values()),
|
|
498
|
+
'paused': self.paused,
|
|
499
|
+
'window_minimized': self.window_minimized,
|
|
500
|
+
'user_idle': self.user_idle,
|
|
501
|
+
'processing': self._processing_queue,
|
|
502
|
+
'registered_components': len(self.components),
|
|
503
|
+
'statistics': self.stats.copy()
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
def get_component_info(self, component_id: str) -> Optional[Dict[str, Any]]:
|
|
507
|
+
"""
|
|
508
|
+
Get information about a registered component.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
component_id: Component identifier
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
Dictionary with component information or None if not found
|
|
515
|
+
"""
|
|
516
|
+
with self.lock:
|
|
517
|
+
component = self.components.get(component_id)
|
|
518
|
+
if not component:
|
|
519
|
+
return None
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
'component_id': component.component_id,
|
|
523
|
+
'visibility_state': component.visibility_state.value,
|
|
524
|
+
'is_valid': component.is_valid,
|
|
525
|
+
'last_update_time': component.last_update_time,
|
|
526
|
+
'time_since_last_update': component.time_since_last_update,
|
|
527
|
+
'update_count': component.update_count,
|
|
528
|
+
'skip_count': component.skip_count,
|
|
529
|
+
'skip_rate': (component.skip_count / max(1, component.update_count + component.skip_count)) * 100
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
533
|
+
"""
|
|
534
|
+
Get statistics about update management.
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
Dictionary with statistics
|
|
538
|
+
"""
|
|
539
|
+
with self.lock:
|
|
540
|
+
stats = self.stats.copy()
|
|
541
|
+
|
|
542
|
+
# Calculate derived statistics
|
|
543
|
+
total_requests = stats['updates_requested']
|
|
544
|
+
if total_requests > 0:
|
|
545
|
+
stats['execution_rate'] = (stats['updates_executed'] / total_requests) * 100
|
|
546
|
+
stats['skip_rate'] = (
|
|
547
|
+
(stats['updates_skipped_visibility'] + stats['updates_skipped_paused']) /
|
|
548
|
+
total_requests
|
|
549
|
+
) * 100
|
|
550
|
+
else:
|
|
551
|
+
stats['execution_rate'] = 0.0
|
|
552
|
+
stats['skip_rate'] = 0.0
|
|
553
|
+
|
|
554
|
+
# Add component statistics
|
|
555
|
+
stats['total_components'] = len(self.components)
|
|
556
|
+
stats['valid_components'] = sum(1 for c in self.components.values() if c.is_valid)
|
|
557
|
+
|
|
558
|
+
# Add visibility breakdown
|
|
559
|
+
visibility_counts = {}
|
|
560
|
+
for component in self.components.values():
|
|
561
|
+
state = component.visibility_state.value
|
|
562
|
+
visibility_counts[state] = visibility_counts.get(state, 0) + 1
|
|
563
|
+
stats['visibility_breakdown'] = visibility_counts
|
|
564
|
+
|
|
565
|
+
return stats
|
|
566
|
+
|
|
567
|
+
def cleanup_invalid_components(self) -> int:
|
|
568
|
+
"""
|
|
569
|
+
Clean up components whose widgets have been destroyed.
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
Number of components cleaned up
|
|
573
|
+
"""
|
|
574
|
+
with self.lock:
|
|
575
|
+
invalid_ids = [
|
|
576
|
+
comp_id for comp_id, comp in self.components.items()
|
|
577
|
+
if not comp.is_valid
|
|
578
|
+
]
|
|
579
|
+
|
|
580
|
+
for comp_id in invalid_ids:
|
|
581
|
+
self.unregister_component(comp_id)
|
|
582
|
+
|
|
583
|
+
return len(invalid_ids)
|
|
584
|
+
|
|
585
|
+
def clear_all_queues(self) -> None:
|
|
586
|
+
"""Clear all pending update requests."""
|
|
587
|
+
with self.lock:
|
|
588
|
+
for queue in self.update_queues.values():
|
|
589
|
+
queue.clear()
|
|
590
|
+
|
|
591
|
+
def force_update_all_visible(self) -> None:
|
|
592
|
+
"""Force immediate update of all visible components."""
|
|
593
|
+
with self.lock:
|
|
594
|
+
for component_id, component in self.components.items():
|
|
595
|
+
if component.visibility_state in [VisibilityState.VISIBLE_ACTIVE,
|
|
596
|
+
VisibilityState.VISIBLE_INACTIVE]:
|
|
597
|
+
self.request_update(component_id, UpdatePriority.IMMEDIATE)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
# Global instance for easy access
|
|
601
|
+
_global_update_manager: Optional[StatisticsUpdateManager] = None
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def get_statistics_update_manager() -> StatisticsUpdateManager:
|
|
605
|
+
"""Get the global statistics update manager instance."""
|
|
606
|
+
global _global_update_manager
|
|
607
|
+
if _global_update_manager is None:
|
|
608
|
+
_global_update_manager = StatisticsUpdateManager()
|
|
609
|
+
return _global_update_manager
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def create_statistics_update_manager() -> StatisticsUpdateManager:
|
|
613
|
+
"""
|
|
614
|
+
Create a new statistics update manager instance.
|
|
615
|
+
|
|
616
|
+
Returns:
|
|
617
|
+
New StatisticsUpdateManager instance
|
|
618
|
+
"""
|
|
619
|
+
return StatisticsUpdateManager()
|