Process monitors stop at the browser boundary, so the thing that usually pegs a machine is invisible: forty identical processes, no age, no tab names, no idea which page is the offender. Vitals resolves every process to a human identity (container, flatpak, systemd unit, tree root, or script), then joins the OS view to the browser's own DevTools counters so each tab carries its own page-work number beside the cores its browser instance is burning. The gap between those two numbers is the diagnosis: idle page, burning compositor. - dependency-free sampler over /proc, 2s cadence, 5 min in-memory history - minimal CDP client (WebSocket + JSON) with no third-party packages - both unit systems side by side: percent of machine and fractional cores - flags for leaked automation instances and live WebRTC calls - user service with CPUQuota so the monitor can never be the problem - identity tests that need no root, server, or browser MIT licensed. Runs on Linux, reads /proc, asks for no privilege.
107 lines
3.5 KiB
Python
107 lines
3.5 KiB
Python
"""Minimal dependency-free CDP client (sync WebSocket) for Vitals."""
|
|
import base64
|
|
import json
|
|
import os
|
|
import socket
|
|
import struct
|
|
import urllib.request
|
|
|
|
|
|
class WS:
|
|
def __init__(self, url, timeout=3.0):
|
|
assert url.startswith("ws://")
|
|
rest = url[5:]
|
|
host_port, path = rest.split("/", 1)
|
|
host, port = host_port.split(":")
|
|
self.sock = socket.create_connection((host, int(port)), timeout=timeout)
|
|
key = base64.b64encode(os.urandom(16)).decode()
|
|
req = (f"GET /{path} HTTP/1.1\r\nHost: {host_port}\r\nUpgrade: websocket\r\n"
|
|
f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\n"
|
|
f"Sec-WebSocket-Version: 13\r\n\r\n")
|
|
self.sock.sendall(req.encode())
|
|
buf = b""
|
|
while b"\r\n\r\n" not in buf:
|
|
chunk = self.sock.recv(4096)
|
|
if not chunk:
|
|
raise OSError("handshake closed")
|
|
buf += chunk
|
|
if b"101" not in buf.split(b"\r\n", 1)[0]:
|
|
raise OSError("handshake failed: " + buf[:120].decode("latin1"))
|
|
self.buf = buf.split(b"\r\n\r\n", 1)[1]
|
|
self._id = 0
|
|
|
|
def send(self, obj):
|
|
payload = json.dumps(obj).encode()
|
|
header = bytearray([0x81])
|
|
n = len(payload)
|
|
if n < 126:
|
|
header.append(0x80 | n)
|
|
elif n < 65536:
|
|
header.append(0x80 | 126)
|
|
header += struct.pack(">H", n)
|
|
else:
|
|
header.append(0x80 | 127)
|
|
header += struct.pack(">Q", n)
|
|
mask = os.urandom(4)
|
|
header += mask
|
|
masked = bytes(b ^ mask[i % 4] for i, b in enumerate(payload))
|
|
self.sock.sendall(bytes(header) + masked)
|
|
|
|
def _read_exact(self, n):
|
|
while len(self.buf) < n:
|
|
chunk = self.sock.recv(65536)
|
|
if not chunk:
|
|
raise OSError("closed")
|
|
self.buf += chunk
|
|
out, self.buf = self.buf[:n], self.buf[n:]
|
|
return out
|
|
|
|
def recv(self):
|
|
while True:
|
|
b0, b1 = self._read_exact(2)
|
|
opcode = b0 & 0x0F
|
|
n = b1 & 0x7F
|
|
if n == 126:
|
|
n = struct.unpack(">H", self._read_exact(2))[0]
|
|
elif n == 127:
|
|
n = struct.unpack(">Q", self._read_exact(8))[0]
|
|
payload = self._read_exact(n)
|
|
if opcode == 0x1:
|
|
return json.loads(payload)
|
|
if opcode == 0x8:
|
|
raise OSError("ws closed by peer")
|
|
if opcode == 0x9: # ping -> pong
|
|
self.sock.sendall(bytes([0x8A, 0x80]) + os.urandom(4))
|
|
|
|
def call(self, method, params=None, session_id=None):
|
|
self._id += 1
|
|
msg = {"id": self._id, "method": method, "params": params or {}}
|
|
if session_id:
|
|
msg["sessionId"] = session_id
|
|
self.send(msg)
|
|
while True:
|
|
resp = self.recv()
|
|
if resp.get("id") == self._id:
|
|
if "error" in resp:
|
|
raise RuntimeError(resp["error"].get("message"))
|
|
return resp.get("result", {})
|
|
|
|
def close(self):
|
|
try:
|
|
self.sock.close()
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def http_json(url, timeout=2.5):
|
|
with urllib.request.urlopen(url, timeout=timeout) as r:
|
|
return json.load(r)
|
|
|
|
|
|
def targets(port, timeout=2.5):
|
|
return http_json(f"http://127.0.0.1:{port}/json/list", timeout)
|
|
|
|
|
|
def browser_ws(port, timeout=2.5):
|
|
return http_json(f"http://127.0.0.1:{port}/json/version", timeout)["webSocketDebuggerUrl"]
|
|
|