@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
package/core/event_hub/bench.py
DELETED
|
@@ -1,459 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Event Hub stress test (standalone).
|
|
3
|
-
|
|
4
|
-
Usage: python -m core.event_hub.bench
|
|
5
|
-
(from Kite root directory)
|
|
6
|
-
|
|
7
|
-
Phase 1: 3-channel mixed stress test (10 minutes)
|
|
8
|
-
CH1 — high-freq small messages (500 evt/s)
|
|
9
|
-
CH2 — medium-freq large messages (50 evt/s, 10KB)
|
|
10
|
-
CH3 — bursty traffic (200-event burst every 2s)
|
|
11
|
-
|
|
12
|
-
Phase 2: 3 extreme tests
|
|
13
|
-
EX1 — max burst (ramp until failure)
|
|
14
|
-
EX2 — max concurrent connections
|
|
15
|
-
EX3 — max message size
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
import asyncio
|
|
19
|
-
import json
|
|
20
|
-
import os
|
|
21
|
-
import socket
|
|
22
|
-
import statistics
|
|
23
|
-
import sys
|
|
24
|
-
import threading
|
|
25
|
-
import time
|
|
26
|
-
import uuid
|
|
27
|
-
from datetime import datetime, timezone
|
|
28
|
-
|
|
29
|
-
_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
30
|
-
if _root not in sys.path:
|
|
31
|
-
sys.path.insert(0, _root)
|
|
32
|
-
|
|
33
|
-
import psutil
|
|
34
|
-
import uvicorn
|
|
35
|
-
import websockets
|
|
36
|
-
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
37
|
-
|
|
38
|
-
from core.event_hub.hub import EventHub
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
# ── Minimal Event Hub server (no auth) ──
|
|
42
|
-
|
|
43
|
-
def _create_app(hub: EventHub) -> FastAPI:
|
|
44
|
-
app = FastAPI()
|
|
45
|
-
|
|
46
|
-
@app.websocket("/ws")
|
|
47
|
-
async def ws(ws: WebSocket):
|
|
48
|
-
mid = ws.query_params.get("id", f"anon_{id(ws)}")
|
|
49
|
-
await ws.accept()
|
|
50
|
-
hub.add_connection(mid, ws)
|
|
51
|
-
try:
|
|
52
|
-
while True:
|
|
53
|
-
raw = await ws.receive_text()
|
|
54
|
-
await hub.handle_message(mid, ws, raw)
|
|
55
|
-
except (WebSocketDisconnect, Exception):
|
|
56
|
-
pass
|
|
57
|
-
finally:
|
|
58
|
-
hub.remove_connection(mid)
|
|
59
|
-
|
|
60
|
-
return app
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _free_port() -> int:
|
|
64
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
65
|
-
s.bind(("127.0.0.1", 0))
|
|
66
|
-
return s.getsockname()[1]
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# ── Client ──
|
|
70
|
-
|
|
71
|
-
class C:
|
|
72
|
-
"""Lightweight WS client."""
|
|
73
|
-
def __init__(self, url: str, name: str):
|
|
74
|
-
self.url, self.name, self.ws = url, name, None
|
|
75
|
-
|
|
76
|
-
async def connect(self):
|
|
77
|
-
self.ws = await websockets.connect(f"{self.url}?id={self.name}", max_size=None)
|
|
78
|
-
|
|
79
|
-
async def sub(self, patterns):
|
|
80
|
-
await self.ws.send(json.dumps({"type": "subscribe", "events": patterns}))
|
|
81
|
-
|
|
82
|
-
async def pub(self, event: str, data: dict) -> str:
|
|
83
|
-
eid = str(uuid.uuid4())
|
|
84
|
-
await self.ws.send(json.dumps({
|
|
85
|
-
"type": "event", "event_id": eid, "event": event,
|
|
86
|
-
"source": self.name, "timestamp": datetime.now(timezone.utc).isoformat(),
|
|
87
|
-
"data": data,
|
|
88
|
-
}))
|
|
89
|
-
return eid
|
|
90
|
-
|
|
91
|
-
async def recv(self, timeout=5.0):
|
|
92
|
-
try:
|
|
93
|
-
return json.loads(await asyncio.wait_for(self.ws.recv(), timeout=timeout))
|
|
94
|
-
except Exception:
|
|
95
|
-
return None
|
|
96
|
-
|
|
97
|
-
async def drain(self, timeout=0.05):
|
|
98
|
-
while await self.recv(timeout=timeout) is not None:
|
|
99
|
-
pass
|
|
100
|
-
|
|
101
|
-
async def close(self):
|
|
102
|
-
if self.ws:
|
|
103
|
-
await self.ws.close()
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# ── Stats collector ──
|
|
107
|
-
|
|
108
|
-
class Stats:
|
|
109
|
-
def __init__(self):
|
|
110
|
-
self.sent = 0
|
|
111
|
-
self.received = 0
|
|
112
|
-
self.acked = 0
|
|
113
|
-
self.latencies: list[float] = []
|
|
114
|
-
self.lock = asyncio.Lock()
|
|
115
|
-
|
|
116
|
-
async def record_latency(self, ts_iso: str):
|
|
117
|
-
try:
|
|
118
|
-
sent = datetime.fromisoformat(ts_iso)
|
|
119
|
-
ms = (datetime.now(timezone.utc) - sent).total_seconds() * 1000
|
|
120
|
-
async with self.lock:
|
|
121
|
-
self.latencies.append(ms)
|
|
122
|
-
except Exception:
|
|
123
|
-
pass
|
|
124
|
-
|
|
125
|
-
def summary(self) -> dict:
|
|
126
|
-
lat = sorted(self.latencies) if self.latencies else [0]
|
|
127
|
-
return {
|
|
128
|
-
"sent": self.sent,
|
|
129
|
-
"recv": self.received,
|
|
130
|
-
"acked": self.acked,
|
|
131
|
-
"loss": self.sent - self.received,
|
|
132
|
-
"lat_avg": round(statistics.mean(lat), 1),
|
|
133
|
-
"lat_p50": round(statistics.median(lat), 1),
|
|
134
|
-
"lat_p95": round(lat[int(len(lat) * 0.95)], 1),
|
|
135
|
-
"lat_p99": round(lat[int(len(lat) * 0.99)], 1),
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
# ── Benchmark ──
|
|
140
|
-
|
|
141
|
-
class Benchmark:
|
|
142
|
-
def __init__(self):
|
|
143
|
-
self.hub = EventHub()
|
|
144
|
-
self.port = _free_port()
|
|
145
|
-
self.ws_url = f"ws://127.0.0.1:{self.port}/ws"
|
|
146
|
-
self._server: uvicorn.Server | None = None
|
|
147
|
-
self.proc = psutil.Process()
|
|
148
|
-
|
|
149
|
-
def _start_server(self):
|
|
150
|
-
app = _create_app(self.hub)
|
|
151
|
-
cfg = uvicorn.Config(app, host="127.0.0.1", port=self.port, log_level="warning")
|
|
152
|
-
self._server = uvicorn.Server(cfg)
|
|
153
|
-
threading.Thread(target=self._server.run, daemon=True).start()
|
|
154
|
-
deadline = time.time() + 5
|
|
155
|
-
while time.time() < deadline:
|
|
156
|
-
if self._server.started:
|
|
157
|
-
return
|
|
158
|
-
time.sleep(0.05)
|
|
159
|
-
raise RuntimeError("Server failed to start")
|
|
160
|
-
|
|
161
|
-
def _res_snapshot(self) -> dict:
|
|
162
|
-
return {
|
|
163
|
-
"cpu": self.proc.cpu_percent(),
|
|
164
|
-
"mem_mb": round(self.proc.memory_info().rss / 1024 / 1024, 1),
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async def _make_channel(self, pub_name: str, sub_name: str, pattern: str):
|
|
168
|
-
"""Create a pub/sub pair, subscribe, return (pub, sub, stats)."""
|
|
169
|
-
pub = C(self.ws_url, pub_name)
|
|
170
|
-
sub = C(self.ws_url, sub_name)
|
|
171
|
-
await pub.connect()
|
|
172
|
-
await sub.connect()
|
|
173
|
-
await sub.sub([pattern])
|
|
174
|
-
await asyncio.sleep(0.1)
|
|
175
|
-
return pub, sub, Stats()
|
|
176
|
-
|
|
177
|
-
# ── ACK drainer (keeps publisher WS buffer clear) ──
|
|
178
|
-
|
|
179
|
-
async def _drainer(self, pub: C, stats: Stats, stop: asyncio.Event):
|
|
180
|
-
while not stop.is_set():
|
|
181
|
-
msg = await pub.recv(timeout=0.3)
|
|
182
|
-
if msg and msg.get("type") == "ack":
|
|
183
|
-
stats.acked += 1
|
|
184
|
-
|
|
185
|
-
# ── Subscriber receiver (shared by all channels) ──
|
|
186
|
-
|
|
187
|
-
async def _receiver(self, sub: C, stats: Stats, stop: asyncio.Event):
|
|
188
|
-
while not stop.is_set():
|
|
189
|
-
msg = await sub.recv(timeout=0.3)
|
|
190
|
-
if not msg:
|
|
191
|
-
continue
|
|
192
|
-
if msg.get("type") == "event":
|
|
193
|
-
stats.received += 1
|
|
194
|
-
ts = msg.get("timestamp", "")
|
|
195
|
-
if ts:
|
|
196
|
-
await stats.record_latency(ts)
|
|
197
|
-
|
|
198
|
-
# ── CH1: high-freq small messages ──
|
|
199
|
-
|
|
200
|
-
async def _ch1_sender(self, pub: C, stats: Stats, stop: asyncio.Event):
|
|
201
|
-
"""500 evt/s, tiny payload."""
|
|
202
|
-
while not stop.is_set():
|
|
203
|
-
await pub.pub("ch1.fast", {"seq": stats.sent})
|
|
204
|
-
stats.sent += 1
|
|
205
|
-
await asyncio.sleep(0.002)
|
|
206
|
-
|
|
207
|
-
# ── CH2: medium-freq large messages ──
|
|
208
|
-
|
|
209
|
-
async def _ch2_sender(self, pub: C, stats: Stats, stop: asyncio.Event):
|
|
210
|
-
"""50 evt/s, 10KB payload."""
|
|
211
|
-
payload = "X" * 10240
|
|
212
|
-
while not stop.is_set():
|
|
213
|
-
await pub.pub("ch2.large", {"p": payload})
|
|
214
|
-
stats.sent += 1
|
|
215
|
-
await asyncio.sleep(0.02)
|
|
216
|
-
|
|
217
|
-
# ── CH3: bursty traffic ──
|
|
218
|
-
|
|
219
|
-
async def _ch3_sender(self, pub: C, stats: Stats, stop: asyncio.Event):
|
|
220
|
-
"""200-event burst every 2s."""
|
|
221
|
-
while not stop.is_set():
|
|
222
|
-
for _ in range(200):
|
|
223
|
-
await pub.pub("ch3.burst", {"b": stats.sent})
|
|
224
|
-
stats.sent += 1
|
|
225
|
-
await asyncio.sleep(2)
|
|
226
|
-
|
|
227
|
-
# ── Phase 1: 10-min mixed stress ──
|
|
228
|
-
|
|
229
|
-
async def phase1(self, duration=600):
|
|
230
|
-
print("=" * 60)
|
|
231
|
-
print(f"PHASE 1: 3-channel mixed stress test ({duration}s)")
|
|
232
|
-
print("=" * 60)
|
|
233
|
-
|
|
234
|
-
p1, s1, st1 = await self._make_channel("ch1_pub", "ch1_sub", "ch1.*")
|
|
235
|
-
p2, s2, st2 = await self._make_channel("ch2_pub", "ch2_sub", "ch2.*")
|
|
236
|
-
p3, s3, st3 = await self._make_channel("ch3_pub", "ch3_sub", "ch3.*")
|
|
237
|
-
stop = asyncio.Event()
|
|
238
|
-
|
|
239
|
-
tasks = [
|
|
240
|
-
asyncio.create_task(self._ch1_sender(p1, st1, stop)),
|
|
241
|
-
asyncio.create_task(self._drainer(p1, st1, stop)),
|
|
242
|
-
asyncio.create_task(self._receiver(s1, st1, stop)),
|
|
243
|
-
asyncio.create_task(self._ch2_sender(p2, st2, stop)),
|
|
244
|
-
asyncio.create_task(self._drainer(p2, st2, stop)),
|
|
245
|
-
asyncio.create_task(self._receiver(s2, st2, stop)),
|
|
246
|
-
asyncio.create_task(self._ch3_sender(p3, st3, stop)),
|
|
247
|
-
asyncio.create_task(self._drainer(p3, st3, stop)),
|
|
248
|
-
asyncio.create_task(self._receiver(s3, st3, stop)),
|
|
249
|
-
]
|
|
250
|
-
|
|
251
|
-
self.proc.cpu_percent() # prime
|
|
252
|
-
start = time.time()
|
|
253
|
-
interval = 30 # report every 30s
|
|
254
|
-
next_report = start + interval
|
|
255
|
-
|
|
256
|
-
while time.time() - start < duration:
|
|
257
|
-
await asyncio.sleep(1)
|
|
258
|
-
if time.time() >= next_report:
|
|
259
|
-
elapsed = round(time.time() - start)
|
|
260
|
-
res = self._res_snapshot()
|
|
261
|
-
hub_stats = self.hub._counters_dict()
|
|
262
|
-
print(f"\n [{elapsed:>4}s] cpu={res['cpu']:.0f}% mem={res['mem_mb']}MB "
|
|
263
|
-
f"hub_recv={hub_stats['events_received']} hub_route={hub_stats['events_routed']}")
|
|
264
|
-
for name, st in [("CH1-fast", st1), ("CH2-large", st2), ("CH3-burst", st3)]:
|
|
265
|
-
s = st.summary()
|
|
266
|
-
print(f" {name}: sent={s['sent']} recv={s['recv']} loss={s['loss']} "
|
|
267
|
-
f"avg={s['lat_avg']}ms p95={s['lat_p95']}ms p99={s['lat_p99']}ms")
|
|
268
|
-
next_report = time.time() + interval
|
|
269
|
-
|
|
270
|
-
stop.set()
|
|
271
|
-
for t in tasks:
|
|
272
|
-
t.cancel()
|
|
273
|
-
await asyncio.gather(*tasks, return_exceptions=True)
|
|
274
|
-
|
|
275
|
-
# Final report
|
|
276
|
-
print(f"\n FINAL ({duration}s):")
|
|
277
|
-
res = self._res_snapshot()
|
|
278
|
-
print(f" cpu={res['cpu']:.0f}% mem={res['mem_mb']}MB")
|
|
279
|
-
for name, st in [("CH1-fast", st1), ("CH2-large", st2), ("CH3-burst", st3)]:
|
|
280
|
-
s = st.summary()
|
|
281
|
-
print(f" {name}: sent={s['sent']} recv={s['recv']} loss={s['loss']} "
|
|
282
|
-
f"avg={s['lat_avg']}ms p50={s['lat_p50']}ms p95={s['lat_p95']}ms p99={s['lat_p99']}ms")
|
|
283
|
-
|
|
284
|
-
for c in [p1, s1, p2, s2, p3, s3]:
|
|
285
|
-
await c.close()
|
|
286
|
-
print()
|
|
287
|
-
|
|
288
|
-
# ── Phase 2: Extreme tests ──
|
|
289
|
-
|
|
290
|
-
async def extreme1_max_burst(self):
|
|
291
|
-
"""Ramp burst size until loss or timeout."""
|
|
292
|
-
print("=" * 60)
|
|
293
|
-
print("EXTREME 1: Max burst (ramp until loss)")
|
|
294
|
-
print("=" * 60)
|
|
295
|
-
|
|
296
|
-
pub, sub, _ = await self._make_channel("ex1_pub", "ex1_sub", "ex1.*")
|
|
297
|
-
|
|
298
|
-
for size in [1000, 5000, 10000, 20000, 50000]:
|
|
299
|
-
recvd = 0
|
|
300
|
-
stop = asyncio.Event()
|
|
301
|
-
|
|
302
|
-
async def _recv():
|
|
303
|
-
nonlocal recvd
|
|
304
|
-
while not stop.is_set():
|
|
305
|
-
msg = await sub.recv(timeout=0.3)
|
|
306
|
-
if msg and msg.get("type") == "event":
|
|
307
|
-
recvd += 1
|
|
308
|
-
|
|
309
|
-
task = asyncio.create_task(_recv())
|
|
310
|
-
start = time.time()
|
|
311
|
-
for i in range(size):
|
|
312
|
-
await pub.pub("ex1.burst", {"i": i})
|
|
313
|
-
send_time = time.time() - start
|
|
314
|
-
|
|
315
|
-
# Wait for delivery
|
|
316
|
-
await asyncio.sleep(max(3, size / 2000))
|
|
317
|
-
stop.set()
|
|
318
|
-
await task
|
|
319
|
-
await pub.drain()
|
|
320
|
-
await sub.drain()
|
|
321
|
-
|
|
322
|
-
rate = size / send_time if send_time > 0 else 0
|
|
323
|
-
loss = size - recvd
|
|
324
|
-
loss_pct = loss / size * 100
|
|
325
|
-
res = self._res_snapshot()
|
|
326
|
-
print(f" burst={size:>6}: recv={recvd} loss={loss}({loss_pct:.1f}%) "
|
|
327
|
-
f"rate={rate:.0f} evt/s cpu={res['cpu']:.0f}% mem={res['mem_mb']}MB")
|
|
328
|
-
|
|
329
|
-
if loss_pct > 5:
|
|
330
|
-
print(f" >> Loss exceeded 5%, stopping ramp")
|
|
331
|
-
break
|
|
332
|
-
|
|
333
|
-
await pub.close()
|
|
334
|
-
await sub.close()
|
|
335
|
-
print()
|
|
336
|
-
|
|
337
|
-
async def extreme2_max_connections(self):
|
|
338
|
-
"""Ramp concurrent connections, each publishing 1 evt/s."""
|
|
339
|
-
print("=" * 60)
|
|
340
|
-
print("EXTREME 2: Max concurrent connections")
|
|
341
|
-
print("=" * 60)
|
|
342
|
-
|
|
343
|
-
# One global subscriber
|
|
344
|
-
gsub = C(self.ws_url, "ex2_gsub")
|
|
345
|
-
await gsub.connect()
|
|
346
|
-
await gsub.sub(["ex2.*"])
|
|
347
|
-
await asyncio.sleep(0.1)
|
|
348
|
-
|
|
349
|
-
for n_conn in [10, 50, 100, 200, 500]:
|
|
350
|
-
clients = []
|
|
351
|
-
try:
|
|
352
|
-
for i in range(n_conn):
|
|
353
|
-
c = C(self.ws_url, f"ex2_c{i}")
|
|
354
|
-
await c.connect()
|
|
355
|
-
clients.append(c)
|
|
356
|
-
except Exception as e:
|
|
357
|
-
print(f" {len(clients)} connections: FAILED to open more ({e})")
|
|
358
|
-
break
|
|
359
|
-
|
|
360
|
-
# Each client sends 1 event
|
|
361
|
-
recvd = 0
|
|
362
|
-
stop = asyncio.Event()
|
|
363
|
-
|
|
364
|
-
async def _recv():
|
|
365
|
-
nonlocal recvd
|
|
366
|
-
while not stop.is_set():
|
|
367
|
-
msg = await gsub.recv(timeout=0.3)
|
|
368
|
-
if msg and msg.get("type") == "event":
|
|
369
|
-
recvd += 1
|
|
370
|
-
|
|
371
|
-
task = asyncio.create_task(_recv())
|
|
372
|
-
start = time.time()
|
|
373
|
-
for c in clients:
|
|
374
|
-
await c.pub("ex2.ping", {"from": c.name})
|
|
375
|
-
send_time = time.time() - start
|
|
376
|
-
|
|
377
|
-
await asyncio.sleep(max(2, n_conn / 200))
|
|
378
|
-
stop.set()
|
|
379
|
-
await task
|
|
380
|
-
|
|
381
|
-
res = self._res_snapshot()
|
|
382
|
-
print(f" {n_conn:>4} conns: recv={recvd}/{n_conn} "
|
|
383
|
-
f"send_time={send_time:.2f}s cpu={res['cpu']:.0f}% mem={res['mem_mb']}MB")
|
|
384
|
-
|
|
385
|
-
for c in clients:
|
|
386
|
-
await c.close()
|
|
387
|
-
|
|
388
|
-
await gsub.close()
|
|
389
|
-
print()
|
|
390
|
-
|
|
391
|
-
async def extreme3_max_message_size(self):
|
|
392
|
-
"""Ramp message size until failure."""
|
|
393
|
-
print("=" * 60)
|
|
394
|
-
print("EXTREME 3: Max message size")
|
|
395
|
-
print("=" * 60)
|
|
396
|
-
|
|
397
|
-
pub, sub, _ = await self._make_channel("ex3_pub", "ex3_sub", "ex3.*")
|
|
398
|
-
|
|
399
|
-
for size_kb in [100, 500, 1000, 2000, 5000]:
|
|
400
|
-
payload = "Z" * (size_kb * 1024)
|
|
401
|
-
latencies = []
|
|
402
|
-
ok = True
|
|
403
|
-
|
|
404
|
-
for _ in range(5):
|
|
405
|
-
try:
|
|
406
|
-
await pub.pub("ex3.big", {"p": payload})
|
|
407
|
-
except Exception as e:
|
|
408
|
-
print(f" {size_kb:>5} KB: SEND FAILED ({e})")
|
|
409
|
-
ok = False
|
|
410
|
-
break
|
|
411
|
-
|
|
412
|
-
if not ok:
|
|
413
|
-
break
|
|
414
|
-
|
|
415
|
-
await asyncio.sleep(max(1, size_kb / 500))
|
|
416
|
-
# Collect
|
|
417
|
-
for _ in range(20):
|
|
418
|
-
msg = await sub.recv(timeout=0.3)
|
|
419
|
-
if msg and msg.get("type") == "event":
|
|
420
|
-
ts = msg.get("timestamp", "")
|
|
421
|
-
try:
|
|
422
|
-
sent = datetime.fromisoformat(ts)
|
|
423
|
-
latencies.append((datetime.now(timezone.utc) - sent).total_seconds() * 1000)
|
|
424
|
-
except Exception:
|
|
425
|
-
pass
|
|
426
|
-
if msg is None:
|
|
427
|
-
break
|
|
428
|
-
|
|
429
|
-
await pub.drain()
|
|
430
|
-
res = self._res_snapshot()
|
|
431
|
-
if latencies:
|
|
432
|
-
avg = statistics.mean(latencies)
|
|
433
|
-
print(f" {size_kb:>5} KB: recv={len(latencies)}/5 "
|
|
434
|
-
f"avg={avg:.1f}ms cpu={res['cpu']:.0f}% mem={res['mem_mb']}MB")
|
|
435
|
-
else:
|
|
436
|
-
print(f" {size_kb:>5} KB: recv=0/5 cpu={res['cpu']:.0f}% mem={res['mem_mb']}MB")
|
|
437
|
-
|
|
438
|
-
await pub.close()
|
|
439
|
-
await sub.close()
|
|
440
|
-
print()
|
|
441
|
-
|
|
442
|
-
# ── Entry ──
|
|
443
|
-
|
|
444
|
-
async def run_all(self):
|
|
445
|
-
self._start_server()
|
|
446
|
-
print(f"Event Hub started on port {self.port}\n")
|
|
447
|
-
try:
|
|
448
|
-
await self.phase1(duration=600)
|
|
449
|
-
await self.extreme1_max_burst()
|
|
450
|
-
await self.extreme2_max_connections()
|
|
451
|
-
await self.extreme3_max_message_size()
|
|
452
|
-
print("All tests complete.")
|
|
453
|
-
finally:
|
|
454
|
-
if self._server:
|
|
455
|
-
self._server.should_exit = True
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
if __name__ == "__main__":
|
|
459
|
-
asyncio.run(Benchmark().run_all())
|