portacode 0.3.4.dev0__py3-none-any.whl → 1.4.11.dev0__py3-none-any.whl
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/_version.py +16 -3
- portacode/cli.py +155 -19
- portacode/connection/client.py +152 -12
- portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +1577 -0
- portacode/connection/handlers/__init__.py +43 -1
- portacode/connection/handlers/base.py +122 -18
- portacode/connection/handlers/chunked_content.py +244 -0
- portacode/connection/handlers/diff_handlers.py +603 -0
- portacode/connection/handlers/file_handlers.py +902 -17
- portacode/connection/handlers/project_aware_file_handlers.py +226 -0
- portacode/connection/handlers/project_state/README.md +312 -0
- portacode/connection/handlers/project_state/__init__.py +92 -0
- portacode/connection/handlers/project_state/file_system_watcher.py +179 -0
- portacode/connection/handlers/project_state/git_manager.py +1502 -0
- portacode/connection/handlers/project_state/handlers.py +875 -0
- portacode/connection/handlers/project_state/manager.py +1331 -0
- portacode/connection/handlers/project_state/models.py +108 -0
- portacode/connection/handlers/project_state/utils.py +50 -0
- portacode/connection/handlers/project_state_handlers.py +45 -0
- portacode/connection/handlers/proxmox_infra.py +307 -0
- portacode/connection/handlers/registry.py +53 -10
- portacode/connection/handlers/session.py +705 -53
- portacode/connection/handlers/system_handlers.py +142 -8
- portacode/connection/handlers/tab_factory.py +389 -0
- portacode/connection/handlers/terminal_handlers.py +150 -11
- portacode/connection/handlers/update_handler.py +61 -0
- portacode/connection/multiplex.py +60 -2
- portacode/connection/terminal.py +695 -28
- portacode/keypair.py +63 -1
- portacode/link_capture/__init__.py +38 -0
- portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
- portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
- portacode/link_capture/bin/elinks +3 -0
- portacode/link_capture/bin/gio-open +3 -0
- portacode/link_capture/bin/gnome-open +3 -0
- portacode/link_capture/bin/gvfs-open +3 -0
- portacode/link_capture/bin/kde-open +3 -0
- portacode/link_capture/bin/kfmclient +3 -0
- portacode/link_capture/bin/link_capture_exec.sh +11 -0
- portacode/link_capture/bin/link_capture_wrapper.py +75 -0
- portacode/link_capture/bin/links +3 -0
- portacode/link_capture/bin/links2 +3 -0
- portacode/link_capture/bin/lynx +3 -0
- portacode/link_capture/bin/mate-open +3 -0
- portacode/link_capture/bin/netsurf +3 -0
- portacode/link_capture/bin/sensible-browser +3 -0
- portacode/link_capture/bin/w3m +3 -0
- portacode/link_capture/bin/x-www-browser +3 -0
- portacode/link_capture/bin/xdg-open +3 -0
- portacode/logging_categories.py +140 -0
- portacode/pairing.py +103 -0
- portacode/service.py +6 -0
- portacode/static/js/test-ntp-clock.html +63 -0
- portacode/static/js/utils/ntp-clock.js +232 -0
- portacode/utils/NTP_ARCHITECTURE.md +136 -0
- portacode/utils/__init__.py +1 -0
- portacode/utils/diff_apply.py +456 -0
- portacode/utils/diff_renderer.py +371 -0
- portacode/utils/ntp_clock.py +65 -0
- portacode-1.4.11.dev0.dist-info/METADATA +298 -0
- portacode-1.4.11.dev0.dist-info/RECORD +97 -0
- {portacode-0.3.4.dev0.dist-info → portacode-1.4.11.dev0.dist-info}/WHEEL +1 -1
- portacode-1.4.11.dev0.dist-info/top_level.txt +3 -0
- test_modules/README.md +296 -0
- test_modules/__init__.py +1 -0
- test_modules/test_device_online.py +44 -0
- test_modules/test_file_operations.py +743 -0
- test_modules/test_git_status_ui.py +370 -0
- test_modules/test_login_flow.py +50 -0
- test_modules/test_navigate_testing_folder.py +361 -0
- test_modules/test_play_store_screenshots.py +294 -0
- test_modules/test_terminal_buffer_performance.py +261 -0
- test_modules/test_terminal_interaction.py +80 -0
- test_modules/test_terminal_loading_race_condition.py +95 -0
- test_modules/test_terminal_start.py +56 -0
- testing_framework/.env.example +21 -0
- testing_framework/README.md +334 -0
- testing_framework/__init__.py +17 -0
- testing_framework/cli.py +326 -0
- testing_framework/core/__init__.py +1 -0
- testing_framework/core/base_test.py +336 -0
- testing_framework/core/cli_manager.py +177 -0
- testing_framework/core/hierarchical_runner.py +577 -0
- testing_framework/core/playwright_manager.py +520 -0
- testing_framework/core/runner.py +447 -0
- testing_framework/core/shared_cli_manager.py +234 -0
- testing_framework/core/test_discovery.py +112 -0
- testing_framework/requirements.txt +12 -0
- portacode-0.3.4.dev0.dist-info/METADATA +0 -236
- portacode-0.3.4.dev0.dist-info/RECORD +0 -27
- portacode-0.3.4.dev0.dist-info/top_level.txt +0 -1
- {portacode-0.3.4.dev0.dist-info → portacode-1.4.11.dev0.dist-info}/entry_points.txt +0 -0
- {portacode-0.3.4.dev0.dist-info → portacode-1.4.11.dev0.dist-info/licenses}/LICENSE +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
-
from typing import Any, Dict
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
6
|
|
|
7
7
|
from .base import AsyncHandler
|
|
8
8
|
from .session import SessionManager
|
|
@@ -21,12 +21,13 @@ class TerminalStartHandler(AsyncHandler):
|
|
|
21
21
|
"""Start a new terminal session."""
|
|
22
22
|
shell = message.get("shell")
|
|
23
23
|
cwd = message.get("cwd")
|
|
24
|
+
project_id = message.get("project_id")
|
|
24
25
|
|
|
25
26
|
session_manager = self.context.get("session_manager")
|
|
26
27
|
if not session_manager:
|
|
27
28
|
raise RuntimeError("Session manager not available")
|
|
28
29
|
|
|
29
|
-
session_info = await session_manager.create_session(shell=shell, cwd=cwd)
|
|
30
|
+
session_info = await session_manager.create_session(shell=shell, cwd=cwd, project_id=project_id)
|
|
30
31
|
|
|
31
32
|
# Start background watcher for process exit
|
|
32
33
|
asyncio.create_task(self._watch_process_exit(session_info["terminal_id"]))
|
|
@@ -35,6 +36,10 @@ class TerminalStartHandler(AsyncHandler):
|
|
|
35
36
|
"event": "terminal_started",
|
|
36
37
|
"terminal_id": session_info["terminal_id"],
|
|
37
38
|
"channel": session_info["channel"],
|
|
39
|
+
"pid": session_info["pid"],
|
|
40
|
+
"shell": session_info.get("shell"),
|
|
41
|
+
"cwd": session_info.get("cwd"),
|
|
42
|
+
"project_id": session_info.get("project_id"),
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
async def _watch_process_exit(self, terminal_id: str) -> None:
|
|
@@ -49,14 +54,16 @@ class TerminalStartHandler(AsyncHandler):
|
|
|
49
54
|
|
|
50
55
|
await session.proc.wait()
|
|
51
56
|
|
|
52
|
-
await self.
|
|
57
|
+
await self.send_response({
|
|
53
58
|
"event": "terminal_exit",
|
|
54
59
|
"terminal_id": terminal_id,
|
|
55
60
|
"returncode": session.proc.returncode,
|
|
56
|
-
|
|
61
|
+
"project_id": session.project_id,
|
|
62
|
+
}, project_id=session.project_id)
|
|
57
63
|
|
|
58
|
-
#
|
|
59
|
-
session_manager.
|
|
64
|
+
# Only cleanup session if it still exists (not already removed by stop handler)
|
|
65
|
+
if session_manager.get_session(terminal_id):
|
|
66
|
+
session_manager.remove_session(terminal_id)
|
|
60
67
|
|
|
61
68
|
|
|
62
69
|
class TerminalSendHandler(AsyncHandler):
|
|
@@ -100,22 +107,106 @@ class TerminalStopHandler(AsyncHandler):
|
|
|
100
107
|
terminal_id = message.get("terminal_id")
|
|
101
108
|
|
|
102
109
|
if not terminal_id:
|
|
110
|
+
logger.error("terminal_stop: Missing terminal_id in message")
|
|
103
111
|
raise ValueError("terminal_id is required")
|
|
104
112
|
|
|
113
|
+
logger.info("terminal_stop: Processing stop request for terminal_id=%s", terminal_id)
|
|
114
|
+
|
|
105
115
|
session_manager = self.context.get("session_manager")
|
|
106
116
|
if not session_manager:
|
|
117
|
+
logger.error("terminal_stop: Session manager not available in context")
|
|
107
118
|
raise RuntimeError("Session manager not available")
|
|
108
119
|
|
|
120
|
+
# Remove session from manager first
|
|
109
121
|
session = session_manager.remove_session(terminal_id)
|
|
110
122
|
if not session:
|
|
111
|
-
|
|
123
|
+
logger.warning("terminal_stop: Terminal %s not found, may have already been stopped", terminal_id)
|
|
124
|
+
# Send completion event immediately for not found terminals
|
|
125
|
+
asyncio.create_task(self._send_not_found_completion(terminal_id, None))
|
|
126
|
+
return {
|
|
127
|
+
"event": "terminal_stopped",
|
|
128
|
+
"terminal_id": terminal_id,
|
|
129
|
+
"status": "not_found",
|
|
130
|
+
"message": "Terminal was not found or already stopped",
|
|
131
|
+
"project_id": None,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
logger.info("terminal_stop: Found session for terminal %s (PID: %s), starting background stop process",
|
|
135
|
+
terminal_id, getattr(session.proc, 'pid', 'unknown'))
|
|
112
136
|
|
|
113
|
-
|
|
137
|
+
# Start stop process in background without blocking the control channel
|
|
138
|
+
asyncio.create_task(self._stop_session_safely(session, terminal_id, session.project_id))
|
|
114
139
|
|
|
115
140
|
return {
|
|
116
141
|
"event": "terminal_stopped",
|
|
117
142
|
"terminal_id": terminal_id,
|
|
143
|
+
"status": "stopping",
|
|
144
|
+
"message": "Terminal stop process initiated",
|
|
145
|
+
"project_id": session.project_id,
|
|
118
146
|
}
|
|
147
|
+
|
|
148
|
+
async def _stop_session_safely(self, session, terminal_id: str, project_id: Optional[str] = None) -> None:
|
|
149
|
+
"""Safely stop a session in the background with timeout and error handling."""
|
|
150
|
+
logger.info("terminal_stop: Starting background stop process for terminal %s", terminal_id)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
# Attempt graceful stop with timeout
|
|
154
|
+
await asyncio.wait_for(session.stop(), timeout=10.0)
|
|
155
|
+
logger.info("terminal_stop: Successfully stopped terminal %s", terminal_id)
|
|
156
|
+
|
|
157
|
+
# Send success notification
|
|
158
|
+
await self.send_response({
|
|
159
|
+
"event": "terminal_stop_completed",
|
|
160
|
+
"terminal_id": terminal_id,
|
|
161
|
+
"status": "success",
|
|
162
|
+
"message": "Terminal stopped successfully",
|
|
163
|
+
"project_id": project_id,
|
|
164
|
+
}, project_id=project_id)
|
|
165
|
+
|
|
166
|
+
except asyncio.TimeoutError:
|
|
167
|
+
logger.warning("terminal_stop: Stop timeout for terminal %s, forcing kill", terminal_id)
|
|
168
|
+
|
|
169
|
+
# Force kill the process
|
|
170
|
+
try:
|
|
171
|
+
if hasattr(session.proc, 'kill'):
|
|
172
|
+
session.proc.kill()
|
|
173
|
+
logger.info("terminal_stop: Force killed terminal %s", terminal_id)
|
|
174
|
+
elif hasattr(session.proc, 'terminate'):
|
|
175
|
+
session.proc.terminate()
|
|
176
|
+
logger.info("terminal_stop: Force terminated terminal %s", terminal_id)
|
|
177
|
+
except Exception as kill_exc:
|
|
178
|
+
logger.error("terminal_stop: Failed to force kill terminal %s: %s", terminal_id, kill_exc)
|
|
179
|
+
|
|
180
|
+
# Send timeout notification
|
|
181
|
+
await self.send_response({
|
|
182
|
+
"event": "terminal_stop_completed",
|
|
183
|
+
"terminal_id": terminal_id,
|
|
184
|
+
"status": "timeout",
|
|
185
|
+
"message": "Terminal stop timed out, process was force killed",
|
|
186
|
+
"project_id": project_id,
|
|
187
|
+
}, project_id=project_id)
|
|
188
|
+
|
|
189
|
+
except Exception as exc:
|
|
190
|
+
logger.exception("terminal_stop: Error stopping terminal %s: %s", terminal_id, exc)
|
|
191
|
+
|
|
192
|
+
# Send error notification
|
|
193
|
+
await self.send_response({
|
|
194
|
+
"event": "terminal_stop_completed",
|
|
195
|
+
"terminal_id": terminal_id,
|
|
196
|
+
"status": "error",
|
|
197
|
+
"message": f"Error stopping terminal: {str(exc)}",
|
|
198
|
+
"project_id": project_id,
|
|
199
|
+
}, project_id=project_id)
|
|
200
|
+
|
|
201
|
+
async def _send_not_found_completion(self, terminal_id: str, project_id: Optional[str] = None) -> None:
|
|
202
|
+
"""Send completion event for not found terminals."""
|
|
203
|
+
await self.send_response({
|
|
204
|
+
"event": "terminal_stop_completed",
|
|
205
|
+
"terminal_id": terminal_id,
|
|
206
|
+
"status": "not_found",
|
|
207
|
+
"message": "Terminal was not found or already stopped",
|
|
208
|
+
"project_id": project_id,
|
|
209
|
+
}, project_id=project_id)
|
|
119
210
|
|
|
120
211
|
|
|
121
212
|
class TerminalListHandler(AsyncHandler):
|
|
@@ -125,15 +216,63 @@ class TerminalListHandler(AsyncHandler):
|
|
|
125
216
|
def command_name(self) -> str:
|
|
126
217
|
return "terminal_list"
|
|
127
218
|
|
|
219
|
+
async def handle(self, message: Dict[str, Any], reply_channel: Optional[str] = None) -> None:
|
|
220
|
+
"""Handle the command by executing it and sending the response to the requesting client session."""
|
|
221
|
+
logger.info("handler: Processing command %s with reply_channel=%s",
|
|
222
|
+
self.command_name, reply_channel)
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
response = await self.execute(message)
|
|
226
|
+
logger.info("handler: Command %s executed successfully", self.command_name)
|
|
227
|
+
|
|
228
|
+
# Automatically copy request_id if present in the incoming message
|
|
229
|
+
if "request_id" in message and "request_id" not in response:
|
|
230
|
+
response["request_id"] = message["request_id"]
|
|
231
|
+
|
|
232
|
+
# Get the source client session from the message
|
|
233
|
+
source_client_session = message.get("source_client_session")
|
|
234
|
+
project_id = response.get("project_id")
|
|
235
|
+
|
|
236
|
+
logger.info("handler: %s response project_id=%s, source_client_session=%s",
|
|
237
|
+
self.command_name, project_id, source_client_session)
|
|
238
|
+
|
|
239
|
+
# Send response only to the requesting client session
|
|
240
|
+
if source_client_session:
|
|
241
|
+
# Add client_sessions field to target only the requesting session
|
|
242
|
+
response["client_sessions"] = [source_client_session]
|
|
243
|
+
|
|
244
|
+
import json
|
|
245
|
+
logger.info("handler: 📤 SENDING EVENT '%s' (via direct control_channel.send)", response.get("event", "unknown"))
|
|
246
|
+
logger.info("handler: 📤 FULL EVENT PAYLOAD: %s", json.dumps(response, indent=2, default=str))
|
|
247
|
+
|
|
248
|
+
await self.control_channel.send(response)
|
|
249
|
+
else:
|
|
250
|
+
# Fallback to original behavior if no source_client_session
|
|
251
|
+
await self.send_response(response, reply_channel, project_id)
|
|
252
|
+
except Exception as exc:
|
|
253
|
+
logger.exception("handler: Error in command %s: %s", self.command_name, exc)
|
|
254
|
+
await self.send_error(str(exc), reply_channel, message.get("project_id"))
|
|
255
|
+
|
|
128
256
|
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
129
257
|
"""List all terminal sessions."""
|
|
130
258
|
session_manager = self.context.get("session_manager")
|
|
131
259
|
if not session_manager:
|
|
132
260
|
raise RuntimeError("Session manager not available")
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
261
|
+
|
|
262
|
+
# Accept project_id argument: None (default) = only no project, 'all' = all, else = filter by project_id
|
|
263
|
+
requested_project_id = message.get("project_id")
|
|
264
|
+
logger.info("terminal_list: requested_project_id=%r (type: %s)", requested_project_id, type(requested_project_id))
|
|
265
|
+
|
|
266
|
+
if requested_project_id == "all":
|
|
267
|
+
logger.info("terminal_list: Using 'all' mode to list all terminals")
|
|
268
|
+
sessions = session_manager.list_sessions(project_id="all")
|
|
269
|
+
else:
|
|
270
|
+
logger.info("terminal_list: Filtering by project_id=%r", requested_project_id)
|
|
271
|
+
sessions = session_manager.list_sessions(project_id=requested_project_id)
|
|
272
|
+
|
|
273
|
+
logger.info("terminal_list: Found %d sessions, returning with project_id=%r", len(sessions), requested_project_id)
|
|
136
274
|
return {
|
|
137
275
|
"event": "terminal_list",
|
|
138
276
|
"sessions": sessions,
|
|
277
|
+
"project_id": requested_project_id,
|
|
139
278
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Update handler for Portacode CLI."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
from .base import AsyncHandler
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UpdatePortacodeHandler(AsyncHandler):
|
|
13
|
+
"""Handler for updating Portacode CLI."""
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def command_name(self) -> str:
|
|
17
|
+
return "update_portacode_cli"
|
|
18
|
+
|
|
19
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
20
|
+
"""Update Portacode package and restart process."""
|
|
21
|
+
try:
|
|
22
|
+
logger.info("Starting Portacode CLI update...")
|
|
23
|
+
|
|
24
|
+
# Update the package
|
|
25
|
+
result = subprocess.run([
|
|
26
|
+
sys.executable, "-m", "pip", "install", "--upgrade", "portacode"
|
|
27
|
+
], capture_output=True, text=True, timeout=120)
|
|
28
|
+
|
|
29
|
+
if result.returncode != 0:
|
|
30
|
+
logger.error("Update failed: %s", result.stderr)
|
|
31
|
+
return {
|
|
32
|
+
"event": "update_portacode_response",
|
|
33
|
+
"success": False,
|
|
34
|
+
"error": f"Update failed: {result.stderr}"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
logger.info("Update successful, restarting process...")
|
|
38
|
+
|
|
39
|
+
# Send success response before exit
|
|
40
|
+
await self.send_response({
|
|
41
|
+
"event": "update_portacode_response",
|
|
42
|
+
"success": True,
|
|
43
|
+
"message": "Update completed. Process restarting..."
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
# Exit with special code to trigger restart
|
|
47
|
+
sys.exit(42)
|
|
48
|
+
|
|
49
|
+
except subprocess.TimeoutExpired:
|
|
50
|
+
return {
|
|
51
|
+
"event": "update_portacode_response",
|
|
52
|
+
"success": False,
|
|
53
|
+
"error": "Update timed out after 120 seconds"
|
|
54
|
+
}
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.exception("Update failed with exception")
|
|
57
|
+
return {
|
|
58
|
+
"event": "update_portacode_response",
|
|
59
|
+
"success": False,
|
|
60
|
+
"error": str(e)
|
|
61
|
+
}
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
|
+
import time
|
|
6
7
|
from asyncio import Queue
|
|
7
8
|
from typing import Any, Dict, Union
|
|
8
9
|
|
|
@@ -49,8 +50,65 @@ class Multiplexer:
|
|
|
49
50
|
return self._channels[channel_id]
|
|
50
51
|
|
|
51
52
|
async def _send_on_channel(self, channel_id: Union[int, str], payload: Any) -> None:
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
# Start timing the serialization and sending
|
|
54
|
+
start_time = time.time()
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
# Serialize the frame
|
|
58
|
+
serialization_start = time.time()
|
|
59
|
+
frame = json.dumps({"channel": channel_id, "payload": payload})
|
|
60
|
+
serialization_time = time.time() - serialization_start
|
|
61
|
+
|
|
62
|
+
# Calculate message size
|
|
63
|
+
frame_size_bytes = len(frame.encode('utf-8'))
|
|
64
|
+
frame_size_kb = frame_size_bytes / 1024
|
|
65
|
+
|
|
66
|
+
# Log warnings for large messages
|
|
67
|
+
if frame_size_kb > 500: # Warn for messages > 500KB
|
|
68
|
+
logger.warning("🚨 LARGE WEBSOCKET MESSAGE: %.1f KB on channel %s (event: %s)",
|
|
69
|
+
frame_size_kb, channel_id, payload.get('event', 'unknown'))
|
|
70
|
+
|
|
71
|
+
# Log additional details for very large messages
|
|
72
|
+
if frame_size_kb > 1000: # > 1MB
|
|
73
|
+
logger.warning("🚨 VERY LARGE MESSAGE: %.1f KB - This may cause connection drops!", frame_size_kb)
|
|
74
|
+
|
|
75
|
+
# Try to identify what's making the message large
|
|
76
|
+
if isinstance(payload, dict):
|
|
77
|
+
large_fields = []
|
|
78
|
+
for key, value in payload.items():
|
|
79
|
+
if isinstance(value, (str, list, dict)):
|
|
80
|
+
field_size = len(json.dumps(value).encode('utf-8')) / 1024
|
|
81
|
+
if field_size > 100: # Fields > 100KB
|
|
82
|
+
large_fields.append(f"{key}: {field_size:.1f}KB")
|
|
83
|
+
if large_fields:
|
|
84
|
+
logger.warning("🚨 Large fields detected: %s", ", ".join(large_fields))
|
|
85
|
+
|
|
86
|
+
elif frame_size_kb > 100: # Info for messages > 100KB
|
|
87
|
+
logger.info("📦 Large websocket message: %.1f KB on channel %s (event: %s)",
|
|
88
|
+
frame_size_kb, channel_id, payload.get('event', 'unknown'))
|
|
89
|
+
|
|
90
|
+
# Send the frame
|
|
91
|
+
send_start = time.time()
|
|
92
|
+
await self._send_func(frame)
|
|
93
|
+
send_time = time.time() - send_start
|
|
94
|
+
|
|
95
|
+
total_time = time.time() - start_time
|
|
96
|
+
|
|
97
|
+
# Log performance metrics for large messages or slow operations
|
|
98
|
+
if frame_size_kb > 50 or total_time > 0.1: # Log for messages > 50KB or operations > 100ms
|
|
99
|
+
logger.info("📊 WebSocket send performance: %.1f KB in %.3fs (serialize: %.3fs, send: %.3fs) - channel %s",
|
|
100
|
+
frame_size_kb, total_time, serialization_time, send_time, channel_id)
|
|
101
|
+
|
|
102
|
+
# Log detailed timing for very large messages
|
|
103
|
+
if frame_size_kb > 200:
|
|
104
|
+
logger.info("🔍 Detailed timing - Channel: %s, Event: %s, Size: %.1f KB, Total: %.3fs",
|
|
105
|
+
channel_id, payload.get('event', 'unknown'), frame_size_kb, total_time)
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
total_time = time.time() - start_time
|
|
109
|
+
logger.error("❌ Failed to send websocket message on channel %s after %.3fs: %s",
|
|
110
|
+
channel_id, total_time, e)
|
|
111
|
+
raise
|
|
54
112
|
|
|
55
113
|
async def on_raw_message(self, raw: str) -> None:
|
|
56
114
|
try:
|