A desktop dashboard that polls multiple hosts for CPU/memory stats in parallel instead of one at a time, and proves it with real measured timing rather than a claim.
Monitoring N hosts by contacting them one after another means total wait time grows linearly with N — if each host takes ~0.5s to respond, 6 hosts takes ~3s, 20 hosts takes ~10s, and so on. That's wasted time: the app is idle, waiting on network I/O, while it could be waiting on all hosts at once.
Parallel System Monitor polls every host concurrently using a thread pool, so total wait time is bounded by the slowest host, not the sum of all hosts.
This isn't a claim — it's measured. tests/core/test_dispatcher.py spins up 3 real mock agent processes, times polling them one-by-one, then times polling them with poll_all(), and asserts the parallel run is meaningfully faster:
Sequential time (one host at a time): 1.512s
Parallel time (poll_all): 0.509s
That's a real ~3x speedup on this machine, from an actual test run, not a synthetic benchmark. The app's own Parallel Connection Timeline panel (visible in the screenshot above) shows the same thing live: every host's poll bar starts at the same offset, because they were all dispatched together.
- Parallel polling — all hosts are polled concurrently via a
ThreadPoolExecutor, proven faster with real timing (see above) - Light/dark mode — every custom-drawn widget (timeline, history graph, gauge dials) is fully theme-aware, not just the built-in CustomTkinter widgets
- Per-host detail — click a host's chevron to expand real
started_at/finished_attimestamps for that poll, without rebuilding the row - Settings with live text scaling — a settings dialog with an Appearance toggle and a Display text-size slider (80%-200%) that previews instantly
- Search & sort — filter and reorder hosts entirely in-memory; neither ever triggers a re-poll
- Auto-refresh — optional periodic polling on a Tkinter
.after()timer (not a background thread), cleanly cancelled on window close - Health score — a single load-weighted score (
100 - max(cpu, memory)per host, averaged, offline hosts count as0) with a 24-hour trend graph - Event history — status-change events (e.g.
healthy -> critical) are recorded to SQLite only when a host's status actually changes, not on every poll
| View | Light | Dark |
|---|---|---|
| Dashboard | ![]() |
![]() |
| Host Offline | ![]() |
![]() |
| Search Filtering | ![]() |
![]() |
| Host Detail | ![]() |
![]() |
| Settings | ![]() |
![]() |
Data flows in one direction: a list of hosts goes in, every host gets polled at the same time, and each response is checked, labeled with a health status, then both saved to disk and shown on screen. No step reaches backward — the polling code doesn't know the database or the dashboard exist.
flowchart LR
A["Host list<br/>(HostConfig)"] --> B["Dispatcher<br/>polls all hosts in parallel"]
B --> C["Agent Client<br/>HTTP GET /stats"]
C --> D["Agent<br/>(Flask + psutil, one per host)"]
D --> E["Parser<br/>validates values,<br/>classifies status"]
E --> F["PollResult<br/>(one per host)"]
F --> G["Storage<br/>SQLite history + events"]
F --> H["GUI<br/>dashboard, timeline,<br/>health score"]
This one-way layering is deliberate: the core (dispatcher, parser, models) has zero dependency on the GUI or the database, so every layer is tested on its own — and it's what makes the "swap one layer to scale" story in the Scaling note below possible without touching anything else. The code lives in src/psm/ (core/, storage/, gui/) plus the simulated host in mock-agents/.
Status thresholds (parser.py), based on the higher of CPU or memory usage:
| Status | Range |
|---|---|
| Healthy | < 50% |
| Warning | 50% – 75% |
| Critical | > 75% |
| Offline | host unreachable or timed out |
poll_all() (dispatcher.py) uses a ThreadPoolExecutor with max_workers=10 by default. Since each poll is a short HTTP request, the work is I/O-bound, so threads (not processes) are sufficient — the GIL is released while waiting on the socket. This means:
- With ≤10 hosts, every host is dispatched at once, so wall-clock time ≈ the slowest single host's response time.
- Beyond 10 hosts, requests queue for a free worker thread, so wall-clock time grows in batches of 10 rather than linearly per host — still far better than fully sequential polling.
Requires Python 3.10+.
git clone https://github.com/Aayush29052006/Parallel-System-Monitor.git
cd Parallel-System-Monitorpython3 -m venv .venv
source .venv/bin/activate
pip install -e .Start the 6 mock agents the GUI expects (srv-cache-01 ... srv-storage-01 on ports 9001-9006):
python mock-agents/agent_server.py --port 9001 &
python mock-agents/agent_server.py --port 9002 &
python mock-agents/agent_server.py --port 9003 &
python mock-agents/agent_server.py --port 9004 &
python mock-agents/agent_server.py --port 9005 &
python mock-agents/agent_server.py --port 9006 &python -m venv .venv
.venv\Scripts\activate
pip install -e .PowerShell's trailing & doesn't background a process the way bash's does, so start each agent with Start-Process instead (or open 6 separate terminal windows and run one python mock-agents/agent_server.py --port ... in each):
Start-Process python -ArgumentList "mock-agents/agent_server.py --port 9001"
Start-Process python -ArgumentList "mock-agents/agent_server.py --port 9002"
Start-Process python -ArgumentList "mock-agents/agent_server.py --port 9003"
Start-Process python -ArgumentList "mock-agents/agent_server.py --port 9004"
Start-Process python -ArgumentList "mock-agents/agent_server.py --port 9005"
Start-Process python -ArgumentList "mock-agents/agent_server.py --port 9006"Each Start-Process opens its own window; close those windows (or use Stop-Process) to stop the agents.
No other code paths differ by OS — agent_server.py and the GUI use no OS-specific paths or signal handling. The one real platform difference we've hit is networking, not setup: connecting to a closed port on Windows' loopback interface times out instead of refusing immediately the way Linux/macOS do, which agent_client.py already accounts for (verified in CI on windows-latest).
python -m psm.gui.appFor screenshots/demos, an agent can be forced to report a fixed load value instead of real psutil data:
python mock-agents/agent_server.py --port 9001 --fake-load 85pip install pytest
pytestAll 47 tests pass, and run on Ubuntu, Windows, and macOS via GitHub Actions on every push and pull request to main.
- GUI: CustomTkinter
- Mock agents: Flask + psutil
- HTTP client: requests
- Storage: SQLite (standard library)
- Concurrency:
concurrent.futures.ThreadPoolExecutor(standard library) - Tests: pytest
Built by Aayush Chaudhari as a portfolio project demonstrating parallel I/O, real-time UI updates, and measured (not assumed) performance claims.









