@agentunion/kite 1.0.7 → 1.3.0
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/CHANGELOG.md +208 -0
- package/README.md +48 -0
- package/cli.js +1 -1
- package/extensions/agents/__init__.py +1 -0
- package/extensions/agents/assistant/__init__.py +1 -0
- package/extensions/agents/assistant/entry.py +329 -0
- package/extensions/agents/assistant/module.md +22 -0
- package/extensions/agents/assistant/server.py +197 -0
- package/extensions/channels/__init__.py +1 -0
- package/extensions/channels/acp_channel/__init__.py +1 -0
- package/extensions/channels/acp_channel/entry.py +329 -0
- package/extensions/channels/acp_channel/module.md +22 -0
- package/extensions/channels/acp_channel/server.py +197 -0
- package/extensions/event_hub_bench/entry.py +624 -379
- package/extensions/event_hub_bench/module.md +2 -1
- package/extensions/services/backup/__init__.py +1 -0
- package/extensions/services/backup/entry.py +508 -0
- package/extensions/services/backup/module.md +22 -0
- package/extensions/services/model_service/__init__.py +1 -0
- package/extensions/services/model_service/entry.py +508 -0
- package/extensions/services/model_service/module.md +22 -0
- package/extensions/services/watchdog/entry.py +468 -102
- package/extensions/services/watchdog/module.md +3 -0
- package/extensions/services/watchdog/monitor.py +170 -69
- package/extensions/services/web/__init__.py +1 -0
- package/extensions/services/web/config.yaml +149 -0
- package/extensions/services/web/entry.py +390 -0
- package/extensions/services/web/module.md +24 -0
- package/extensions/services/web/routes/__init__.py +1 -0
- package/extensions/services/web/routes/routes_call.py +189 -0
- package/extensions/services/web/routes/routes_config.py +512 -0
- package/extensions/services/web/routes/routes_contacts.py +98 -0
- package/extensions/services/web/routes/routes_devlog.py +99 -0
- package/extensions/services/web/routes/routes_phone.py +81 -0
- package/extensions/services/web/routes/routes_sms.py +48 -0
- package/extensions/services/web/routes/routes_stats.py +17 -0
- package/extensions/services/web/routes/routes_voicechat.py +554 -0
- package/extensions/services/web/routes/schemas.py +216 -0
- package/extensions/services/web/server.py +375 -0
- package/extensions/services/web/static/css/style.css +1064 -0
- package/extensions/services/web/static/index.html +1445 -0
- package/extensions/services/web/static/js/app.js +4671 -0
- package/extensions/services/web/vendor/__init__.py +1 -0
- package/extensions/services/web/vendor/bluetooth/audio.py +348 -0
- package/extensions/services/web/vendor/bluetooth/contacts.py +251 -0
- package/extensions/services/web/vendor/bluetooth/manager.py +395 -0
- package/extensions/services/web/vendor/bluetooth/sms.py +290 -0
- package/extensions/services/web/vendor/bluetooth/telephony.py +274 -0
- package/extensions/services/web/vendor/config.py +139 -0
- package/extensions/services/web/vendor/conversation/asr.py +936 -0
- package/extensions/services/web/vendor/conversation/engine.py +548 -0
- package/extensions/services/web/vendor/conversation/llm.py +534 -0
- package/extensions/services/web/vendor/conversation/mcp_tools.py +190 -0
- package/extensions/services/web/vendor/conversation/tts.py +322 -0
- package/extensions/services/web/vendor/conversation/vad.py +138 -0
- package/extensions/services/web/vendor/storage/__init__.py +1 -0
- package/extensions/services/web/vendor/storage/identity.py +312 -0
- package/extensions/services/web/vendor/storage/store.py +507 -0
- package/extensions/services/web/vendor/task/manager.py +864 -0
- package/extensions/services/web/vendor/task/models.py +45 -0
- package/extensions/services/web/vendor/task/webhook.py +263 -0
- package/extensions/services/web/vendor/tools/registry.py +321 -0
- package/kernel/__init__.py +0 -0
- package/kernel/entry.py +407 -0
- package/{core/event_hub/hub.py → kernel/event_hub.py} +62 -74
- package/kernel/module.md +33 -0
- package/{core/registry/store.py → kernel/registry_store.py} +23 -8
- package/kernel/rpc_router.py +388 -0
- package/kernel/server.py +267 -0
- package/launcher/__init__.py +10 -0
- package/launcher/__main__.py +6 -0
- package/launcher/count_lines.py +258 -0
- package/launcher/entry.py +1778 -0
- package/launcher/logging_setup.py +289 -0
- package/{core/launcher → launcher}/module_scanner.py +11 -6
- package/launcher/process_manager.py +880 -0
- package/main.py +11 -210
- package/package.json +6 -9
- package/__init__.py +0 -1
- package/__main__.py +0 -15
- package/core/event_hub/BENCHMARK.md +0 -94
- package/core/event_hub/bench.py +0 -459
- package/core/event_hub/bench_extreme.py +0 -308
- package/core/event_hub/bench_perf.py +0 -350
- package/core/event_hub/entry.py +0 -157
- package/core/event_hub/module.md +0 -20
- package/core/event_hub/server.py +0 -206
- package/core/launcher/entry.py +0 -1158
- package/core/launcher/process_manager.py +0 -470
- package/core/registry/entry.py +0 -110
- package/core/registry/module.md +0 -30
- package/core/registry/server.py +0 -289
- package/extensions/services/watchdog/server.py +0 -167
- /package/{core → extensions/services/web/vendor/bluetooth}/__init__.py +0 -0
- /package/{core/event_hub → extensions/services/web/vendor/conversation}/__init__.py +0 -0
- /package/{core/launcher → extensions/services/web/vendor/task}/__init__.py +0 -0
- /package/{core/registry → extensions/services/web/vendor/tools}/__init__.py +0 -0
- /package/{core/event_hub → kernel}/dedup.py +0 -0
- /package/{core/event_hub → kernel}/router.py +0 -0
- /package/{core/launcher → launcher}/module.md +0 -0
|
@@ -1,379 +1,624 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Event Hub Benchmark module entry point.
|
|
3
|
-
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import asyncio
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
1
|
+
"""
|
|
2
|
+
Event Hub Benchmark module entry point.
|
|
3
|
+
Connects to Kernel via WebSocket JSON-RPC 2.0, runs benchmarks, saves results, exits.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import builtins
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import platform
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
import traceback
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
|
|
18
|
+
import statistics
|
|
19
|
+
import uuid
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
import websockets
|
|
23
|
+
|
|
24
|
+
_project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
25
|
+
if _project_root not in sys.path:
|
|
26
|
+
sys.path.insert(0, _project_root)
|
|
27
|
+
|
|
28
|
+
# Results go to KITE_MODULE_DATA (set by Launcher per module)
|
|
29
|
+
# Fallback to source tree for standalone runs
|
|
30
|
+
RESULTS_DIR = os.environ.get("KITE_MODULE_DATA") or os.path.join(
|
|
31
|
+
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
|
32
|
+
"core", "event_hub", "bench_results",
|
|
33
|
+
)
|
|
34
|
+
TAG = "[event_hub_bench]"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── Module configuration ──
|
|
38
|
+
MODULE_NAME = "event_hub_bench"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── Timestamped print + log file writer ──
|
|
42
|
+
_builtin_print = builtins.print
|
|
43
|
+
_start_ts = time.monotonic()
|
|
44
|
+
_last_ts = time.monotonic()
|
|
45
|
+
_ANSI_RE = re.compile(r"\033\[[0-9;]*m")
|
|
46
|
+
_log_lock = threading.Lock()
|
|
47
|
+
_log_latest_path = None
|
|
48
|
+
_log_daily_path = None
|
|
49
|
+
_log_daily_date = ""
|
|
50
|
+
_log_dir = None
|
|
51
|
+
_crash_log_path = None
|
|
52
|
+
|
|
53
|
+
def _strip_ansi(s: str) -> str:
|
|
54
|
+
return _ANSI_RE.sub("", s)
|
|
55
|
+
|
|
56
|
+
def _resolve_daily_log_path():
|
|
57
|
+
global _log_daily_path, _log_daily_date
|
|
58
|
+
if not _log_dir:
|
|
59
|
+
return
|
|
60
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
61
|
+
if today == _log_daily_date and _log_daily_path:
|
|
62
|
+
return
|
|
63
|
+
month_dir = os.path.join(_log_dir, today[:7])
|
|
64
|
+
os.makedirs(month_dir, exist_ok=True)
|
|
65
|
+
_log_daily_path = os.path.join(month_dir, f"{today}.log")
|
|
66
|
+
_log_daily_date = today
|
|
67
|
+
|
|
68
|
+
def _write_log(plain_line: str):
|
|
69
|
+
with _log_lock:
|
|
70
|
+
if _log_latest_path:
|
|
71
|
+
try:
|
|
72
|
+
with open(_log_latest_path, "a", encoding="utf-8") as f:
|
|
73
|
+
f.write(plain_line)
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
_resolve_daily_log_path()
|
|
77
|
+
if _log_daily_path:
|
|
78
|
+
try:
|
|
79
|
+
with open(_log_daily_path, "a", encoding="utf-8") as f:
|
|
80
|
+
f.write(plain_line)
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
def _write_crash(exc_type, exc_value, exc_tb, thread_name=None, severity="critical", handled=False):
|
|
85
|
+
record = {
|
|
86
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
87
|
+
"module": MODULE_NAME,
|
|
88
|
+
"thread": thread_name or threading.current_thread().name,
|
|
89
|
+
"exception_type": exc_type.__name__ if exc_type else "Unknown",
|
|
90
|
+
"exception_message": str(exc_value),
|
|
91
|
+
"traceback": "".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
|
|
92
|
+
"severity": severity,
|
|
93
|
+
"handled": handled,
|
|
94
|
+
"process_id": os.getpid(),
|
|
95
|
+
"platform": sys.platform,
|
|
96
|
+
"runtime_version": f"Python {sys.version.split()[0]}",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if exc_tb:
|
|
100
|
+
tb_entries = traceback.extract_tb(exc_tb)
|
|
101
|
+
if tb_entries:
|
|
102
|
+
last = tb_entries[-1]
|
|
103
|
+
record["context"] = {
|
|
104
|
+
"function": last.name,
|
|
105
|
+
"file": os.path.basename(last.filename),
|
|
106
|
+
"line": last.lineno,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
line = json.dumps(record, ensure_ascii=False) + "\n"
|
|
110
|
+
|
|
111
|
+
if _crash_log_path:
|
|
112
|
+
try:
|
|
113
|
+
with open(_crash_log_path, "a", encoding="utf-8") as f:
|
|
114
|
+
f.write(line)
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
if _log_dir:
|
|
119
|
+
try:
|
|
120
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
121
|
+
archive_dir = os.path.join(_log_dir, "crashes", today[:7])
|
|
122
|
+
os.makedirs(archive_dir, exist_ok=True)
|
|
123
|
+
archive_path = os.path.join(archive_dir, f"{today}.jsonl")
|
|
124
|
+
with open(archive_path, "a", encoding="utf-8") as f:
|
|
125
|
+
f.write(line)
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
def _print_crash_summary(exc_type, exc_tb, thread_name=None):
|
|
130
|
+
RED = "\033[91m"
|
|
131
|
+
RESET = "\033[0m"
|
|
132
|
+
|
|
133
|
+
if exc_tb:
|
|
134
|
+
tb_entries = traceback.extract_tb(exc_tb)
|
|
135
|
+
if tb_entries:
|
|
136
|
+
last = tb_entries[-1]
|
|
137
|
+
location = f"{os.path.basename(last.filename)}:{last.lineno}"
|
|
138
|
+
else:
|
|
139
|
+
location = "unknown"
|
|
140
|
+
else:
|
|
141
|
+
location = "unknown"
|
|
142
|
+
|
|
143
|
+
prefix = f"[{MODULE_NAME}]"
|
|
144
|
+
if thread_name:
|
|
145
|
+
_builtin_print(f"{prefix} {RED}线程 {thread_name} 崩溃: "
|
|
146
|
+
f"{exc_type.__name__} in {location}{RESET}")
|
|
147
|
+
else:
|
|
148
|
+
_builtin_print(f"{prefix} {RED}崩溃: {exc_type.__name__} in {location}{RESET}")
|
|
149
|
+
if _crash_log_path:
|
|
150
|
+
_builtin_print(f"{prefix} 崩溃日志: {_crash_log_path}")
|
|
151
|
+
|
|
152
|
+
def _setup_exception_hooks():
|
|
153
|
+
_orig_excepthook = sys.excepthook
|
|
154
|
+
|
|
155
|
+
def _excepthook(exc_type, exc_value, exc_tb):
|
|
156
|
+
_write_crash(exc_type, exc_value, exc_tb, severity="critical", handled=False)
|
|
157
|
+
_print_crash_summary(exc_type, exc_tb)
|
|
158
|
+
_orig_excepthook(exc_type, exc_value, exc_tb)
|
|
159
|
+
|
|
160
|
+
sys.excepthook = _excepthook
|
|
161
|
+
|
|
162
|
+
if hasattr(threading, "excepthook"):
|
|
163
|
+
def _thread_excepthook(args):
|
|
164
|
+
_write_crash(args.exc_type, args.exc_value, args.exc_traceback,
|
|
165
|
+
thread_name=args.thread.name if args.thread else "unknown",
|
|
166
|
+
severity="error", handled=False)
|
|
167
|
+
_print_crash_summary(args.exc_type, args.exc_traceback,
|
|
168
|
+
thread_name=args.thread.name if args.thread else None)
|
|
169
|
+
|
|
170
|
+
threading.excepthook = _thread_excepthook
|
|
171
|
+
|
|
172
|
+
def _tprint(*args, **kwargs):
|
|
173
|
+
global _last_ts
|
|
174
|
+
now = time.monotonic()
|
|
175
|
+
elapsed = now - _start_ts
|
|
176
|
+
delta = now - _last_ts
|
|
177
|
+
_last_ts = now
|
|
178
|
+
|
|
179
|
+
if elapsed < 1:
|
|
180
|
+
elapsed_str = f"{elapsed * 1000:.0f}ms"
|
|
181
|
+
elif elapsed < 100:
|
|
182
|
+
elapsed_str = f"{elapsed:.1f}s"
|
|
183
|
+
else:
|
|
184
|
+
elapsed_str = f"{elapsed:.0f}s"
|
|
185
|
+
|
|
186
|
+
if delta < 0.001:
|
|
187
|
+
delta_str = ""
|
|
188
|
+
elif delta < 1:
|
|
189
|
+
delta_str = f"+{delta * 1000:.0f}ms"
|
|
190
|
+
elif delta < 100:
|
|
191
|
+
delta_str = f"+{delta:.1f}s"
|
|
192
|
+
else:
|
|
193
|
+
delta_str = f"+{delta:.0f}s"
|
|
194
|
+
|
|
195
|
+
ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
|
196
|
+
|
|
197
|
+
_builtin_print(*args, **kwargs)
|
|
198
|
+
|
|
199
|
+
if _log_latest_path or _log_daily_path:
|
|
200
|
+
sep = kwargs.get("sep", " ")
|
|
201
|
+
end = kwargs.get("end", "\n")
|
|
202
|
+
text = sep.join(str(a) for a in args)
|
|
203
|
+
prefix = f"[{elapsed_str:>6}] {ts} {delta_str:>8} "
|
|
204
|
+
_write_log(prefix + _strip_ansi(text) + end)
|
|
205
|
+
|
|
206
|
+
builtins.print = _tprint
|
|
207
|
+
|
|
208
|
+
# Initialize log files
|
|
209
|
+
module_data = os.environ.get("KITE_MODULE_DATA")
|
|
210
|
+
if module_data:
|
|
211
|
+
_log_dir = os.path.join(module_data, "log")
|
|
212
|
+
os.makedirs(_log_dir, exist_ok=True)
|
|
213
|
+
suffix = os.environ.get("KITE_INSTANCE_SUFFIX", "")
|
|
214
|
+
|
|
215
|
+
_log_latest_path = os.path.join(_log_dir, f"latest{suffix}.log")
|
|
216
|
+
try:
|
|
217
|
+
with open(_log_latest_path, "w", encoding="utf-8") as f:
|
|
218
|
+
pass
|
|
219
|
+
except Exception:
|
|
220
|
+
_log_latest_path = None
|
|
221
|
+
|
|
222
|
+
_crash_log_path = os.path.join(_log_dir, f"crashes{suffix}.jsonl")
|
|
223
|
+
try:
|
|
224
|
+
with open(_crash_log_path, "w", encoding="utf-8") as f:
|
|
225
|
+
pass
|
|
226
|
+
except Exception:
|
|
227
|
+
_crash_log_path = None
|
|
228
|
+
|
|
229
|
+
_resolve_daily_log_path()
|
|
230
|
+
|
|
231
|
+
_setup_exception_hooks()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _get_kernel_stats(kernel_port: int) -> dict:
|
|
235
|
+
"""Fetch Kernel /stats via HTTP."""
|
|
236
|
+
try:
|
|
237
|
+
resp = httpx.get(f"http://127.0.0.1:{kernel_port}/stats", timeout=5)
|
|
238
|
+
if resp.status_code == 200:
|
|
239
|
+
return resp.json()
|
|
240
|
+
except Exception:
|
|
241
|
+
pass
|
|
242
|
+
return {}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _sample_kernel_resources(kernel_port: int) -> dict:
|
|
246
|
+
"""Sample Kernel process CPU/memory via psutil. Returns {} if unavailable."""
|
|
247
|
+
try:
|
|
248
|
+
import psutil
|
|
249
|
+
except ImportError:
|
|
250
|
+
return {"error": "psutil not installed"}
|
|
251
|
+
for conn in psutil.net_connections(kind="tcp"):
|
|
252
|
+
if conn.laddr.port == kernel_port and conn.status == "LISTEN":
|
|
253
|
+
try:
|
|
254
|
+
p = psutil.Process(conn.pid)
|
|
255
|
+
mem = p.memory_info()
|
|
256
|
+
return {
|
|
257
|
+
"pid": conn.pid,
|
|
258
|
+
"cpu_percent": p.cpu_percent(interval=1),
|
|
259
|
+
"rss_mb": round(mem.rss / 1048576, 1),
|
|
260
|
+
"vms_mb": round(mem.vms / 1048576, 1),
|
|
261
|
+
"threads": p.num_threads(),
|
|
262
|
+
}
|
|
263
|
+
except Exception:
|
|
264
|
+
pass
|
|
265
|
+
return {"error": "kernel process not found"}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ── WebSocket client ──
|
|
269
|
+
|
|
270
|
+
class WsClient:
|
|
271
|
+
"""Lightweight async WebSocket client for Kernel benchmarks (JSON-RPC 2.0)."""
|
|
272
|
+
|
|
273
|
+
def __init__(self, ws_url: str, token: str, client_id: str):
|
|
274
|
+
self.url = f"{ws_url}?token={token}&id={client_id}"
|
|
275
|
+
self.id = client_id
|
|
276
|
+
self.ws = None
|
|
277
|
+
|
|
278
|
+
async def connect(self):
|
|
279
|
+
self.ws = await websockets.connect(self.url, max_size=None)
|
|
280
|
+
|
|
281
|
+
async def subscribe(self, patterns: list[str]):
|
|
282
|
+
msg = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": "event.subscribe", "params": {"events": patterns}}
|
|
283
|
+
await self.ws.send(json.dumps(msg))
|
|
284
|
+
|
|
285
|
+
async def publish(self, event_type: str, data: dict) -> str:
|
|
286
|
+
eid = str(uuid.uuid4())
|
|
287
|
+
msg = {
|
|
288
|
+
"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": "event.publish",
|
|
289
|
+
"params": {
|
|
290
|
+
"event_id": eid, "event": event_type, "data": data,
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
await self.ws.send(json.dumps(msg))
|
|
294
|
+
return eid
|
|
295
|
+
|
|
296
|
+
async def recv(self, timeout=5.0):
|
|
297
|
+
try:
|
|
298
|
+
return json.loads(await asyncio.wait_for(self.ws.recv(), timeout=timeout))
|
|
299
|
+
except Exception:
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
async def close(self):
|
|
303
|
+
if self.ws:
|
|
304
|
+
await self.ws.close()
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# ── Benchmark runner ──
|
|
308
|
+
|
|
309
|
+
class BenchRunner:
|
|
310
|
+
def __init__(self, ws_url: str, token: str, kernel_port: int):
|
|
311
|
+
self.ws_url = ws_url
|
|
312
|
+
self.token = token
|
|
313
|
+
self.kernel_port = kernel_port
|
|
314
|
+
self.results = {}
|
|
315
|
+
|
|
316
|
+
async def _make_client(self, name: str) -> WsClient:
|
|
317
|
+
c = WsClient(self.ws_url, self.token, name)
|
|
318
|
+
await c.connect()
|
|
319
|
+
return c
|
|
320
|
+
|
|
321
|
+
# ── Throughput: burst N events, measure hub processing rate ──
|
|
322
|
+
|
|
323
|
+
async def bench_throughput(self, n: int = 10000):
|
|
324
|
+
print(f"{TAG} Throughput test: {n} events...")
|
|
325
|
+
pub = await self._make_client("bench_tp_pub")
|
|
326
|
+
sub = await self._make_client("bench_tp_sub")
|
|
327
|
+
await sub.subscribe(["bench.tp.*"])
|
|
328
|
+
await asyncio.sleep(0.2)
|
|
329
|
+
|
|
330
|
+
recvd = 0
|
|
331
|
+
stop = asyncio.Event()
|
|
332
|
+
|
|
333
|
+
async def _recv():
|
|
334
|
+
nonlocal recvd
|
|
335
|
+
while not stop.is_set():
|
|
336
|
+
try:
|
|
337
|
+
raw = await asyncio.wait_for(sub.ws.recv(), timeout=0.5)
|
|
338
|
+
msg = json.loads(raw)
|
|
339
|
+
# JSON-RPC Notification (event): has method, no id
|
|
340
|
+
if "method" in msg and "id" not in msg:
|
|
341
|
+
recvd += 1
|
|
342
|
+
except Exception:
|
|
343
|
+
pass
|
|
344
|
+
|
|
345
|
+
async def _drain_acks():
|
|
346
|
+
while not stop.is_set():
|
|
347
|
+
try:
|
|
348
|
+
await asyncio.wait_for(pub.ws.recv(), timeout=0.5)
|
|
349
|
+
except Exception:
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
recv_task = asyncio.create_task(_recv())
|
|
353
|
+
ack_task = asyncio.create_task(_drain_acks())
|
|
354
|
+
|
|
355
|
+
hub_before = (_get_kernel_stats(self.kernel_port).get("counters") or {})
|
|
356
|
+
|
|
357
|
+
t0 = time.time()
|
|
358
|
+
for i in range(n):
|
|
359
|
+
await pub.publish("bench.tp.test", {"i": i})
|
|
360
|
+
send_time = time.time() - t0
|
|
361
|
+
|
|
362
|
+
deadline = time.time() + max(15, n / 300)
|
|
363
|
+
while recvd < n and time.time() < deadline:
|
|
364
|
+
await asyncio.sleep(0.1)
|
|
365
|
+
total_time = time.time() - t0
|
|
366
|
+
|
|
367
|
+
stop.set()
|
|
368
|
+
await recv_task
|
|
369
|
+
ack_task.cancel()
|
|
370
|
+
|
|
371
|
+
hub_after = (_get_kernel_stats(self.kernel_port).get("counters") or {})
|
|
372
|
+
|
|
373
|
+
await pub.close()
|
|
374
|
+
await sub.close()
|
|
375
|
+
|
|
376
|
+
self.results["throughput"] = {
|
|
377
|
+
"events": n,
|
|
378
|
+
"send_rate": round(n / send_time),
|
|
379
|
+
"recv_rate": round(recvd / total_time) if total_time > 0 else 0,
|
|
380
|
+
"client_recv": recvd,
|
|
381
|
+
"hub_queued": hub_after.get("events_queued", 0) - hub_before.get("events_queued", 0),
|
|
382
|
+
"hub_routed": hub_after.get("events_routed", 0) - hub_before.get("events_routed", 0),
|
|
383
|
+
"send_time_s": round(send_time, 2),
|
|
384
|
+
"total_time_s": round(total_time, 2),
|
|
385
|
+
}
|
|
386
|
+
print(f"{TAG} send_rate={self.results['throughput']['send_rate']} evt/s, "
|
|
387
|
+
f"recv={recvd}/{n}, time={round(total_time, 1)}s")
|
|
388
|
+
|
|
389
|
+
# ── Latency: sequential pub→recv round-trip ──
|
|
390
|
+
|
|
391
|
+
async def bench_latency(self, n: int = 200):
|
|
392
|
+
print(f"{TAG} Latency test: {n} samples...")
|
|
393
|
+
pub = await self._make_client("bench_lat_pub")
|
|
394
|
+
sub = await self._make_client("bench_lat_sub")
|
|
395
|
+
await sub.subscribe(["bench.lat.*"])
|
|
396
|
+
await asyncio.sleep(0.2)
|
|
397
|
+
|
|
398
|
+
latencies = []
|
|
399
|
+
for i in range(n):
|
|
400
|
+
t0 = time.time()
|
|
401
|
+
await pub.publish("bench.lat.test", {"i": i})
|
|
402
|
+
await pub.recv(timeout=2) # RPC response
|
|
403
|
+
msg = await sub.recv(timeout=2)
|
|
404
|
+
if msg and "method" in msg and "id" not in msg:
|
|
405
|
+
latencies.append((time.time() - t0) * 1000)
|
|
406
|
+
|
|
407
|
+
await pub.close()
|
|
408
|
+
await sub.close()
|
|
409
|
+
|
|
410
|
+
if latencies:
|
|
411
|
+
latencies.sort()
|
|
412
|
+
self.results["latency"] = {
|
|
413
|
+
"samples": len(latencies),
|
|
414
|
+
"avg_ms": round(statistics.mean(latencies), 2),
|
|
415
|
+
"p50_ms": round(latencies[len(latencies) // 2], 2),
|
|
416
|
+
"p95_ms": round(latencies[int(len(latencies) * 0.95)], 2),
|
|
417
|
+
"p99_ms": round(latencies[int(len(latencies) * 0.99)], 2),
|
|
418
|
+
}
|
|
419
|
+
print(f"{TAG} avg={self.results['latency']['avg_ms']}ms, "
|
|
420
|
+
f"p50={self.results['latency']['p50_ms']}ms, "
|
|
421
|
+
f"p99={self.results['latency']['p99_ms']}ms")
|
|
422
|
+
|
|
423
|
+
# ── Fan-out: 1 pub, N subs ──
|
|
424
|
+
|
|
425
|
+
async def bench_fanout(self, n_events: int = 2000):
|
|
426
|
+
for n_subs in [1, 5, 10, 50]:
|
|
427
|
+
print(f"{TAG} Fan-out x{n_subs}: {n_events} events...")
|
|
428
|
+
pub = await self._make_client("bench_fo_pub")
|
|
429
|
+
subs = []
|
|
430
|
+
for i in range(n_subs):
|
|
431
|
+
s = await self._make_client(f"bench_fo_sub_{i}")
|
|
432
|
+
await s.subscribe(["bench.fo.*"])
|
|
433
|
+
subs.append(s)
|
|
434
|
+
await asyncio.sleep(0.3)
|
|
435
|
+
|
|
436
|
+
counts = [0] * n_subs
|
|
437
|
+
stop = asyncio.Event()
|
|
438
|
+
|
|
439
|
+
async def _recv(idx, client):
|
|
440
|
+
while not stop.is_set():
|
|
441
|
+
try:
|
|
442
|
+
raw = await asyncio.wait_for(client.ws.recv(), timeout=0.5)
|
|
443
|
+
msg = json.loads(raw)
|
|
444
|
+
if "method" in msg and "id" not in msg:
|
|
445
|
+
counts[idx] += 1
|
|
446
|
+
except Exception:
|
|
447
|
+
pass
|
|
448
|
+
|
|
449
|
+
tasks = [asyncio.create_task(_recv(i, s)) for i, s in enumerate(subs)]
|
|
450
|
+
ack_stop = asyncio.Event()
|
|
451
|
+
|
|
452
|
+
async def _drain():
|
|
453
|
+
while not ack_stop.is_set():
|
|
454
|
+
try:
|
|
455
|
+
await asyncio.wait_for(pub.ws.recv(), timeout=0.5)
|
|
456
|
+
except Exception:
|
|
457
|
+
pass
|
|
458
|
+
|
|
459
|
+
ack_task = asyncio.create_task(_drain())
|
|
460
|
+
|
|
461
|
+
t0 = time.time()
|
|
462
|
+
for i in range(n_events):
|
|
463
|
+
await pub.publish("bench.fo.test", {"i": i})
|
|
464
|
+
send_time = time.time() - t0
|
|
465
|
+
|
|
466
|
+
await asyncio.sleep(max(3, n_events * n_subs / 3000))
|
|
467
|
+
stop.set()
|
|
468
|
+
ack_stop.set()
|
|
469
|
+
for t in tasks:
|
|
470
|
+
await t
|
|
471
|
+
ack_task.cancel()
|
|
472
|
+
|
|
473
|
+
await pub.close()
|
|
474
|
+
for s in subs:
|
|
475
|
+
await s.close()
|
|
476
|
+
|
|
477
|
+
avg_recv = sum(counts) / n_subs if n_subs else 0
|
|
478
|
+
self.results[f"fanout_{n_subs}"] = {
|
|
479
|
+
"subs": n_subs,
|
|
480
|
+
"events": n_events,
|
|
481
|
+
"send_rate": round(n_events / send_time) if send_time > 0 else 0,
|
|
482
|
+
"avg_recv": round(avg_recv),
|
|
483
|
+
"min_recv": min(counts),
|
|
484
|
+
}
|
|
485
|
+
print(f"{TAG} subs={n_subs}, avg_recv={round(avg_recv)}/{n_events}, "
|
|
486
|
+
f"min={min(counts)}")
|
|
487
|
+
|
|
488
|
+
# ── Save results ──
|
|
489
|
+
|
|
490
|
+
def save(self):
|
|
491
|
+
os.makedirs(RESULTS_DIR, exist_ok=True)
|
|
492
|
+
now = datetime.now()
|
|
493
|
+
filepath = os.path.join(RESULTS_DIR, now.strftime("%Y-%m-%d_%H-%M-%S") + ".json")
|
|
494
|
+
|
|
495
|
+
kernel_stats = _get_kernel_stats(self.kernel_port)
|
|
496
|
+
resources = _sample_kernel_resources(self.kernel_port)
|
|
497
|
+
|
|
498
|
+
data = {
|
|
499
|
+
"timestamp": now.isoformat(),
|
|
500
|
+
"env": {
|
|
501
|
+
"platform": sys.platform,
|
|
502
|
+
"python": platform.python_version(),
|
|
503
|
+
},
|
|
504
|
+
"hub_resources": resources,
|
|
505
|
+
"hub_counters": kernel_stats.get("counters", {}),
|
|
506
|
+
**self.results,
|
|
507
|
+
}
|
|
508
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
509
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
510
|
+
print(f"{TAG} Results saved: {filepath}")
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _send_ready_event(ws_url: str, token: str):
|
|
514
|
+
"""Send module.ready to Kernel via JSON-RPC 2.0. Startup phase complete."""
|
|
515
|
+
try:
|
|
516
|
+
import websockets.sync.client as ws_sync
|
|
517
|
+
url = f"{ws_url}?token={token}&id=event_hub_bench"
|
|
518
|
+
with ws_sync.connect(url, close_timeout=3) as ws:
|
|
519
|
+
# Subscribe (先订阅)
|
|
520
|
+
ws.send(json.dumps({
|
|
521
|
+
"jsonrpc": "2.0", "id": str(uuid.uuid4()),
|
|
522
|
+
"method": "event.subscribe", "params": {"events": []},
|
|
523
|
+
}))
|
|
524
|
+
# Register (再注册)
|
|
525
|
+
ws.send(json.dumps({
|
|
526
|
+
"jsonrpc": "2.0", "id": str(uuid.uuid4()),
|
|
527
|
+
"method": "registry.register",
|
|
528
|
+
"params": {
|
|
529
|
+
"module_id": "event_hub_bench",
|
|
530
|
+
"module_type": "tool",
|
|
531
|
+
"name": "Event Hub Benchmark",
|
|
532
|
+
},
|
|
533
|
+
}))
|
|
534
|
+
# Publish ready
|
|
535
|
+
ws.send(json.dumps({
|
|
536
|
+
"jsonrpc": "2.0", "id": str(uuid.uuid4()),
|
|
537
|
+
"method": "event.publish",
|
|
538
|
+
"params": {
|
|
539
|
+
"event_id": str(uuid.uuid4()),
|
|
540
|
+
"event": "module.ready",
|
|
541
|
+
"data": {"module_id": "event_hub_bench"},
|
|
542
|
+
},
|
|
543
|
+
}))
|
|
544
|
+
time.sleep(0.1)
|
|
545
|
+
except Exception as e:
|
|
546
|
+
print(f"{TAG} WARNING: Could not send module.ready: {e}")
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _send_exiting_event(ws_url: str, token: str, reason: str):
|
|
550
|
+
"""Send module.exiting event to Kernel via JSON-RPC 2.0 before exit."""
|
|
551
|
+
try:
|
|
552
|
+
import websockets.sync.client as ws_sync
|
|
553
|
+
url = f"{ws_url}?token={token}&id=event_hub_bench"
|
|
554
|
+
with ws_sync.connect(url, close_timeout=3) as ws:
|
|
555
|
+
ws.send(json.dumps({
|
|
556
|
+
"jsonrpc": "2.0", "id": str(uuid.uuid4()),
|
|
557
|
+
"method": "event.publish",
|
|
558
|
+
"params": {
|
|
559
|
+
"event_id": str(uuid.uuid4()),
|
|
560
|
+
"event": "module.exiting",
|
|
561
|
+
"data": {
|
|
562
|
+
"module_id": "event_hub_bench",
|
|
563
|
+
"reason": reason,
|
|
564
|
+
"action": "none",
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
}))
|
|
568
|
+
time.sleep(0.3)
|
|
569
|
+
except Exception as e:
|
|
570
|
+
print(f"{TAG} WARNING: Could not send module.exiting: {e}")
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
# ── Entry point ──
|
|
574
|
+
|
|
575
|
+
async def _run(ws_url: str, token: str, kernel_port: int):
|
|
576
|
+
bench = BenchRunner(ws_url, token, kernel_port)
|
|
577
|
+
await bench.bench_throughput()
|
|
578
|
+
await bench.bench_latency()
|
|
579
|
+
await bench.bench_fanout()
|
|
580
|
+
bench.save()
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def main():
|
|
584
|
+
# Read boot_info from stdin (only token)
|
|
585
|
+
token = ""
|
|
586
|
+
try:
|
|
587
|
+
line = sys.stdin.readline().strip()
|
|
588
|
+
if line:
|
|
589
|
+
boot = json.loads(line)
|
|
590
|
+
token = boot.get("token", "")
|
|
591
|
+
except Exception:
|
|
592
|
+
pass
|
|
593
|
+
|
|
594
|
+
# Read registry_port from environment variable
|
|
595
|
+
kernel_port = int(os.environ.get("KITE_KERNEL_PORT", "0"))
|
|
596
|
+
|
|
597
|
+
if not token or not kernel_port:
|
|
598
|
+
print(f"{TAG} ERROR: Missing token or KITE_KERNEL_PORT")
|
|
599
|
+
sys.exit(1)
|
|
600
|
+
|
|
601
|
+
ws_url = f"ws://127.0.0.1:{kernel_port}/ws"
|
|
602
|
+
print(f"{TAG} Kernel at port {kernel_port}")
|
|
603
|
+
|
|
604
|
+
# Debug mode required: multiple benchmark clients share one token
|
|
605
|
+
if os.environ.get("KITE_DEBUG") != "1":
|
|
606
|
+
print(f"{TAG} 需要调试模式才能运行 (启动时加 --debug)")
|
|
607
|
+
_send_exiting_event(ws_url, token, "requires KITE_DEBUG=1")
|
|
608
|
+
sys.exit(0)
|
|
609
|
+
|
|
610
|
+
# ── Startup complete — notify Launcher ──
|
|
611
|
+
_send_ready_event(ws_url, token)
|
|
612
|
+
|
|
613
|
+
# ── Business logic: run benchmarks ──
|
|
614
|
+
print(f"{TAG} Starting benchmarks...")
|
|
615
|
+
|
|
616
|
+
asyncio.run(_run(ws_url, token, kernel_port))
|
|
617
|
+
|
|
618
|
+
print(f"{TAG} Benchmarks complete, sending exit intent...")
|
|
619
|
+
_send_exiting_event(ws_url, token, "benchmarks complete")
|
|
620
|
+
print(f"{TAG} Exiting.")
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
if __name__ == "__main__":
|
|
624
|
+
main()
|