From 95c1c1d3eb9fdab7f8ee2f2066b105405d4852b1 Mon Sep 17 00:00:00 2001 From: CLU Date: Tue, 20 Oct 2026 13:52:49 -0500 Subject: [PATCH] Live UI: stop the page from jumping, and put Vitals in the bookmark bar The page rebuilt its whole header, bar and row list twice a second, so numbers snapped, columns resized as digits changed, and rows swapped places over a hundredth of a core. - header values are tabular-width and tweened toward the newest sample - machine-bar segments are keyed and reordered by flex order, so widths animate instead of being recreated mid-transition - rows are keyed DOM nodes updated in place, sorted on a 0.1-core grid with the previous order as tiebreak, with hysteresis at the idle threshold - tabs, banner and detail panes only re-render when their data changes - poll every 700ms and drop repeat frames from the 2s sampler bookmark.py puts a Vitals bookmark and a popup bookmarklet in the browser's bookmark bar. It recomputes Chromium's bookmark checksum (MD5 over the canonical node serialisation) and verifies it after writing, because a file that fails that check leaves the browser showing an empty bar while the file on disk still looks perfect. Refuses while the browser holds the profile, keeps a timestamped backup, idempotent. install.sh runs it. Co-developed-with: CLU --- README.md | 38 +++++ bookmark.py | 363 +++++++++++++++++++++++++++++++++++++++++ index.html | 308 ++++++++++++++++++++++++---------- install.sh | 15 ++ tests/test_bookmark.py | 115 +++++++++++++ 5 files changed, 754 insertions(+), 85 deletions(-) create mode 100644 bookmark.py create mode 100644 tests/test_bookmark.py diff --git a/README.md b/README.md index 8d70867..9c7fd00 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,44 @@ silently orphaned process. Options: `--port N`, `--host ADDR`, `VITALS_PORT` env var. +### The bookmark + +`install.sh` also puts Vitals in your browser's bookmark bar, as a bookmark and +as a popup bookmarklet that opens it in its own window. A tool you have to +remember the URL of does not get used. + +Chromium keeps the bookmark model in memory and rewrites the file on its next +save, so the browser has to be closed while that write happens. If it is running, +install prints the one command to run afterwards instead of failing: + +```bash +python3 bookmark.py --install # add bookmark + popup +python3 bookmark.py --install --wait 600 # wait for the browser to exit +python3 bookmark.py --status # is it there, is the file sane +python3 bookmark.py --remove # take them back out +``` + +The tool recomputes Chromium's bookmark checksum (MD5 over a canonical +serialisation of every node) and verifies it after writing, because a file that +fails that check leaves the browser showing an empty bar while the file on disk +still looks perfect. It keeps a timestamped backup of the file it edited. + +## Live updates + +The sampler runs every 2 seconds, because one sample costs about 400 ms of CPU +and a monitor that eats the machine is the joke writing itself. The page polls +faster than that and eases between frames, so the numbers move continuously +instead of snapping twice a second: + +- header values are tabular-width and tweened toward the newest sample +- machine-bar segments are keyed and reordered with flex `order`, so their + widths actually animate instead of being recreated mid-transition +- rows are keyed DOM nodes updated in place, sorted on a 0.1-core grid with the + previous order as tiebreak, so a row does not jump position over a hundredth + of a core +- a row appears at 0.05 cores but only leaves after it stays under 0.02, so + nothing flickers in and out at the threshold + ## Getting browser tabs Tabs come from the DevTools Protocol, which a browser only exposes if it was diff --git a/bookmark.py b/bookmark.py new file mode 100644 index 0000000..cdaa2b0 --- /dev/null +++ b/bookmark.py @@ -0,0 +1,363 @@ +#!/usr/bin/env python3 +"""Put Vitals in the browser's bookmark bar. + +Chromium-family browsers (Brave, Chrome, Chromium) store bookmarks as a JSON +file with an MD5 checksum over a canonical serialisation of every node. If the +checksum does not match, the browser silently falls back to Bookmarks.bak and +the edit disappears, so this tool recomputes it and refuses to write a file it +cannot verify. + +Checksum algorithm (components/bookmarks/browser/bookmark_codec.cc): + for each root folder, in order bookmark_bar, other, synced: + feed the root itself as a folder node + then every descendant, depth first, in order + folder node : id + name (UTF-16LE bytes) + b"folder" + url node : id + name (UTF-16LE bytes) + b"url" + url (UTF-8) + +Verified by reproducing the stored checksum of a real 13.8 MB profile file. + +Safe by construction: refuses while the browser owns the profile, writes through +a temp file with an atomic rename, keeps a timestamped backup, and re-reads the +file it just wrote to confirm the checksum lands. + +Usage: + python3 bookmark.py --install # add Vitals + Vitals popup + python3 bookmark.py --install --wait 600 # wait up to 10 min for browser exit + python3 bookmark.py --status # what is in the bar right now + python3 bookmark.py --remove # take them back out + python3 bookmark.py --install --profile-dir /path/to/Profile + python3 bookmark.py --install --dry-run +""" +from __future__ import annotations + +import argparse +import glob +import hashlib +import json +import os +import shutil +import sys +import time +import uuid + +VITALS_URL = "http://127.0.0.1:8791/" +BOOKMARKLET = ( + "javascript:(function(){window.open('http://127.0.0.1:8791/'," + "'vitals','width=1500,height=950,menubar=no,toolbar=no,location=no," + "status=no');})();void(0);" +) +ROOTS = ("bookmark_bar", "other", "synced") +WINDOWS_EPOCH_OFFSET = 11644473600 # seconds between 1601-01-01 and 1970-01-01 + + +# ---------------------------------------------------------------- checksum +def _u16(s: str) -> bytes: + return s.encode("utf-16-le") + + +def _feed_url(h, node) -> None: + h.update(str(node["id"]).encode()) + h.update(_u16(node.get("name") or "")) + h.update(b"url") + h.update((node.get("url") or "").encode("utf-8")) + + +def _feed_folder(h, node) -> None: + h.update(str(node["id"]).encode()) + h.update(_u16(node.get("name") or "")) + h.update(b"folder") + + +def _walk(h, node) -> None: + if node.get("type") == "url": + _feed_url(h, node) + else: + _feed_folder(h, node) + for child in node.get("children", []): + _walk(h, child) + + +def checksum(doc) -> str: + h = hashlib.md5() + for key in ROOTS: + root = doc["roots"].get(key) + if not root: + continue + _feed_folder(h, root) + for child in root.get("children", []): + _walk(h, child) + return h.hexdigest() + + +# ---------------------------------------------------------------- discovery +def candidate_profiles(): + home = os.path.expanduser("~") + pats = [ + # Flatpak builds keep their profile inside the sandbox. + f"{home}/.var/app/com.brave.Browser/config/BraveSoftware/Brave-Browser/Default", + f"{home}/.var/app/com.google.Chrome/config/google-chrome/Default", + f"{home}/.var/app/org.chromium.Chromium/config/chromium/Default", + # Native builds. + f"{home}/.config/BraveSoftware/Brave-Browser/Default", + f"{home}/.config/google-chrome/Default", + f"{home}/.config/chromium/Default", + # Chromium-family shells. + f"{home}/.config/microsoft-edge/Default", + f"{home}/.config/vivaldi/Default", + ] + out = [] + for p in pats: + if os.path.isfile(os.path.join(p, "Bookmarks")): + out.append(p) + return out + + +def profile_in_use(profile_dir: str) -> bool: + """True when a browser currently owns this profile. + + Chromium takes a SingletonLock in the user data dir (the parent of the + profile dir). Reading the bookmarks file under a live browser is pointless: + the in-memory model overwrites it on the next save. + """ + user_data_dir = os.path.dirname(profile_dir.rstrip("/")) + for name in ("SingletonLock", "SingletonSocket", "SingletonCookie"): + if os.path.exists(os.path.join(user_data_dir, name)): + return True + # Also catch a browser pointed at this profile with an explicit flag. + needle = os.path.abspath(profile_dir) + try: + pids = [p for p in os.listdir("/proc") if p.isdigit()] + except OSError: + return False + for pid in pids: + try: + with open(f"/proc/{pid}/cmdline", "rb") as f: + cmd = f.read().replace(b"\x00", b" ").decode("utf8", "replace") + except OSError: + continue + if "--type=" in cmd: + continue + if needle in cmd and any(b in cmd for b in ("brave", "chrome", "chromium", "edge", "vivaldi")): + return True + return False + + +def max_id(doc) -> int: + top = 0 + + def walk(node): + nonlocal top + try: + top = max(top, int(node.get("id", 0))) + except (TypeError, ValueError): + pass + for child in node.get("children", []): + walk(child) + + for key in ROOTS: + if doc["roots"].get(key): + walk(doc["roots"][key]) + return top + + +def now_micros() -> str: + return str(int((time.time() + WINDOWS_EPOCH_OFFSET) * 1_000_000)) + + +def find_ours(doc): + """Return (vitals_node, bookmarklet_node) currently in the bar, if any.""" + plain = bm = None + + def walk(node): + nonlocal plain, bm + if node.get("type") == "url": + url = node.get("url") or "" + if url == VITALS_URL: + plain = node + elif url.startswith("javascript:") and "8791" in url: + bm = node + for child in node.get("children", []): + walk(child) + + for key in ROOTS: + if doc["roots"].get(key): + walk(doc["roots"][key]) + return plain, bm + + +def make_node(node_id: int, name: str, url: str) -> dict: + stamp = now_micros() + return { + "date_added": stamp, + "date_last_used": "0", + "guid": str(uuid.uuid4()), + "id": str(node_id), + "name": name, + "type": "url", + "url": url, + } + + +# ---------------------------------------------------------------- operations +def load(profile_dir): + path = os.path.join(profile_dir, "Bookmarks") + with open(path, encoding="utf8") as f: + raw = f.read() + return path, raw, json.loads(raw) + + +def save(path, doc, raw_before) -> None: + doc["checksum"] = checksum(doc) + doc.pop("checksum_sha256", None) # browser rewrites it; absent is valid + text = json.dumps(doc, indent=3, sort_keys=True, ensure_ascii=True) + stamp = time.strftime("%Y%m%d-%H%M%S") + backup = f"{path}.pre-vitals-{stamp}.bak" + shutil.copy2(path, backup) + tmp = f"{path}.vitals.tmp" + with open(tmp, "w", encoding="utf8") as f: + f.write(text) + shutil.copystat(path, tmp) + os.replace(tmp, path) + print(f" backup: {backup}") + + # Verify what actually landed on disk. + with open(path, encoding="utf8") as f: + check = json.load(f) + if checksum(check) != check.get("checksum"): + raise SystemExit("REFUSING: checksum did not verify after write") + if len(raw_before) and not check["roots"].get("bookmark_bar"): + raise SystemExit("REFUSING: bookmark bar missing after write") + + +def install(profile_dir, dry_run=False, name="Vitals", popup_name="Vitals popup") -> int: + path, raw, doc = load(profile_dir) + plain, bm = find_ours(doc) + next_id = max_id(doc) + 1 + added = [] + + if plain is None: + doc["roots"]["bookmark_bar"].setdefault("children", []).append( + make_node(next_id, name, VITALS_URL) + ) + next_id += 1 + added.append(name) + elif plain.get("name") != name: + plain["name"] = name + added.append(f"{name} (renamed)") + + if bm is None: + doc["roots"]["bookmark_bar"].setdefault("children", []).append( + make_node(next_id, popup_name, BOOKMARKLET) + ) + added.append(popup_name) + + if not added: + print(" already installed, nothing to do") + return 0 + + if dry_run: + probe = json.loads(raw) + probe["checksum"] = checksum(doc) + print(f" dry run: would add {added} | new checksum {probe['checksum']}") + return 0 + + print(f" profile: {profile_dir}") + print(f" adding: {', '.join(added)}") + save(path, doc, raw) + print(f" checksum: {doc['checksum']} verified on disk") + return 0 + + +def remove(profile_dir, dry_run=False) -> int: + path, raw, doc = load(profile_dir) + plain, bm = find_ours(doc) + if plain is None and bm is None: + print(" nothing to remove") + return 0 + removed = [] + for key in ROOTS: + root = doc["roots"].get(key) + if not root: + continue + keep = [] + for child in root.get("children", []): + if child is plain or child is bm: + removed.append(child.get("name")) + else: + keep.append(child) + root["children"] = keep + if dry_run: + print(f" dry run: would remove {removed}") + return 0 + save(path, doc, raw) + print(f" removed: {removed}") + return 0 + + +def status(profile_dir) -> int: + _, _, doc = load(profile_dir) + plain, bm = find_ours(doc) + bar = doc["roots"]["bookmark_bar"] + print(f" profile : {profile_dir}") + print(f" bar : {len(bar.get('children', []))} items") + print(f" Vitals : {plain.get('name') + ' -> ' + plain['url'] if plain else 'not present'}") + print(f" popup : {bm.get('name') if bm else 'not present'}") + stored = doc.get("checksum", "") + good = stored == checksum(doc) + print(f" checksum: {'valid' if good else 'MISMATCH'}") + return 0 + + +def main(argv=None) -> int: + ap = argparse.ArgumentParser(description="Install Vitals into the bookmark bar") + ap.add_argument("--install", action="store_true") + ap.add_argument("--remove", action="store_true") + ap.add_argument("--status", action="store_true") + ap.add_argument("--profile-dir", default=None) + ap.add_argument("--name", default="Vitals") + ap.add_argument("--dry-run", action="store_true") + ap.add_argument("--wait", type=int, default=0, + help="seconds to wait for the browser to exit before giving up") + args = ap.parse_args(argv) + + if not (args.install or args.remove or args.status): + ap.print_help() + return 2 + + profiles = [args.profile_dir] if args.profile_dir else candidate_profiles() + if not profiles: + print("no Chromium-family profile found; pass --profile-dir") + return 1 + + targets = [] + for p in profiles: + if not os.path.isfile(os.path.join(p, "Bookmarks")): + print(f" skip {p}: no Bookmarks file") + continue + waited = 0 + while profile_in_use(p) and waited < args.wait: + if waited == 0: + print(f" browser is running on {p}; waiting up to {args.wait}s") + time.sleep(2) + waited += 2 + if profile_in_use(p): + print(f" skip {p}: browser holds the profile lock. Close the browser " + f"and re-run, or pass --wait N.") + continue + targets.append(p) + + if not targets: + return 1 + + for p in targets: + if args.status: + status(p) + elif args.remove: + remove(p, args.dry_run) + else: + install(p, args.dry_run, name=args.name) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/index.html b/index.html index 486c6ac..a5a2a88 100644 --- a/index.html +++ b/index.html @@ -115,6 +115,19 @@ input[type=search]:focus{border-color:#37424d} button{background:var(--panel);border:1px solid var(--line);color:var(--dim);border-radius:3px; padding:4px 9px;font-size:11px;cursor:pointer;font-family:inherit} button:hover{border-color:#37424d;color:var(--fg)} + +/* --- live-update stability: nothing moves unless its own value moved --- */ +.stat b{font-variant-numeric:tabular-nums} +.stat .v{display:inline-block;text-align:right} +.stat.busy .v{min-width:2.2ch} +.stat.cores .v{min-width:4.4ch} +.machine div{transition:width .9s linear} +.legend span{white-space:nowrap;font-variant-numeric:tabular-nums} +.legend span em{font-style:normal;display:inline-block;min-width:3.1em;text-align:right} +.rows{display:flex;flex-direction:column} +.row .bar i{transition:width .9s linear} +.num{font-variant-numeric:tabular-nums} +.stat, .legend span, .tabs .tr{will-change:auto} @@ -126,9 +139,9 @@ button:hover{border-color:#37424d;color:var(--fg)}
-
–of machine busy
-
–cores in use
-
–largest workload
+
–%of machine busy
+
– / –cores in use
+
–largest workload
@@ -174,21 +187,50 @@ function colorFor(key){ let h=0; for(const c of key) h=(h*31+c.charCodeAt(0))>>>0; return colorCache[key]=PALETTE[h%PALETTE.length]; } -const state = {snap:null, open:new Set(), details:{}, sort:"cpu", hideIdle:true, q:""}; +const state = {snap:null, open:new Set(), details:{}, sort:"cpu", hideIdle:true, q:"", + rows:new Map(), dets:new Map(), segs:new Map(), leg:new Map(), + rank:new Map(), low:new Map(), lastT:0, lastTabs:"", lastBanner:""}; +const ease = {busy:0, busyT:0, cores:0, coresT:0}; +const REDUCED = window.matchMedia("(prefers-reduced-motion: reduce)").matches; +const elBusy = $("#busy .v"), elCores = $("#cores .v"); + +/* write only when the value actually changed */ +function setText(el,txt){ if(el && el.__t!==txt){ el.textContent=txt; el.__t=txt; } } +function setHTML(el,html){ if(el && el.__h!==html){ el.innerHTML=html; el.__h=html; } } +function setW(el,pct){ const v=pct.toFixed(2)+"%"; if(el && el.__w!==v){ el.style.width=v; el.__w=v; } } +function keyed(parent,map,key,tag,cls){ + let el=map.get(key); + if(!el){ el=document.createElement(tag); if(cls) el.className=cls; parent.appendChild(el); map.set(key,el); } + return el; +} +function prune(map,keep){ for(const [k,el] of map){ if(!keep.has(k)){ el.remove(); map.delete(k); } } } + +/* one rAF loop eases the hero numbers toward the newest sample instead of snapping */ +function loop(){ + const step=(cur,tgt)=> { + const d=tgt-cur; + if(REDUCED || Math.abs(d)<0.02) return tgt; + return cur + d*0.16; + }; + ease.busy = step(ease.busy, ease.busyT); + ease.cores = step(ease.cores, ease.coresT); + setText(elBusy, String(Math.round(ease.busy))); + setText(elCores, ease.cores.toFixed(2)); + requestAnimationFrame(loop); +} +requestAnimationFrame(loop); function fmtAge(h){ if(h<1) return Math.round(h*60)+"m"; if(h<48) return h.toFixed(1)+"h"; return (h/24).toFixed(1)+"d"; } -function spark(series, color){ +function sparkPath(series){ const w=92,h=22; - if(!series||series.length<2) return ``; + if(!series||series.length<2) return ""; const max=Math.max(...series,0.1); - const pts=series.map((v,i)=>[i/(series.length-1)*w, h-2-(v/max)*(h-4)]); - const d=pts.map((p,i)=>(i?"L":"M")+p[0].toFixed(1)+" "+p[1].toFixed(1)).join(" "); - return ` - `; + return series.map((v,i)=>(i?"L":"M")+(i/(series.length-1)*w).toFixed(1)+" "+ + (h-2-(v/max)*(h-4)).toFixed(1)).join(" "); } function flagChips(w){ return (w.flags||[]).map(f=>`${f}`).join(""); @@ -197,96 +239,186 @@ function flagChips(w){ function render(){ const s=state.snap; if(!s) return; const cores=s.cores; - $("#meta").textContent = `sampled ${new Date(s.t*1000).toLocaleTimeString()} · ${s.sample_ms}ms per sample`; - if(s.host) $("#hostlabel").textContent = "workload map · " + s.host; - $("#busy").innerHTML = `${s.total_pct}%`; - $("#cores").innerHTML = `${s.total_busy.toFixed(2)} / ${cores}`; + setText($("#meta"), `live · ${new Date(s.t*1000).toLocaleTimeString()} · ${s.sample_ms}ms sample`); + if(s.host) setText($("#hostlabel"), "workload map · " + s.host); + ease.busyT = s.total_pct; ease.coresT = s.total_busy; + setText($("#coresmax"), " / " + cores); const top=s.workloads[0]; - $("#topname").textContent = top? top.name : "—"; - $("#topname").parentElement.querySelector("i").textContent = - top ? `largest workload · ${top.cpu.toFixed(2)} cores` : "largest workload"; - $("#selfcost").textContent = `${s.self_cost.toFixed(2)} cores`; + setText($("#topname"), top? top.name : "—"); + setText($("#topsub"), top ? `largest workload · ${top.cpu.toFixed(2)} cores` : "largest workload"); + setText($("#selfcost"), `${s.self_cost.toFixed(2)} cores`); - // machine bar: top 8 workloads + rest, then free + // machine bar: top 8 workloads + rest. Segments are keyed and reordered by + // flex order, so widths actually animate instead of being recreated each tick. const top8=s.workloads.slice(0,8); const used=top8.reduce((a,w)=>a+w.cpu,0); const rest=Math.max(0, s.total_busy-used); - let bar="", leg=""; - top8.forEach(w=>{ - bar+=`
`; - leg+=`${w.name} ${w.cpu.toFixed(2)}`; + const segs=[]; + top8.forEach((w,i)=>segs.push({key:w.key,name:w.name,cpu:w.cpu,color:colorFor(w.key)})); + if(rest>0.02) segs.push({key:"__rest",name:"rest",cpu:rest,color:"#3a434c"}); + const keepSeg=new Set(); + segs.forEach((sg,i)=>{ + keepSeg.add(sg.key); + const el=keyed($("#machine"),state.segs,sg.key,"div"); + setW(el, Math.min(100, sg.cpu/cores*100)); + if(el.__bg!==sg.color){ el.style.background=sg.color; el.__bg=sg.color; } + el.style.order=i; + const lg=keyed($("#legend"),state.leg,sg.key,"span"); + lg.style.order=i; + const sig=sg.name+"|"+sg.cpu.toFixed(2)+"|"+sg.color; + if(lg.__s!==sig){ + lg.innerHTML=`${esc(sg.name)} ${sg.cpu.toFixed(2)}`; + lg.__s=sig; + } }); - if(rest>0.02){ - bar+=`
`; - leg+=`rest ${rest.toFixed(2)}`; - } - $("#machine").innerHTML=bar; $("#legend").innerHTML=leg; + prune(state.segs,keepSeg); prune(state.leg,keepSeg); - $("#flags").innerHTML = (s.flags||[]).map(f=>{ + setHTML($("#flags"), (s.flags||[]).map(f=>{ const cls = f.flag==="leaked" ? "hot" : "warn"; return `${f.name}: ${f.flag}${f.flag==="leaked"?` · ${f.cpu.toFixed(2)} cores`:""}`; - }).join(""); + }).join("")); + $("#flags").style.display = (s.flags||[]).length ? "flex" : "none"; - // browser tabs - const tabRows = (s.tabs||[]).map(t=>{ - const meta = [shortUrl(t.url)]; - if(t.port) meta.push("port "+t.port); - if(t.proc_cores) meta.push(t.proc_cores+" cores in this browser"); - if(t.instance_age) meta.push(fmtAge(t.instance_age)+" old"); - return `
-
${esc(t.title)}${esc(meta.join(" · "))}
-
${t.port ? "instance "+t.port : esc(t.browser)}
-
${t.task_pct.toFixed(1)}%
-
${t.heap_mb} MB
-
`;}).join(""); - $("#tabs").innerHTML = tabRows || `
no tab data yet
`; + // browser tabs: rebuilt only when the tab data itself changes + const tabSig=JSON.stringify(s.tabs||[]); + if(state.lastTabs!==tabSig){ + state.lastTabs=tabSig; + const tabRows = (s.tabs||[]).map(t=>{ + const meta = [shortUrl(t.url)]; + if(t.port) meta.push("port "+t.port); + if(t.proc_cores) meta.push(t.proc_cores+" cores in this browser"); + if(t.instance_age) meta.push(fmtAge(t.instance_age)+" old"); + return `
+
${esc(t.title)}${esc(meta.join(" · "))}
+
${t.port ? "instance "+t.port : esc(t.browser)}
+
${t.task_pct.toFixed(1)}%
+
${t.heap_mb} MB
+
`;}).join(""); + $("#tabs").innerHTML = tabRows || `
no tab data yet
`; + } const brave = (s.workloads||[]).find(w=>/brave/i.test(w.name) && w.cpu > 0.05); - $("#banner").innerHTML = - (!s.brave_cdp && brave) ? `