Skip to content

Commit 3ecdf43

Browse files
committed
Add UI benchmark and desktop mode
1 parent 86921b3 commit 3ecdf43

7 files changed

Lines changed: 243 additions & 12 deletions

File tree

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ data/models/*.pt
3535
data/traces/*
3636
!data/traces/.gitkeep
3737
!data/models/.gitkeep
38+
ui-overhead-results/
3839

3940
# Logs
4041
logs/

‎benchmark_ui_overhead.py‎

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
"""Measure CogniOS process-tree and system overhead in browser or desktop mode."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import csv
7+
import json
8+
import os
9+
import statistics
10+
import time
11+
from datetime import datetime, timezone
12+
from pathlib import Path
13+
14+
import psutil
15+
16+
17+
def summarize(samples: list[dict]) -> dict:
18+
fields = (
19+
"system_cpu_pct", "app_cpu_pct", "app_rss_mb", "app_processes",
20+
"memory_available_mb", "swap_used_mb", "disk_read_mb_s",
21+
"disk_write_mb_s", "network_recv_mb_s", "network_sent_mb_s",
22+
)
23+
if not samples:
24+
return {key: {"average": 0.0, "peak": 0.0, "minimum": 0.0} for key in fields}
25+
26+
return {
27+
key: {
28+
"average": round(statistics.mean(sample[key] for sample in samples), 2),
29+
"peak": round(max(sample[key] for sample in samples), 2),
30+
"minimum": round(min(sample[key] for sample in samples), 2),
31+
}
32+
for key in fields
33+
}
34+
35+
36+
def self_test() -> None:
37+
result = summarize([
38+
{"system_cpu_pct": 10, "app_cpu_pct": 20, "app_rss_mb": 100, "app_processes": 2,
39+
"memory_available_mb": 1000, "swap_used_mb": 0, "disk_read_mb_s": 1,
40+
"disk_write_mb_s": 2, "network_recv_mb_s": 3, "network_sent_mb_s": 4},
41+
{"system_cpu_pct": 30, "app_cpu_pct": 40, "app_rss_mb": 200, "app_processes": 4,
42+
"memory_available_mb": 800, "swap_used_mb": 1, "disk_read_mb_s": 3,
43+
"disk_write_mb_s": 4, "network_recv_mb_s": 5, "network_sent_mb_s": 6},
44+
])
45+
assert result["system_cpu_pct"] == {"average": 20, "peak": 30, "minimum": 10}
46+
assert result["app_rss_mb"] == {"average": 150, "peak": 200, "minimum": 100}
47+
assert summarize([])["system_cpu_pct"] == {"average": 0.0, "peak": 0.0, "minimum": 0.0}
48+
49+
50+
def main() -> None:
51+
parser = argparse.ArgumentParser(description=__doc__)
52+
parser.add_argument("--mode", choices=("browser", "desktop"))
53+
parser.add_argument("--pid", type=int, help="PID of the CogniOS main.py process")
54+
parser.add_argument("--ui-pid", type=int, help="Optional browser PID; include its process tree")
55+
parser.add_argument("--duration", type=int, default=60, help="Sample duration in seconds")
56+
parser.add_argument("--interval", type=float, default=1, help="Sample interval in seconds")
57+
parser.add_argument("--output", type=Path, default=Path("ui-overhead-results"))
58+
parser.add_argument("--self-test", action="store_true")
59+
args = parser.parse_args()
60+
61+
if args.self_test:
62+
self_test()
63+
print("Benchmark summary check passed.")
64+
return
65+
if not args.mode or not args.pid or args.duration < 1 or args.interval <= 0:
66+
parser.error("--mode, --pid, positive --duration, and positive --interval are required")
67+
68+
try:
69+
roots = [psutil.Process(args.pid)]
70+
if args.ui_pid:
71+
roots.append(psutil.Process(args.ui_pid))
72+
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess) as exc:
73+
parser.error(f"Could not access process PID(s): {exc}")
74+
for proc in roots:
75+
proc.cpu_percent(None)
76+
psutil.cpu_percent(None)
77+
78+
args.output.mkdir(parents=True, exist_ok=True)
79+
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
80+
stem = f"ui-overhead-{args.mode}-{stamp}"
81+
csv_path = args.output / f"{stem}.csv"
82+
json_path = args.output / f"{stem}.json"
83+
columns = (
84+
"timestamp", "elapsed_s", "mode", "system_cpu_pct", "memory_used_mb",
85+
"memory_available_mb", "swap_used_mb", "disk_read_mb_s", "disk_write_mb_s",
86+
"network_recv_mb_s", "network_sent_mb_s", "app_cpu_pct", "app_rss_mb",
87+
"app_processes", "pid", "parent_pid", "process_name", "process_cpu_pct",
88+
"process_rss_mb", "threads", "read_mb", "write_mb",
89+
)
90+
totals = []
91+
started_at = datetime.now(timezone.utc)
92+
started = time.monotonic()
93+
previous_elapsed = 0.0
94+
previous_disk = psutil.disk_io_counters()
95+
previous_network = psutil.net_io_counters()
96+
97+
with csv_path.open("w", newline="", encoding="utf-8") as output:
98+
writer = csv.DictWriter(output, fieldnames=columns)
99+
writer.writeheader()
100+
while time.monotonic() - started < args.duration:
101+
time.sleep(min(args.interval, max(0, args.duration - (time.monotonic() - started))))
102+
now = datetime.now(timezone.utc).isoformat()
103+
app_cpu = app_rss = process_count = 0
104+
rows = []
105+
seen = set()
106+
107+
for root in roots:
108+
try:
109+
processes = [root, *root.children(recursive=True)]
110+
except psutil.Error:
111+
processes = []
112+
for proc in processes:
113+
if proc.pid in seen:
114+
continue
115+
seen.add(proc.pid)
116+
try:
117+
info = proc.as_dict(attrs=["pid", "ppid", "name", "memory_info", "num_threads"])
118+
cpu = proc.cpu_percent(None)
119+
io = proc.io_counters()
120+
rss = info["memory_info"].rss / (1024 * 1024)
121+
app_cpu += cpu
122+
app_rss += rss
123+
process_count += 1
124+
rows.append({
125+
"pid": proc.pid, "parent_pid": info["ppid"], "process_name": info["name"],
126+
"process_cpu_pct": round(cpu, 2), "process_rss_mb": round(rss, 2),
127+
"threads": info["num_threads"],
128+
"read_mb": round(io.read_bytes / (1024 * 1024), 2) if io else "",
129+
"write_mb": round(io.write_bytes / (1024 * 1024), 2) if io else "",
130+
})
131+
except (psutil.Error, OSError):
132+
continue
133+
134+
memory = psutil.virtual_memory()
135+
system_cpu = psutil.cpu_percent(None)
136+
elapsed = time.monotonic() - started
137+
interval = max(elapsed - previous_elapsed, 0.001)
138+
disk = psutil.disk_io_counters()
139+
network = psutil.net_io_counters()
140+
disk_read = (
141+
max(0, disk.read_bytes - previous_disk.read_bytes) / 1048576 / interval
142+
if disk and previous_disk else 0
143+
)
144+
disk_write = (
145+
max(0, disk.write_bytes - previous_disk.write_bytes) / 1048576 / interval
146+
if disk and previous_disk else 0
147+
)
148+
network_recv = (
149+
max(0, network.bytes_recv - previous_network.bytes_recv) / 1048576 / interval
150+
if network and previous_network else 0
151+
)
152+
network_sent = (
153+
max(0, network.bytes_sent - previous_network.bytes_sent) / 1048576 / interval
154+
if network and previous_network else 0
155+
)
156+
previous_elapsed, previous_disk, previous_network = elapsed, disk, network
157+
sample = {
158+
"system_cpu_pct": system_cpu,
159+
"app_cpu_pct": app_cpu,
160+
"app_rss_mb": app_rss,
161+
"app_processes": process_count,
162+
"memory_available_mb": round(memory.available / (1024 * 1024), 2),
163+
"swap_used_mb": round(psutil.swap_memory().used / (1024 * 1024), 2),
164+
"disk_read_mb_s": round(disk_read, 3),
165+
"disk_write_mb_s": round(disk_write, 3),
166+
"network_recv_mb_s": round(network_recv, 3),
167+
"network_sent_mb_s": round(network_sent, 3),
168+
}
169+
totals.append(sample)
170+
shared = {
171+
"timestamp": now,
172+
"elapsed_s": round(elapsed, 2),
173+
"mode": args.mode,
174+
**sample,
175+
"memory_used_mb": round(memory.used / (1024 * 1024), 2),
176+
}
177+
for row in rows or [{}]:
178+
writer.writerow({**shared, **row})
179+
output.flush()
180+
181+
report = {
182+
"mode": args.mode,
183+
"started_at": started_at.isoformat(),
184+
"duration_seconds": round(time.monotonic() - started, 2),
185+
"sample_interval_seconds": args.interval,
186+
"samples": len(totals),
187+
"process_roots": [proc.pid for proc in roots],
188+
"summary": summarize(totals),
189+
"rss_note": "Process-tree RSS is summed and can double-count shared memory.",
190+
"csv": str(csv_path),
191+
}
192+
json_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
193+
print(json.dumps(report, indent=2))
194+
195+
196+
if __name__ == "__main__":
197+
main()

‎check_requirements.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"scikit-learn": "sklearn",
1616
"python-dotenv": "dotenv",
1717
"streamlit-autorefresh": "streamlit_autorefresh",
18+
"pyside6": "PySide6",
1819
"umap-learn": "umap",
1920
"google-genai": "google.genai",
2021
"pyyaml": "yaml",

‎dashboard/desktop.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Native desktop shell for the existing Streamlit dashboard."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
7+
from PySide6.QtCore import QUrl
8+
from PySide6.QtWidgets import QApplication, QMainWindow
9+
from PySide6.QtWebEngineWidgets import QWebEngineView
10+
11+
12+
def run_dashboard(url: str = "http://127.0.0.1:8501") -> int:
13+
app = QApplication(sys.argv)
14+
window = QMainWindow()
15+
window.setWindowTitle("CogniOS - System Observability")
16+
window.resize(1440, 900)
17+
18+
view = QWebEngineView()
19+
view.setUrl(QUrl(url))
20+
window.setCentralWidget(view)
21+
window.show()
22+
23+
return app.exec()

‎main.py‎

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import argparse
12
import os
23
import sys
34
import subprocess
45
import time
56
import threading
7+
import webbrowser
68

79
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
810
if BASE_DIR not in sys.path:
@@ -77,7 +79,7 @@ def _daemon_watchdog(daemon_script, telemetry_log_path, restart_delay=3):
7779
return process
7880

7981

80-
def main():
82+
def main(ui_mode="desktop"):
8183
_ensure_environment()
8284

8385
print("\n=======================================================")
@@ -102,7 +104,7 @@ def main():
102104
# Give the daemon a moment to initialise DB before dashboard connects
103105
time.sleep(2)
104106

105-
# 2. Start the Streamlit Dashboard
107+
# 2. Start the existing dashboard so localhost and launcher use one UI.
106108
dashboard_log = open(dashboard_log_path, 'w')
107109
dashboard_process = subprocess.Popen(
108110
[sys.executable, "-m", "streamlit", "run", dashboard_script, "--server.headless=true"],
@@ -113,15 +115,21 @@ def main():
113115

114116
print("\n=======================================================")
115117
print("All systems are running!")
116-
print("Dashboard is available at: http://localhost:8501")
118+
print(f"Opening dashboard in {ui_mode} mode...")
117119
print(f" Telemetry logs : tail -f '{telemetry_log_path}'")
118120
print(f" Dashboard logs : tail -f '{dashboard_log_path}'")
119121
print("=======================================================\n")
120122
print("Press Ctrl+C to stop all services.")
121123

122124
try:
123-
while True:
124-
time.sleep(1)
125+
if ui_mode == "browser":
126+
if not webbrowser.open("http://127.0.0.1:8501", new=2):
127+
raise RuntimeError("Could not open the dashboard in a browser.")
128+
while True:
129+
time.sleep(1)
130+
else:
131+
from dashboard.desktop import run_dashboard
132+
run_dashboard()
125133
except KeyboardInterrupt:
126134
print("\nStopping CogniOS System...")
127135
finally:
@@ -134,11 +142,7 @@ def main():
134142
except subprocess.TimeoutExpired:
135143
dashboard_process.kill()
136144

137-
try:
138-
dashboard_log.close()
139-
except Exception:
140-
pass
141-
145+
dashboard_log.close()
142146
print("Shutdown complete. Goodbye!")
143147

144148

@@ -147,6 +151,9 @@ def run_os_doctor():
147151
flag_anomaly()
148152

149153
if __name__ == "__main__":
154+
parser = argparse.ArgumentParser()
155+
parser.add_argument("--ui", choices=("browser", "desktop"), default="desktop")
156+
args = parser.parse_args()
150157
os_doctor_thread = threading.Thread(target=run_os_doctor, daemon=True)
151158
os_doctor_thread.start()
152-
main()
159+
main(args.ui)

‎pyproject.toml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ dependencies = [
1212
"scikit-learn>=1.4.0",
1313
"xgboost>=3.4.0",
1414
"streamlit>=1.61.1",
15-
"plotly>=6.9.0",
15+
"pyside6>=6.7.0",
1616
"streamlit-autorefresh>=1.0.1",
17+
"plotly>=6.9.0",
1718
"umap-learn>=0.5.5",
1819
]

‎requirements.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ requests>=2.31.0
1515
python-dotenv>=1.0.0
1616

1717
# Visualization & Dashboard
18+
PySide6>=6.7.0
1819
streamlit>=1.35.0
1920
streamlit-autorefresh>=1.0.1
2021
plotly>=5.20.0

0 commit comments

Comments
 (0)