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;
|
||||
}
|
||||
.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 {
|
||||
width: 56px;
|
||||
padding: 2px 6px;
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@
|
|||
<button class="footer-btn" id="reload-sidebar" title="Reload sidebar">reload</button>
|
||||
</div>
|
||||
<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="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">
|
||||
|
|
|
|||
|
|
@ -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 ───────────────────────────────────────────────────
|
||||
|
||||
async function fetchRefs() {
|
||||
|
|
@ -893,9 +984,16 @@ function updateConnection(url, token) {
|
|||
chrome.runtime.sendMessage({ type: 'sidebarOpened' }).catch(() => {});
|
||||
connectSSE();
|
||||
connectInspectorSSE();
|
||||
startMemPolling();
|
||||
} else {
|
||||
document.getElementById('footer-dot').className = 'dot';
|
||||
document.getElementById('footer-port').textContent = '';
|
||||
const memEl = document.getElementById('footer-mem');
|
||||
if (memEl) {
|
||||
memEl.textContent = '';
|
||||
memEl.classList.remove('warn', 'bad');
|
||||
}
|
||||
stopMemPolling();
|
||||
setActionButtonsEnabled(false);
|
||||
if (wasConnected) startReconnect();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue