portacode 1.3.30__tar.gz → 1.3.31__tar.gz
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.
Potentially problematic release.
This version of portacode might be problematic. Click here for more details.
- {portacode-1.3.30 → portacode-1.3.31}/.claude/settings.local.json +3 -2
- {portacode-1.3.30 → portacode-1.3.31}/PKG-INFO +1 -1
- {portacode-1.3.30 → portacode-1.3.31}/portacode/_version.py +2 -2
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_state/file_system_watcher.py +24 -10
- {portacode-1.3.30 → portacode-1.3.31}/portacode.egg-info/PKG-INFO +1 -1
- {portacode-1.3.30 → portacode-1.3.31}/portacode.egg-info/SOURCES.txt +4 -0
- portacode-1.3.31/todo/agent_context_management.md +12 -0
- portacode-1.3.31/todo/issues/device_performance_degradation.md +129 -0
- portacode-1.3.31/todo/issues/git_data_not_captured_in_proxmox.md +2004 -0
- portacode-1.3.31/todo/issues/terminals_exit_upon_starting.md +3 -0
- {portacode-1.3.30 → portacode-1.3.31}/.claude/agents/communication-manager.md +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/.gitignore +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/.gitmodules +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/LICENSE +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/MANIFEST.in +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/Makefile +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/README.md +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/backup.sh +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/connect.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/connect.sh +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/docker-compose.yaml +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/README.md +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/__init__.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/__main__.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/cli.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/README.md +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/__init__.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/client.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/README.md +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/__init__.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/base.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/chunked_content.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/file_handlers.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_aware_file_handlers.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_state/README.md +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_state/__init__.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_state/git_manager.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_state/handlers.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_state/manager.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_state/models.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_state/utils.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_state_handlers.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/registry.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/session.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/system_handlers.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/tab_factory.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/terminal_handlers.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/multiplex.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/terminal.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/data.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/keypair.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/logging_categories.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/service.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/static/js/test-ntp-clock.html +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/static/js/utils/ntp-clock.js +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/utils/NTP_ARCHITECTURE.md +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/utils/__init__.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode/utils/ntp_clock.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode.egg-info/dependency_links.txt +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode.egg-info/entry_points.txt +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode.egg-info/requires.txt +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/portacode.egg-info/top_level.txt +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/pyproject.toml +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/restore.sh +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/run_tests.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/setup.cfg +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/setup.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/test.sh +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/test_modules/README.md +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/test_modules/__init__.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/test_modules/test_device_online.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/test_modules/test_file_operations.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/test_modules/test_git_status_ui.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/test_modules/test_login_flow.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/test_modules/test_navigate_testing_folder.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/test_modules/test_terminal_buffer_performance.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/test_modules/test_terminal_interaction.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/test_modules/test_terminal_loading_race_condition.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/test_modules/test_terminal_start.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/test_request_id.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/testing_framework/.env.example +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/testing_framework/README.md +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/testing_framework/__init__.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/testing_framework/cli.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/testing_framework/core/__init__.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/testing_framework/core/base_test.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/testing_framework/core/cli_manager.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/testing_framework/core/hierarchical_runner.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/testing_framework/core/playwright_manager.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/testing_framework/core/runner.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/testing_framework/core/shared_cli_manager.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/testing_framework/core/test_discovery.py +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/testing_framework/requirements.txt +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/todo/issues/indefinite_resource_loading.md +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/todo/issues/premature_terminal_exit.md +0 -0
- {portacode-1.3.30 → portacode-1.3.31}/tools/test_python_ntp_clock.py +0 -0
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
"Bash(mkdir:*)",
|
|
16
16
|
"Bash(./connect.sh)",
|
|
17
17
|
"Bash(git -C /home/menas/testing_folder status --porcelain)",
|
|
18
|
-
"Bash(docker-compose restart:*)"
|
|
18
|
+
"Bash(docker-compose restart:*)",
|
|
19
|
+
"Bash(./debug/list_user_devices_and_projects.sh:*)"
|
|
19
20
|
],
|
|
20
21
|
"deny": []
|
|
21
22
|
}
|
|
22
|
-
}
|
|
23
|
+
}
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.3.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 3,
|
|
31
|
+
__version__ = version = '1.3.31'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 3, 31)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -28,12 +28,13 @@ except ImportError:
|
|
|
28
28
|
|
|
29
29
|
class FileSystemWatcher:
|
|
30
30
|
"""Watches file system changes for project folders."""
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
def __init__(self, project_manager):
|
|
33
33
|
self.project_manager = project_manager # Reference to ProjectStateManager
|
|
34
34
|
self.observer: Optional[Observer] = None
|
|
35
35
|
self.event_handler: Optional[FileSystemEventHandler] = None
|
|
36
36
|
self.watched_paths: Set[str] = set()
|
|
37
|
+
self.watch_handles: dict = {} # Map path -> watch handle for proper cleanup
|
|
37
38
|
# Store reference to the event loop for thread-safe async task creation
|
|
38
39
|
try:
|
|
39
40
|
self.event_loop = asyncio.get_running_loop()
|
|
@@ -140,14 +141,15 @@ class FileSystemWatcher:
|
|
|
140
141
|
if not WATCHDOG_AVAILABLE or not self.observer:
|
|
141
142
|
logger.warning("Watchdog not available, cannot start watching: %s", path)
|
|
142
143
|
return
|
|
143
|
-
|
|
144
|
+
|
|
144
145
|
if path not in self.watched_paths:
|
|
145
146
|
try:
|
|
146
147
|
# Use recursive=False to watch only direct contents of each folder
|
|
147
|
-
self.observer.schedule(self.event_handler, path, recursive=False)
|
|
148
|
+
watch_handle = self.observer.schedule(self.event_handler, path, recursive=False)
|
|
148
149
|
self.watched_paths.add(path)
|
|
150
|
+
self.watch_handles[path] = watch_handle # Store handle for cleanup
|
|
149
151
|
logger.info("Started watching path (non-recursive): %s", path)
|
|
150
|
-
|
|
152
|
+
|
|
151
153
|
if not self.observer.is_alive():
|
|
152
154
|
self.observer.start()
|
|
153
155
|
logger.info("Started file system observer")
|
|
@@ -161,14 +163,15 @@ class FileSystemWatcher:
|
|
|
161
163
|
if not WATCHDOG_AVAILABLE or not self.observer:
|
|
162
164
|
logger.warning("Watchdog not available, cannot start watching git directory: %s", git_path)
|
|
163
165
|
return
|
|
164
|
-
|
|
166
|
+
|
|
165
167
|
if git_path not in self.watched_paths:
|
|
166
168
|
try:
|
|
167
169
|
# Watch .git directory recursively to catch changes in refs/, logs/, etc.
|
|
168
|
-
self.observer.schedule(self.event_handler, git_path, recursive=True)
|
|
170
|
+
watch_handle = self.observer.schedule(self.event_handler, git_path, recursive=True)
|
|
169
171
|
self.watched_paths.add(git_path)
|
|
172
|
+
self.watch_handles[git_path] = watch_handle # Store handle for cleanup
|
|
170
173
|
logger.info("Started watching git directory (recursive): %s", git_path)
|
|
171
|
-
|
|
174
|
+
|
|
172
175
|
if not self.observer.is_alive():
|
|
173
176
|
self.observer.start()
|
|
174
177
|
logger.info("Started file system observer")
|
|
@@ -181,9 +184,19 @@ class FileSystemWatcher:
|
|
|
181
184
|
"""Stop watching a specific path."""
|
|
182
185
|
if not WATCHDOG_AVAILABLE or not self.observer:
|
|
183
186
|
return
|
|
184
|
-
|
|
187
|
+
|
|
185
188
|
if path in self.watched_paths:
|
|
186
|
-
#
|
|
189
|
+
# Actually unschedule the watch using stored handle
|
|
190
|
+
watch_handle = self.watch_handles.get(path)
|
|
191
|
+
if watch_handle:
|
|
192
|
+
try:
|
|
193
|
+
self.observer.unschedule(watch_handle)
|
|
194
|
+
logger.info("Successfully unscheduled watch for: %s", path)
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.error("Error unscheduling watch for %s: %s", path, e)
|
|
197
|
+
finally:
|
|
198
|
+
self.watch_handles.pop(path, None)
|
|
199
|
+
|
|
187
200
|
self.watched_paths.discard(path)
|
|
188
201
|
logger.debug("Stopped watching path: %s", path)
|
|
189
202
|
|
|
@@ -192,4 +205,5 @@ class FileSystemWatcher:
|
|
|
192
205
|
if self.observer and self.observer.is_alive():
|
|
193
206
|
self.observer.stop()
|
|
194
207
|
self.observer.join()
|
|
195
|
-
self.watched_paths.clear()
|
|
208
|
+
self.watched_paths.clear()
|
|
209
|
+
self.watch_handles.clear()
|
|
@@ -86,6 +86,10 @@ testing_framework/core/playwright_manager.py
|
|
|
86
86
|
testing_framework/core/runner.py
|
|
87
87
|
testing_framework/core/shared_cli_manager.py
|
|
88
88
|
testing_framework/core/test_discovery.py
|
|
89
|
+
todo/agent_context_management.md
|
|
90
|
+
todo/issues/device_performance_degradation.md
|
|
91
|
+
todo/issues/git_data_not_captured_in_proxmox.md
|
|
89
92
|
todo/issues/indefinite_resource_loading.md
|
|
90
93
|
todo/issues/premature_terminal_exit.md
|
|
94
|
+
todo/issues/terminals_exit_upon_starting.md
|
|
91
95
|
tools/test_python_ntp_clock.py
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Agent Context Management
|
|
2
|
+
|
|
3
|
+
This document describes how the unibot portacode agent is expected to access contextual information to complete show awerness of user data. It is in the form of phases to be implemented one by one.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Phase one: Project selection
|
|
7
|
+
|
|
8
|
+
- Each account object should carry some state data, which needs to include the fields "Selected Project": project UUID (can be null and default is null)
|
|
9
|
+
- A user can only select a project if they are authorized to access that project. And by default, no pojects are selected.
|
|
10
|
+
- project selection data should be accessible through the Account model
|
|
11
|
+
- the agent/bot needs to be equipped with a tool to select/unselect projects
|
|
12
|
+
- The system message of the agent is affected by which project is currently selected. If non, in contains data similar to that in the dashboard page. If a project is selected, it shows data similar to that of the project page.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Device Performance Degradation over time.
|
|
2
|
+
|
|
3
|
+
## **CONFIRMED ROOT CAUSE (2025-10-13)**
|
|
4
|
+
|
|
5
|
+
**Trigger: Opening/refreshing project workspace pages for git repositories**
|
|
6
|
+
|
|
7
|
+
Experimental findings:
|
|
8
|
+
- ✅ Terminal sessions have NO impact on performance (can run for days with heavy I/O)
|
|
9
|
+
- ✅ Project workspace pages WITHOUT .git folder: always fast, no degradation
|
|
10
|
+
- ❌ Project workspace pages WITH .git folder: **catastrophic degradation**
|
|
11
|
+
- 1st load: takes several seconds
|
|
12
|
+
- 2nd load: noticeably slower
|
|
13
|
+
- 5th load: barely responsive, device goes offline momentarily
|
|
14
|
+
|
|
15
|
+
**Diagnostic evidence from running service (PID 1891992, 16+ hours uptime):**
|
|
16
|
+
- 200 pipes (normally <20)
|
|
17
|
+
- 65 threads (normally 5-10)
|
|
18
|
+
- 91% sustained CPU usage
|
|
19
|
+
- Performance: 55ms → 12,418ms (224x slowdown)
|
|
20
|
+
|
|
21
|
+
**Primary culprit: Watchdog file system watcher + Git operations**
|
|
22
|
+
- file_system_watcher.py:186-188 - `stop_watching()` doesn't unschedule watches
|
|
23
|
+
- file_system_watcher.py:168 - .git directories watched **recursively**
|
|
24
|
+
- manager.py:904 - Every project state refresh adds .git watch
|
|
25
|
+
- Result: Orphaned watches accumulate, every file change triggers 100+ unnecessary handlers
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Original Issue Description
|
|
30
|
+
|
|
31
|
+
When we run "portacode connect" or "portacode service install" and the device gets connected, initially, the totall time elapsed from the moment a client session sends a command to the device till the device response event arrivs back to the client session is typically less than 100ms with an average of 55ms. However, when we start actively using the device, the device starts to slow down gradually until within just a couple of hours or so, it becomes so slow it takes more than 12 seconds! The trace looks something like this:
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
"client_send": 1760282723260.5,
|
|
35
|
+
"ping": 12418,
|
|
36
|
+
"server_receive": 1760282723264,
|
|
37
|
+
"server_send": 1760282723278,
|
|
38
|
+
"device_receive": 1760282727601,
|
|
39
|
+
"handler_receive": 1760282728420,
|
|
40
|
+
"handler_dispatch": 1760282728420,
|
|
41
|
+
"handler_complete": 1760282735611,
|
|
42
|
+
"device_send": 1760282735611,
|
|
43
|
+
"server_receive_response": 1760282735611,
|
|
44
|
+
"server_send_response": 1760282735611,
|
|
45
|
+
"client_receive": 1760282735678.5
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Client → Server: 3.50ms
|
|
49
|
+
Server Processing: 14.00ms
|
|
50
|
+
Server → Device: 4323.00ms
|
|
51
|
+
Device Processing: 819.00ms
|
|
52
|
+
Handler Queue: 0.00ms
|
|
53
|
+
Handler Execution: 7191.00ms
|
|
54
|
+
Device Response: 0.00ms
|
|
55
|
+
Device → Server: 0.00ms
|
|
56
|
+
Server → Client: 67.50ms
|
|
57
|
+
TOTAL: 12418.00ms
|
|
58
|
+
|
|
59
|
+
While the trace might missleadingly make it look like the connection Server → Device is contributing to the issue, but the truth is that the timestamp taken in the device side not immidiately as soon as the message is actually received. It's also clear from the "Device Processing" and the "Handler Execution" that the device is actually very slow. We also tried closing all terminal sessions in the device assuming some might be overwhelming it with the size of their buffer or so but that didn't change anything and it stayed very slow until we restart the portacode service.
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
Hypothesis 1: Asyncio Task Accumulation (Memory Leak)
|
|
63
|
+
|
|
64
|
+
Location: Multiple locations throughout the codebase
|
|
65
|
+
|
|
66
|
+
Evidence:
|
|
67
|
+
- asyncio.create_task() is called extensively but tasks are rarely tracked or cleaned up:
|
|
68
|
+
- Terminal session debounce tasks: /home/menas/portacode/portacode/connection/handlers/session.py:260
|
|
69
|
+
- File watcher event handling: /home/menas/portacode/portacode/connection/handlers/project_state/file_system_watcher.py:125-128
|
|
70
|
+
- Project state refresh tasks: /home/menas/portacode/portacode/connection/terminal.py:502-503
|
|
71
|
+
- Git change callbacks: /home/menas/portacode/portacode/connection/handlers/project_state/manager.py:840
|
|
72
|
+
|
|
73
|
+
Why it causes degradation:
|
|
74
|
+
- Each uncancelled/uncompleted task consumes memory and CPU
|
|
75
|
+
- Over hours of usage, thousands of orphaned tasks accumulate in the event loop
|
|
76
|
+
- The event loop has to check all pending tasks on each iteration, causing exponential slowdown
|
|
77
|
+
- The 7191ms "Handler Execution" time suggests the event loop is overwhelmed
|
|
78
|
+
|
|
79
|
+
Key smoking gun: Line 260 in session.py creates debounce tasks for terminal data that may never complete if terminals are long-running. Similarly, line 840 in
|
|
80
|
+
manager.py creates tasks without storing references to cancel them later.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
Hypothesis 2: Git Operations Blocking the Event Loop
|
|
84
|
+
|
|
85
|
+
Location: /home/menas/portacode/portacode/connection/handlers/project_state/git_manager.py and
|
|
86
|
+
/home/menas/portacode/portacode/connection/handlers/project_state/manager.py
|
|
87
|
+
|
|
88
|
+
Evidence:
|
|
89
|
+
- Git periodic monitoring runs every 1 second checking full repository status: git_manager.py:1839
|
|
90
|
+
- File system watcher triggers git operations on every file change: manager.py:842-878
|
|
91
|
+
- Many git operations are NOT using run_in_executor despite being synchronous I/O
|
|
92
|
+
- get_file_status_batch() performs multiple git commands per file: git_manager.py:295-406
|
|
93
|
+
|
|
94
|
+
Why it causes degradation:
|
|
95
|
+
- Git operations on large repos can take 100-500ms each
|
|
96
|
+
- With active file editing, git operations are triggered continuously
|
|
97
|
+
- The synchronous git calls block the event loop, preventing handlers from processing
|
|
98
|
+
- The 819ms "Device Processing" + 7191ms "Handler Execution" = 8010ms spent in device-side operations
|
|
99
|
+
|
|
100
|
+
Key smoking gun: Line 1839 shows monitoring runs every 1 second, and lines 372-407 show batch git operations that aren't async-wrapped, causing cumulative
|
|
101
|
+
blocking over time.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
Hypothesis 3: Watchdog File System Watcher Resource Leak
|
|
105
|
+
|
|
106
|
+
Location: /home/menas/portacode/portacode/connection/handlers/project_state/file_system_watcher.py and
|
|
107
|
+
/home/menas/portacode/portacode/connection/handlers/project_state/manager.py
|
|
108
|
+
|
|
109
|
+
Evidence:
|
|
110
|
+
- Watchdog observers are created but never stopped: file_system_watcher.py:136
|
|
111
|
+
- .git directories are watched recursively: file_system_watcher.py:168
|
|
112
|
+
- New paths are added to watched_paths but cleanup only discards from set without stopping observers: file_system_watcher.py:180-188
|
|
113
|
+
- Multiple project sessions can create multiple watchers for overlapping paths
|
|
114
|
+
|
|
115
|
+
Why it causes degradation:
|
|
116
|
+
- Each watchdog observer creates background threads that consume resources
|
|
117
|
+
- Recursive watching of .git/objects/ can trigger thousands of events during git operations
|
|
118
|
+
- File system events queue up faster than they can be processed
|
|
119
|
+
- The cross-thread communication (watchdog thread → asyncio event loop) adds overhead
|
|
120
|
+
|
|
121
|
+
Key smoking gun: Line 168 watches git directories recursively, and line 186 shows stop_watching() only removes from the set but doesn't actually stop the observer
|
|
122
|
+
schedules. The Observer instance lives for the lifetime of the watcher, accumulating schedules.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
Recommended Investigation Order:
|
|
126
|
+
|
|
127
|
+
1. Check for task accumulation first (use asyncio.all_tasks() to count pending tasks)
|
|
128
|
+
2. Profile git operation frequency and duration
|
|
129
|
+
3. Check watchdog observer thread count and file descriptor usage
|