mirror of https://github.com/garrytan/gstack.git
add sidebar footer RSS readout (polls /memory every 30s)
Footer now shows "<bun-rss> · <tab-count>" sourced from the /memory endpoint, polled every 30s. Color thresholds: orange warn at 2 GB Bun RSS or 50 tabs; red bad at 8 GB or 200 tabs (matches the tab-guardrail threshold landing in a later commit). The footer gives the user an early signal that the cliff is forming, instead of only learning when the OS OOM-kills the process. Backoff per Codex's flag: if a poll takes > 2s response time the sidebar drops to a 5-minute cadence until the next successful fast poll. The diagnostic shouldn't add load to a browser that's already unhealthy. Start/stop is wired to the existing setServerInfo() hook so the timer only runs while the sidebar is connected to a server. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
10495978e6
commit
98b2ae8103
|
|
@ -1137,6 +1137,21 @@ footer {
|
||||||
transition: color 150ms;
|
transition: color 150ms;
|
||||||
}
|
}
|
||||||
.footer-port:hover { color: var(--text-label); }
|
.footer-port:hover { color: var(--text-label); }
|
||||||
|
.footer-mem {
|
||||||
|
color: var(--text-meta);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
margin-right: 6px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition: color 150ms;
|
||||||
|
}
|
||||||
|
.footer-mem.warn {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
.footer-mem.bad {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
.port-input {
|
.port-input {
|
||||||
width: 56px;
|
width: 56px;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,7 @@
|
||||||
<button class="footer-btn" id="reload-sidebar" title="Reload sidebar">reload</button>
|
<button class="footer-btn" id="reload-sidebar" title="Reload sidebar">reload</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-right">
|
<div class="footer-right">
|
||||||
|
<span class="footer-mem" id="footer-mem" title="Process memory + tab count from $B memory (polled every 30s, paused if slow)"></span>
|
||||||
<span class="dot" id="footer-dot"></span>
|
<span class="dot" id="footer-dot"></span>
|
||||||
<span class="footer-port" id="footer-port" title="Click to change port"></span>
|
<span class="footer-port" id="footer-port" title="Click to change port"></span>
|
||||||
<input type="text" class="port-input" id="port-input" placeholder="34567" autocomplete="off" style="display:none">
|
<input type="text" class="port-input" id="port-input" placeholder="34567" autocomplete="off" style="display:none">
|
||||||
|
|
|
||||||
|
|
@ -292,6 +292,97 @@ async function connectSSE() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Memory Footer Readout ──────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Polls /memory every 30s and renders "RSS: 1.4 GB · 12 tabs" in the
|
||||||
|
// footer. Backs off to 5min if a poll takes > 2s (Codex flag — diagnostic
|
||||||
|
// shouldn't add load when the browser is already unhealthy). Uses Bearer
|
||||||
|
// auth like /refs above; /memory is a plain GET so EventSource semantics
|
||||||
|
// don't apply.
|
||||||
|
|
||||||
|
const MEM_POLL_FAST_MS = 30_000;
|
||||||
|
const MEM_POLL_SLOW_MS = 5 * 60_000;
|
||||||
|
const MEM_POLL_TIMEOUT_MS = 8_000;
|
||||||
|
const MEM_POLL_SLOW_THRESHOLD_MS = 2_000;
|
||||||
|
let memPollTimer = null;
|
||||||
|
let memPollMode = 'fast'; // 'fast' | 'slow'
|
||||||
|
|
||||||
|
function fmtBytesShort(n) {
|
||||||
|
if (typeof n !== 'number' || isNaN(n)) return '?';
|
||||||
|
if (n < 1024) return n + ' B';
|
||||||
|
if (n < 1024 * 1024) return (n / 1024).toFixed(0) + ' KB';
|
||||||
|
if (n < 1024 * 1024 * 1024) return (n / 1024 / 1024).toFixed(0) + ' MB';
|
||||||
|
return (n / 1024 / 1024 / 1024).toFixed(2) + ' GB';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMemFooter(snapshot) {
|
||||||
|
const el = document.getElementById('footer-mem');
|
||||||
|
if (!el) return;
|
||||||
|
const bunRss = snapshot?.bunServer?.rss ?? 0;
|
||||||
|
const tabCount = Array.isArray(snapshot?.tabs) ? snapshot.tabs.length : 0;
|
||||||
|
el.textContent = `${fmtBytesShort(bunRss)} · ${tabCount} tabs`;
|
||||||
|
// Color thresholds: ~2 GB Bun RSS or 50 tabs is "watch this"; ~8 GB or
|
||||||
|
// 200 tabs is "this is the cliff" (matches the 200-tab guardrail).
|
||||||
|
el.classList.remove('warn', 'bad');
|
||||||
|
if (bunRss > 8 * 1024 * 1024 * 1024 || tabCount > 200) el.classList.add('bad');
|
||||||
|
else if (bunRss > 2 * 1024 * 1024 * 1024 || tabCount > 50) el.classList.add('warn');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollMemoryOnce() {
|
||||||
|
if (!serverUrl || !serverToken) return { ok: false, slow: false };
|
||||||
|
const start = Date.now();
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${serverUrl}/memory`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${serverToken}` },
|
||||||
|
signal: AbortSignal.timeout(MEM_POLL_TIMEOUT_MS),
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
const elapsed = Date.now() - start;
|
||||||
|
if (!resp.ok) return { ok: false, slow: elapsed > MEM_POLL_SLOW_THRESHOLD_MS };
|
||||||
|
const snapshot = await resp.json();
|
||||||
|
renderMemFooter(snapshot);
|
||||||
|
return { ok: true, slow: elapsed > MEM_POLL_SLOW_THRESHOLD_MS };
|
||||||
|
} catch (err) {
|
||||||
|
const elapsed = Date.now() - start;
|
||||||
|
// Don't log every poll failure — common during browser restarts / restoring
|
||||||
|
// sessions. Only log on the slow path so the user sees something in the
|
||||||
|
// console if the diagnostic itself is misbehaving.
|
||||||
|
if (elapsed > MEM_POLL_SLOW_THRESHOLD_MS) {
|
||||||
|
console.debug('[gstack sidebar] /memory poll slow/failed:', elapsed, 'ms', err && err.message);
|
||||||
|
}
|
||||||
|
return { ok: false, slow: elapsed > MEM_POLL_SLOW_THRESHOLD_MS };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleNextMemPoll(delayMs) {
|
||||||
|
if (memPollTimer) clearTimeout(memPollTimer);
|
||||||
|
memPollTimer = setTimeout(async () => {
|
||||||
|
const { ok, slow } = await pollMemoryOnce();
|
||||||
|
if (!ok || slow) {
|
||||||
|
memPollMode = 'slow';
|
||||||
|
scheduleNextMemPoll(MEM_POLL_SLOW_MS);
|
||||||
|
} else {
|
||||||
|
// Successful + fast → back to fast cadence.
|
||||||
|
if (memPollMode === 'slow') memPollMode = 'fast';
|
||||||
|
scheduleNextMemPoll(MEM_POLL_FAST_MS);
|
||||||
|
}
|
||||||
|
}, delayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startMemPolling() {
|
||||||
|
if (memPollTimer) return; // already running
|
||||||
|
// Kick off an immediate poll so the footer populates within ~1s of sidebar
|
||||||
|
// open, instead of waiting 30s for the first cycle.
|
||||||
|
scheduleNextMemPoll(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopMemPolling() {
|
||||||
|
if (memPollTimer) {
|
||||||
|
clearTimeout(memPollTimer);
|
||||||
|
memPollTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Refs Tab ───────────────────────────────────────────────────
|
// ─── Refs Tab ───────────────────────────────────────────────────
|
||||||
|
|
||||||
async function fetchRefs() {
|
async function fetchRefs() {
|
||||||
|
|
@ -893,9 +984,16 @@ function updateConnection(url, token) {
|
||||||
chrome.runtime.sendMessage({ type: 'sidebarOpened' }).catch(() => {});
|
chrome.runtime.sendMessage({ type: 'sidebarOpened' }).catch(() => {});
|
||||||
connectSSE();
|
connectSSE();
|
||||||
connectInspectorSSE();
|
connectInspectorSSE();
|
||||||
|
startMemPolling();
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('footer-dot').className = 'dot';
|
document.getElementById('footer-dot').className = 'dot';
|
||||||
document.getElementById('footer-port').textContent = '';
|
document.getElementById('footer-port').textContent = '';
|
||||||
|
const memEl = document.getElementById('footer-mem');
|
||||||
|
if (memEl) {
|
||||||
|
memEl.textContent = '';
|
||||||
|
memEl.classList.remove('warn', 'bad');
|
||||||
|
}
|
||||||
|
stopMemPolling();
|
||||||
setActionButtonsEnabled(false);
|
setActionButtonsEnabled(false);
|
||||||
if (wasConnected) startReconnect();
|
if (wasConnected) startReconnect();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue